【Spring 学习笔记(十四)】Spring AOP 通知中获取数据

956 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第17天,点击查看活动详情

写在前面😘

大一电子信息工程新生,请多多关照,希望能在掘金记录自己的学习历程!
【Spring 学习笔记】 系列教程基于 Spring 5.2.10.RELEASE讲解。

一、环境准备

添加AOP依赖

pom.xml文件里添加Spring AOPAspectJ的jar包依赖

<dependencies>
    <!--包含Spring AOP:有基本的AOP功能-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.2.10.RELEASE</version>
    </dependency>
    <!--AspectJ框架有更强大的AOP功能-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.5</version>
    </dependency>
</dependencies>

创建目标接口和实现类

/*UserService接口*/
public interface UserService {
    //自我介绍方法
    public string hello(String username,int age);
}
/*UserServiceImpl实现类*/
@Repository
public class UserServiceImpl implements UserService {
    @Override
    public String hello(String username, int age) {
        return "username:" + username + ",age:" + age;
    }
}

创建通知类

  • 创建通知类,并指定切入点
/*通知类*/
@Component//将这个类定义成 Bean
@Aspect//将这个Bean定义为切面
public class MyAdvice {
    //指定UserService类中的hello方法为切入点
    @Pointcut("execution(* com.bighorn.service.UserService.hello(..))")
    private void pt1(){}
}

创建Spring核心配置类

/*Spring核心配置类*/
@Configuration
@ComponentScan("com.bighorn") //开启注解扫描
@EnableAspectJAutoProxy //开启 AspectJ 的自动代理
public class SpringConfig {
}

编写运行程序

  • hello()方法中传入两个参数:"bighorn",18
public class App {
    public static void main(String[] args) throws SQLException {
        //获取配置类初始化容器
        ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class);
        //从容器中获取UserService对象
        UserService UserService = context.getBean(UserService.class);
        //调用UserService的方法
        String user = UserService.hello("bighorn", 18);
        System.out.println(user);
    }
}

二、JoinPoint和ProceedingJoinPoint

通知方法中如果有JoinPoint或者ProceedingJoinPoint参数,必须要把他们放在第一位

JoinPoint接口

在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象

适用场景:前置通知、后置通知、异常通知、返回通知

常用API

方法名功能
Signature getSignature()获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息
Object[ ] getArgs()获取传入目标方法的参数数组
Object getTarget()获取目标对象(被代理对象)
Object getThis()获取代理对象
  • 目标方法名: joinPoint.getSignature().getName());
  • 目标方法所属类的简单类名:joinPoint.getSignature().getDeclaringType().getSimpleName());
  • 目标方法所属类的类名:joinPoint.getSignature().getDeclaringTypeName();
  • 目标方法声明类型:Modifier.toString(joinPoint.getSignature().getModifiers()));

案例1

  • 创建一个前置通知,在通知方法中传入JoinPoint参数,并调用一些方法获取目标方法的相关信息。
/*前置通知*/
@Before("pt1()")
public void before(JoinPoint jp) {
    System.out.println("目标方法名为:" + jp.getSignature().getName());
    System.out.println("目标方法所属类的简单类名:" + jp.getSignature().getDeclaringType().getSimpleName());
    System.out.println("目标方法所属类的类名:" + jp.getSignature().getDeclaringTypeName());
    System.out.println("目标方法声明类型:" + Modifier.toString(jp.getSignature().getModifiers()));
    //获取传入目标方法的参数
    Object[] args = jp.getArgs();
    for (int i = 0; i < args.length; i++) {
        System.out.println("第" + (i+1) + "个参数为:" + args[i]);
    }
    //
    System.out.println("目标对象:" + jp.getTarget());
    System.out.println("代理对象:" + jp.getThis());
}

运行结果如下👇

image-20220618212930296

注意点

仔细观察,发现代理对象自己和被代理对象输出的内存地址一样啊!

其实不然,两个不同的对象内存地址不可能一样,代理类其实内部调用了目标类的方法,上面的目标类和代理类打印调用的是toString(),但是代理类调用的其实是目标类的toString(),所以打印的地址一样。但是你如果用==比较结果就是false了。

我们也可以通过getClass()方法获取它们的class对象并打印出来,比较一下就会发现不同了。

System.out.println("目标对象:" + jp.getTarget().getClass());
System.out.println("代理对象:" + jp.getThis().getClass());

image-20220618213816709

ProceedingJoinPoint接口

ProceedingJoinPoint接口 是JoinPoint的子接口,适用场景:只用在环绕通知中

而且环绕通知方法中也必须有ProceedingJoinPoint这个形参,不然就没意义了。

proceed方法

  • proceed()方法是有两个构造方法的
    • 调用无参数的proceed,当原始方法有参数,会在调用的过程中自动传入参数
    • 但是当需要修改原始方法的参数时,就只能采用有参数的proceed
Object proceed() throws Throwable //执行目标方法
Object proceed(Object[] var1) throws Throwable //传入的新的参数去执行目标方法 
  • 有了这个特性后,我们就可以在环绕通知中对原始方法的参数进行拦截过滤:如去除字符串最后一位的空格等操作。

案例2

对原始方法的第一个参数username进行拦截,把英文名变成大写

/*环绕通知*/
@Around("pt1()")
public Object around(ProceedingJoinPoint pjp) {
    Object result = null;
    try {
        //获取传入目标方法的参数
        Object[] args = pjp.getArgs();
        //前置通知
        System.out.println("目标方法执行前...原参数:"+Arrays.toString(args));
        //将传入的英文名变成大写
        args[0] = args[0].toString().toUpperCase();
        //用新的参数args执行目标方法
        result = pjp.proceed(args);
        //返回通知
        System.out.println("目标方法返回结果后...新参数:"+ Arrays.toString(args));
    } catch (Throwable e) {
        //异常通知
        System.out.println("执行目标方法异常后...");
        e.printStackTrace();
    }
    //后置通知
    System.out.println("目标方法执行后...");
    return result;
}

运行结果如下👇

image-20220618222929642

三、获取返回值

对于目标方法的返回值,只有返回通知@AfterReturing环绕通知@Around这两个通知类型可以获取,下面来介绍如何获取。

环绕通知获取返回值

  • 使用Object result = pjp.proceed( );获取返回值,result就是原始方法的返回值,我们不但可以获取,如果需要还可以进行修改。
  • 案例2中有类似代码:result = pjp.proceed(args)。这里就不演示了

返回通知获取返回值

  • @AfterReturning注解中,使用 returning 指定返回值名称,且需要与返回通知方法中形参名称保持一致。
/*返回后通知*/
@AfterReturning(value = "pt1()", returning = "ret")
public void afterReturning(Object ret) {
    System.out.println("返回通知,获取原始方法返回值--->" + ret);
}

运行结果如下👇

image-20220618225004460

四、获取异常

对于目标方法抛出异常,只有异常通知@AfterThrowing环绕通知@Around这两个通知类型可以获取,下面来介绍如何获取。

环绕通知获取异常

  • 使用try-catch语句就能获取异常了,异常处理在catch中进行。在案例2中有类似代码,这里就不多赘述了。
try{
    ret = pjp.proceed();
}catch(Throwable e){
    //根据业务需求处理异常
    e.printStackTrace();
}

异常通常获取异常

  • 异常通常和返回通知类似,也要在注解上加一句话。不过@AfterThrowing 注解中是使用 throwing 指定异常名称,需要与方法形参名相同。
//异常后通知
@AfterThrowing(value = "pt1()",throwing = "t")
public void afterThrowing(Throwable t) {
    System.out.println("异常通知获取到的异常"+t);
}

写在后面🍻

感谢观看啦✨
有什么不足,欢迎指出哦💖
掘金的运营同学审核辛苦了💗