Spring AOP进阶-AOP的延伸知识和进阶使用

从 0 开始深入学习 Spring 专栏目录总览

通过前面两章的学习,其实 Spring AOP 的核心知识就全部讲解完了,不过咱小册的学习可不会止步于此,咱继续深入学习一些 Spring AOP 的延伸知识,以及扩展的进阶使用。

1. AOP联盟【了解】

在 SpringFramework 2.0 之前,它还没有整合 AspectJ ,当时的 SpringFramework 还有一套相对低层级的实现,它也是 SpringFramework 原生的实现,而我们要了解它,首先要先了解一个组织:AOP 联盟

早在很久之前,AOP 的概念就被提出来了。同之前的 EJB 一样,作为一个概念、思想,它要有一批人来制定规范,于是就有了这样一个 AOP 联盟。这个联盟的人将 AOP 的这些概念都整理好,形成了一个规范 AOP 框架底层实现的 API ,并最终总结出了 5 种 AOP 通知类型。

咱要了解的,就是 AOP 联盟提出的这 5 种通知类型。

1.1 AOP联盟制定的通知类型

5 种通知类型分别为:

  • 前置通知
  • 后置通知(返回通知
  • 异常通知
  • 环绕通知
  • 引介通知

注意它跟 AspectJ 规定的 5 种通知类型的区别:它多了一个引介通知,少了一个后置通知。而且还有一个要注意的,AOP 联盟定义的后置通知实际上是返回通知( after-returning ),而 AspectJ 的后置通知是真的后置通知,与返回通知是两码事。

1.2 SpringFramework中对应的通知接口

AOP 联盟定义的 5 种通知类型在 SpringFramework 中都有对应的接口定义:

  • 前置通知:org.springframework.aop.MethodBeforeAdvice
  • 返回通知:org.springframework.aop.AfterReturningAdvice
  • 异常通知:org.springframework.aop.ThrowsAdvice
  • 环绕通知:org.aopalliance.intercept.MethodInterceptor
  • 引介通知:org.springframework.aop.IntroductionAdvisor

注意!环绕通知的接口是 AOP 联盟原生定义的接口(不是 cglib 的那个 MethodInterceptor )!小伙伴们可以先思考一下为什么会是这样。

其实答案不难理解,由于 SpringFramework 是基于 AOP 联盟制定的规范来的,所以自然会去兼容原有的方案。又由于咱之前写过原生的动态代理,知道它其实就是环绕通知,所以 SpringFramework 要在环绕通知上拆解结构,自然也会保留原本环绕通知的接口支持。

了解这部分的知识,在后面咱分析 Spring AOP 的原理时,看到一些特殊的 API 接口时,就不会觉得奇怪或者陌生了,现在小伙伴们只是有个基本的印象即可。

2. 切面类的通知方法参数【掌握】

在上一章的环绕通知编写中,咱提到了一个特殊的接口 ProceedingJoinPoint ,它的具体使用,以及切面类中的通知方法参数等等,咱都有必要来学习一下。

其实在之前的代码中,或许有的小伙伴就已经产生很强的不适感了:这所有的日志打印都是一样的,我也不知道哪个日志打印是哪个方法触发的,这咋区分呢? 所以,我们得想个办法,把被增强的方法,以及对应的目标对象的信息拿到才行。(原生动态代理都行,到 AOP 就不行了?这肯定不合理)

将 b_aspectj 的代码完整的复制一份到 c_joinpoint 包下。

本节源码位置:com.linkedbear.spring.aop.c_joinpoint 。

2.1 JoinPoint的使用

其实切面类的通知方法,咱都可以在方法的参数列表上加上切入点的引用,就像这样:(咱以 beforePrint 方法为例)

@Before("execution(public * com..FinanceService.*(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println("Logger beforePrint run ......");
}

这样写之后,重新运行程序不会有任何错误,说明这样写是被允许的,但咱更关心的是,能从这个 JoinPoint 中得到什么呢?

直接在方法中调用 joinPoint 的方法,可以发现它能获取到这么多东西:

Spring AOP进阶-AOP的延伸知识和进阶使用

这么多内容,咱选几个比较重要的来讲解。

2.1.1 getTarget & getThis

getTarget 方法是最容易被理解的,咱可以简单的测试一下效果:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(joinPoint.getTarget());
    System.out.println("Logger beforePrint run ......");
}

运行 main 方法,控制台会打印 FinanceService 的信息:

com.linkedbear.spring.aop.c_joinpoint.service.FinanceService@4c309d4d
Logger beforePrint run ......
FinanceService 收钱 === 123.45

包括使用 Debug 打断点,识别到的 FinanceService 也是未经过代理的原始对象:

Spring AOP进阶-AOP的延伸知识和进阶使用

那相对的,getThis 方法返回的就是代理对象咯?咱也可以来打印一下:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(joinPoint.getTarget());
    System.out.println(joinPoint.getThis());
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台打印了两个一模一样的 FinanceService :

com.linkedbear.spring.aop.c_joinpoint.service.FinanceService@4c309d4d
com.linkedbear.spring.aop.c_joinpoint.service.FinanceService@4c309d4d
Logger beforePrint run ......
FinanceService 收钱 === 123.45

??????怎么个情况?难道是我们推理错了吗?用 Debug 看一眼:

Spring AOP进阶-AOP的延伸知识和进阶使用

诶呦吓一跳,getThis 肯定还是获取到代理对象才是啦。那为什么原始的目标对象,与代理对象的控制台打印结果是一样的呢?

其实从上面的截图中也能猜到端倪:它增强了 equals 方法,增强了 hashcode 方法,就是没有增强 toString 方法,那当然就执行目标对象的方法啦,自然也就打印原来的目标对象的全限定名了。

2.1.2 getArgs

这个方法也是超级好理解,它可以获取到被拦截的方法的参数列表。快速的来测试一下吧:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(Arrays.toString(joinPoint.getArgs()));
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台打印出了 addMoney 方法传入的 123.45 :

[123.45]
Logger beforePrint run ......
FinanceService 收钱 === 123.45

2.1.3 getSignature

这个方法,从名上看是获取签名,关键是这个签名是个啥?不知道,猜不出来,干脆先打印一把吧:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(joinPoint.getSignature());
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台打印的是被拦截的方法的全限定名等信息:

void com.linkedbear.spring.aop.c_joinpoint.service.FinanceService.addMoney(double)
Logger beforePrint run ......
FinanceService 收钱 === 123.45

哦,突然明白了,合着它打印的是这个被拦截的方法的签名啊!那是不是还可以顺便拿到方法的信息呢?

直接调用 getSignature() 的方法,发现这里面只有类的信息,没有方法的信息:

Spring AOP进阶-AOP的延伸知识和进阶使用

诶?那可奇了怪了,既然基于 AspectJ 的 AOP 是对方法的拦截,那理所应当的应该能拿到方法的信息才对呀!那当然,肯定能拿到,只是缺少了一点点步骤而已。

既然是基于方法的拦截,那获取到的 Signature 就应该可以强转为一种类似于 Method 的 Signature ,刚好还真就有这么一个 MethodSignature 的接口!

所以,咱就可以这样写了:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    
    System.out.println("Logger beforePrint run ......");
}

那既然是这样,MethodSignature 中一定能拿到方法的信息了!果不其然,这个接口还真就定义了获取 Method 的方法:

public interface MethodSignature extends CodeSignature {
    Class getReturnType();
	Method getMethod();
}

so ,我们就可以打印出这个方法的信息了:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    Method method = signature.getMethod();
    System.out.println(method.getName());
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台可以打印出方法的信息:

addMoney
Logger beforePrint run ......
FinanceService 收钱 === 123.45

其实 Signature 的 getName 方法,就相当于拿到 Method 后再调 getName 方法了,小伙伴们可以自行测试一下。

至此,其实我们就可以完成前面说的需求了。

2.1.4 需求的改造

重新修改 addMoney 方法的逻辑,就可以很简单轻松的完成一开始说的 “不知道哪个日志打印是哪个方法触发” 的需求了:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println("Logger beforePrint run ......");
    System.out.println("被拦截的类:" + joinPoint.getTarget().getClass().getName());
    System.out.println("被拦截的方法:" + ((MethodSignature) joinPoint.getSignature()).getMethod().getName());
    System.out.println("被拦截的方法参数:" + Arrays.toString(joinPoint.getArgs()));
}

重新运行 main 方法,控制台打印出了我们预期的需求效果:

Logger beforePrint run ......
被拦截的类:com.linkedbear.spring.aop.c_joinpoint.service.FinanceService
被拦截的方法:addMoney
被拦截的方法参数:[123.45]
FinanceService 收钱 === 123.45

2.2 ProceedingJoinPoint的扩展

上一章中我们提前使用了 ProceedingJoinPoint 这个家伙,而它是基于 JoinPoint 的扩展,它扩展的方法只有 proceed 方法,也就是那个能让我们在环绕通知中显式执行目标对象的目标方法的那个 API 。

不过有一点要注意:proceed 方法还有一个带参数的重载方法:

public Object proceed(Object[] args) throws Throwable;

由此可以说明一点:在环绕通知中,可以自行替换掉原始目标方法执行时传入的参数列表

其实这个一点也不奇怪,想想在之前的动态代理案例中,咱不就是可以随便改参数的嘛。

2.3 返回通知和异常通知的特殊参数

之前我们在写返回通知和异常通知时,还有一个小问题没有解决:返回通知中我们要拿到方法的返回值,异常通知中我们要拿到具体的异常抛出。这个呢,其实非常容易解决。

咱先把之前的代码再拿出来:(顺便简化了一下)

@AfterReturning("execution(* com..FinanceService.subtractMoney(double))")
public void afterReturningPrint() {
    System.out.println("Logger afterReturningPrint run ......");
}

@AfterThrowing("defaultPointcut()")
public void afterThrowingPrint() {
    System.out.println("Logger afterThrowingPrint run ......");
}

想拿到返回值或者异常非常简单,两个步骤。

首先在方法的参数列表中声明一个 result 或者 e :

@AfterReturning("execution(* com..FinanceService.subtractMoney(double))")
public void afterReturningPrint(Object retval) {
    System.out.println("Logger afterReturningPrint run ......");
    System.out.println("返回的数据:" + retval);
}

@AfterThrowing("defaultPointcut()")
public void afterThrowingPrint(Exception e) {
    System.out.println("Logger afterThrowingPrint run ......");
}

注意只是这样写了之后,此时运行 main 方法是不好使的,是拿不到返回值的!我们还需要告诉 SpringFramework ,我拿了一个名叫 retval 的参数来接这个方法返回的异常,拿一个名叫 e 的参数来接收方法抛出的异常,反映到代码上就应该是这样:

@AfterReturning(value = "execution(* com..FinanceService.subtractMoney(double))", returning = "retval")
public void afterReturningPrint(Object retval) {
    System.out.println("Logger afterReturningPrint run ......");
    System.out.println("返回的数据:" + retval);
}

@AfterThrowing(value = "defaultPointcut()", throwing = "e")
public void afterThrowingPrint(Exception e) {
    System.out.println("Logger afterThrowingPrint run ......");
    System.out.println("抛出的异常:" + e.getMessage());
}

这样再运行 main 方法,控制台才会打印出方法的返回值:

FinanceService 付钱 === 543.21
Logger afterReturningPrint run ......
返回的数据:543.21

异常的信息接收同理,小伙伴们可以自行测试。

3. 多个切面的执行顺序【熟悉】

日常开发中,或许我们会碰到一些特殊的情况:一个方法被多个切面同时增强了,这个时候如何控制好各个切面的执行顺序,以保证最终的运行结果能符合最初设计,这个也是非常重要的,咱有必要来研究一下多个切面的执行顺序问题。

本节源码位置:com.linkedbear.spring.aop.d_order 。

3.1 代码准备

咱先把测试的代码准备一下,很简单,咱只声明两个切面和一个 Service 即可:

@Service
public class UserService {
    
    public void saveUser(String id) {
        System.out.println("UserService 保存用户" + id);
    }
}
@Component
@Aspect
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void printLog() {
        System.out.println("LogAspect 打印日志 ......");
    }
}
@Component
@Aspect
public class TransactionAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void beginTransaction() {
        System.out.println("TransactionAspect 开启事务 ......");
    }
}

然后,编写配置类,开启注解 AOP :

@Configuration
@ComponentScan("com.linkedbear.spring.aop.d_order")
@EnableAspectJAutoProxy
public class AspectOrderConfiguration {
    
}

最后,编写启动类,用上面的配置类驱动 IOC 容器:

public class AspectOrderApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AspectOrderConfiguration.class);
        UserService userService = ctx.getBean(UserService.class);
        userService.saveUser("abc");
    }
}

运行 main 方法,控制台可以打印出两个切面的前置通知:

LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
UserService 保存用户abc

3.2 预设的顺序?

观察这个打印的结果,它是打印日志在前,开启事务在后,这难不成是因为我先写的 LogAspect ,后写的 TransactionAspect ,它就按照我的顺序来了?那不可能啊,即便我后写 TransactionAspect ,也是日志打印在前啊!所以它一定有一个默认的预设规则。

小伙伴可以先猜测一下预设的规则是什么,小册在这里再写一个切面,想必写完这个切面后,基本上顺序就非常容易推理了:

@Component
@Aspect
public class AbcAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void abc() {
        System.out.println("abc abc abc");
    }
}

重新运行 main 方法,发现 AbcAspect 的前置通知打印在 LogAspect 之前!

由此是不是咱就可以推测出预设的顺序了:默认的切面执行顺序,是按照字母表的顺序来的

严谨一点,排序规则其实是根据切面类的 unicode 编码,按照十六进制排序得来的,unicode 编码靠前的,那自然就会排在前面。(作者个人习惯称其为字典表顺序)

3.3 显式声明执行顺序

那我们不能因为这个毛病,就搞得调整顺序就必须改类名吧,一定有更好的方案才对。

还记得之前在第 26 章,小册讲解 BeanPostProcessor 的 javadoc 中提到的 Ordered 接口吗?而且在 IOC 原理的 BeanPostProcessor 的初始化部分,也提到过有关排序的接口,也涉及到了这个 Ordered 接口。不过前面我们一直都没有实际演示 Ordered 接口的使用,这里咱就来搞一下。

现在咱希望让事务控制的切面提早执行,让它在所有切面之前,那么我们就可以这样写:给 TransactionAspect 实现 Ordered 接口,并声明 getOrder 的返回值:

@Component
@Aspect
public class TransactionAspect implements Ordered {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void beginTransaction() {
        System.out.println("TransactionAspect 开启事务 ......");
    }
    
    @Override
    public int getOrder() {
        return 0;
    }
}

这个值设置成多少呢?咱先放个 0 试试,运行 main 方法,观察控制台的打印:

TransactionAspect 开启事务 ......
abc abc abc
LogAspect 打印日志 ......
UserService 保存用户abc

咦,发现事务切面的前置通知已经提前执行了,说明 0 这个顺序已经是提早了的,那最晚的时机对应的 order 值是什么呢?

3.3.1 默认的排序值?

很简单,Integer 的最大值 2147483647 嘛!其实在 Ordered 接口中,就有这两个常量的定义:

public interface Ordered {
	int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
	int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

那我们把这个值调到最低试一下?

@Override
public int getOrder() {
    return Ordered.LOWEST_PRECEDENCE;
}

重新运行 main 方法,发现事务的切面又回去了:

abc abc abc
LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
UserService 保存用户abc

那到底默认值是啥呢?咱把 order 值往上调一个点试试?

@Override
public int getOrder() {
    return Ordered.LOWEST_PRECEDENCE - 1;
}

再次重新运行 main 方法,事务的切面打印又上去了。。。

所以得出结论:在不显式声明 order 排序值时,默认的排序值是 Integer.MAX_VALUE 。

3.3.2 另一种声明办法

除了使用 Ordered 接口,还有通过注解的方式声明:@Order 。

这次我们在 LogAspect 上标注 @Order 注解,并声明一下排序值:

@Component
@Aspect
@Order(0)
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void printLog() {
        System.out.println("LogAspect 打印日志 ......");
    }
}

重新运行 main 方法,发现日志切面的打印提到最早了:

LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
abc abc abc
UserService 保存用户abc

说明 @Order 注解也可以实现同样的效果。

4. 同切面的多个通知执行顺序【熟悉】

除了多个切面的顺序问题,如果同一个切面定义了多个相同类型的通知,它的执行顺序又是怎么样呢?咱也来研究一下。

这次编码的内容就少多了,直接在 AbsAspect 中添加一个方法 def 即可:

@Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
public void def() {
    System.out.println("def def def");
}

直接重新运行 main 方法,控制台先后打印了 abc 和 def 的内容:

LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
abc abc abc
def def def
UserService 保存用户abc

原因是什么呢?估计小伙伴们也能猜到了,跟上面的逻辑一样,都是根据unicode编码顺序(字典表顺序)来的。

至于怎么搞,小伙伴们立马想到办法了吧!直接在方法上标注 @Order 注解就 OK (方法没办法实现接口的嘛)!

@Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
@Order(Ordered.HIGHEST_PRECEDENCE)
public void def() {
    System.out.println("def def def");
}

运行 main 方法,发现 def 的通知内容并没有被提前执行。。。看来这个办法行不通。。。

那怎么办呢?哎,这还真没办法。。。只能靠方法名去区分了。。。(是不是很无奈)

好了,有关切面、通知的执行顺序的研究,咱就到这里了。

5. 代理对象调用自身的方法【熟悉】

有一些特殊的场景下,我们产生的这些代理对象,会出现自身调用自身的另外方法的。下面我们也来演示一下这个现象。

本节源码位置:com.linkedbear.spring.aop.e_aopcontext 。

5.1 代码准备

测试代码还是三个类,不再重复了:

@Service
public class UserService {
    
    public void update(String id, String name) {
        this.get(id);
        System.out.println("修改指定id的name。。。");
    }
    
    public void get(String id) {
        System.out.println("获取指定id的user。。。");
    }
}
@Component
@Aspect
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.e_aopcontext.service.UserService.*(..))")
    public void beforePrint() {
        System.out.println("LogAspect 前置通知 ......");
    }
}
@Configuration
@ComponentScan("com.linkedbear.spring.aop.e_aopcontext")
@EnableAspectJAutoProxy
public class AopContextConfiguration {
    
}

然后,依然是编写测试启动类:

public class AopContextApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AopContextConfiguration.class);
        UserService userService = ctx.getBean(UserService.class);
        userService.update("abc", "def");
    }
}

这样写完之后,运行 main 方法,发现控制台只打印了一次 LogAspect 的切面打印:

LogAspect 前置通知 ......
获取指定id的user。。。
修改指定id的name。。。

如果需求是每次调用 UserService 的方法都需要打印切面日志,应该怎么处理呢?

5.2 不优雅的解决方案

可能有的小伙伴想到了一个能行但不太优雅的解决方案,就是利用依赖注入的特性,把自己注入进来,之后不用 this.get ,换用 userService.get 方法:

@Service
public class UserService {
    
    @Autowired
    UserService userService;
    
    public void update(String id, String name) {
        // this.get(id);
        userService.get(id);
        System.out.println("修改指定id的name。。。");
    }

重新运行 main 方法,控制台确实打印了两次切面日志:

LogAspect 前置通知 ......
LogAspect 前置通知 ......
获取指定id的user。。。
修改指定id的name。。。

但是吧。。。这样写真的好吗。。。有木有感觉怪怪的。。。难不成SpringFramework 就没有考虑到这个问题吗?

5.3 正确的解决方案:AopContext

当然还是得有的,SpringFramework 从一开始就考虑到这个问题了,于是它提供了一个 AopContext 的类,使用这个类,可以在代理对象中取到自身,它的使用方法很简单:

public void update(String id, String name) {
    ((UserService) AopContext.currentProxy()).get(id);
    System.out.println("修改指定id的name。。。");
}

使用 AopContext.currentProxy() 方法就可以取到代理对象的 this 了。

不过这样直接写完之后,运行是不好使的,它会抛出一个异常:

Exception in thread "main" java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.

这个异常的大致含义是,没有开启一个 exposeProxy 的属性,导致无法暴露出代理对象,从而无法获取。那开启 exposeProxy 这个属性的位置在哪里呢?好巧不巧,它是在我们一开始学习注解 AOP 的那个 @EnableAspectJAutoProxy 上:

@Configuration
@ComponentScan("com.linkedbear.spring.aop.e_aopcontext")
@EnableAspectJAutoProxy(exposeProxy = true) // 暴露代理对象
public class AopContextConfiguration {
    
}

它的默认值是 false ,改为 true 之后,再运行 main 方法,就可以达到同样的效果了,控制台会打印两次切面日志。

通过前面两章的学习,其实 Spring AOP 的核心知识就全部讲解完了,不过咱小册的学习可不会止步于此,咱继续深入学习一些 Spring AOP 的延伸知识,以及扩展的进阶使用。

1. AOP联盟【了解】

在 SpringFramework 2.0 之前,它还没有整合 AspectJ ,当时的 SpringFramework 还有一套相对低层级的实现,它也是 SpringFramework 原生的实现,而我们要了解它,首先要先了解一个组织:AOP 联盟

早在很久之前,AOP 的概念就被提出来了。同之前的 EJB 一样,作为一个概念、思想,它要有一批人来制定规范,于是就有了这样一个 AOP 联盟。这个联盟的人将 AOP 的这些概念都整理好,形成了一个规范 AOP 框架底层实现的 API ,并最终总结出了 5 种 AOP 通知类型。

咱要了解的,就是 AOP 联盟提出的这 5 种通知类型。

1.1 AOP联盟制定的通知类型

5 种通知类型分别为:

  • 前置通知
  • 后置通知(返回通知
  • 异常通知
  • 环绕通知
  • 引介通知

注意它跟 AspectJ 规定的 5 种通知类型的区别:它多了一个引介通知,少了一个后置通知。而且还有一个要注意的,AOP 联盟定义的后置通知实际上是返回通知( after-returning ),而 AspectJ 的后置通知是真的后置通知,与返回通知是两码事。

1.2 SpringFramework中对应的通知接口

AOP 联盟定义的 5 种通知类型在 SpringFramework 中都有对应的接口定义:

  • 前置通知:org.springframework.aop.MethodBeforeAdvice
  • 返回通知:org.springframework.aop.AfterReturningAdvice
  • 异常通知:org.springframework.aop.ThrowsAdvice
  • 环绕通知:org.aopalliance.intercept.MethodInterceptor
  • 引介通知:org.springframework.aop.IntroductionAdvisor

注意!环绕通知的接口是 AOP 联盟原生定义的接口(不是 cglib 的那个 MethodInterceptor )!小伙伴们可以先思考一下为什么会是这样。

其实答案不难理解,由于 SpringFramework 是基于 AOP 联盟制定的规范来的,所以自然会去兼容原有的方案。又由于咱之前写过原生的动态代理,知道它其实就是环绕通知,所以 SpringFramework 要在环绕通知上拆解结构,自然也会保留原本环绕通知的接口支持。

了解这部分的知识,在后面咱分析 Spring AOP 的原理时,看到一些特殊的 API 接口时,就不会觉得奇怪或者陌生了,现在小伙伴们只是有个基本的印象即可。

2. 切面类的通知方法参数【掌握】

在上一章的环绕通知编写中,咱提到了一个特殊的接口 ProceedingJoinPoint ,它的具体使用,以及切面类中的通知方法参数等等,咱都有必要来学习一下。

其实在之前的代码中,或许有的小伙伴就已经产生很强的不适感了:这所有的日志打印都是一样的,我也不知道哪个日志打印是哪个方法触发的,这咋区分呢? 所以,我们得想个办法,把被增强的方法,以及对应的目标对象的信息拿到才行。(原生动态代理都行,到 AOP 就不行了?这肯定不合理)

将 b_aspectj 的代码完整的复制一份到 c_joinpoint 包下。

本节源码位置:com.linkedbear.spring.aop.c_joinpoint 。

2.1 JoinPoint的使用

其实切面类的通知方法,咱都可以在方法的参数列表上加上切入点的引用,就像这样:(咱以 beforePrint 方法为例)

@Before("execution(public * com..FinanceService.*(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println("Logger beforePrint run ......");
}

这样写之后,重新运行程序不会有任何错误,说明这样写是被允许的,但咱更关心的是,能从这个 JoinPoint 中得到什么呢?

直接在方法中调用 joinPoint 的方法,可以发现它能获取到这么多东西:

Spring AOP进阶-AOP的延伸知识和进阶使用

这么多内容,咱选几个比较重要的来讲解。

2.1.1 getTarget & getThis

getTarget 方法是最容易被理解的,咱可以简单的测试一下效果:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(joinPoint.getTarget());
    System.out.println("Logger beforePrint run ......");
}

运行 main 方法,控制台会打印 FinanceService 的信息:

com.linkedbear.spring.aop.c_joinpoint.service.FinanceService@4c309d4d
Logger beforePrint run ......
FinanceService 收钱 === 123.45

包括使用 Debug 打断点,识别到的 FinanceService 也是未经过代理的原始对象:

Spring AOP进阶-AOP的延伸知识和进阶使用

那相对的,getThis 方法返回的就是代理对象咯?咱也可以来打印一下:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(joinPoint.getTarget());
    System.out.println(joinPoint.getThis());
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台打印了两个一模一样的 FinanceService :

com.linkedbear.spring.aop.c_joinpoint.service.FinanceService@4c309d4d
com.linkedbear.spring.aop.c_joinpoint.service.FinanceService@4c309d4d
Logger beforePrint run ......
FinanceService 收钱 === 123.45

??????怎么个情况?难道是我们推理错了吗?用 Debug 看一眼:

Spring AOP进阶-AOP的延伸知识和进阶使用

诶呦吓一跳,getThis 肯定还是获取到代理对象才是啦。那为什么原始的目标对象,与代理对象的控制台打印结果是一样的呢?

其实从上面的截图中也能猜到端倪:它增强了 equals 方法,增强了 hashcode 方法,就是没有增强 toString 方法,那当然就执行目标对象的方法啦,自然也就打印原来的目标对象的全限定名了。

2.1.2 getArgs

这个方法也是超级好理解,它可以获取到被拦截的方法的参数列表。快速的来测试一下吧:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(Arrays.toString(joinPoint.getArgs()));
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台打印出了 addMoney 方法传入的 123.45 :

[123.45]
Logger beforePrint run ......
FinanceService 收钱 === 123.45

2.1.3 getSignature

这个方法,从名上看是获取签名,关键是这个签名是个啥?不知道,猜不出来,干脆先打印一把吧:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(joinPoint.getSignature());
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台打印的是被拦截的方法的全限定名等信息:

void com.linkedbear.spring.aop.c_joinpoint.service.FinanceService.addMoney(double)
Logger beforePrint run ......
FinanceService 收钱 === 123.45

哦,突然明白了,合着它打印的是这个被拦截的方法的签名啊!那是不是还可以顺便拿到方法的信息呢?

直接调用 getSignature() 的方法,发现这里面只有类的信息,没有方法的信息:

Spring AOP进阶-AOP的延伸知识和进阶使用

诶?那可奇了怪了,既然基于 AspectJ 的 AOP 是对方法的拦截,那理所应当的应该能拿到方法的信息才对呀!那当然,肯定能拿到,只是缺少了一点点步骤而已。

既然是基于方法的拦截,那获取到的 Signature 就应该可以强转为一种类似于 Method 的 Signature ,刚好还真就有这么一个 MethodSignature 的接口!

所以,咱就可以这样写了:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    
    System.out.println("Logger beforePrint run ......");
}

那既然是这样,MethodSignature 中一定能拿到方法的信息了!果不其然,这个接口还真就定义了获取 Method 的方法:

public interface MethodSignature extends CodeSignature {
    Class getReturnType();
	Method getMethod();
}

so ,我们就可以打印出这个方法的信息了:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    Method method = signature.getMethod();
    System.out.println(method.getName());
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台可以打印出方法的信息:

addMoney
Logger beforePrint run ......
FinanceService 收钱 === 123.45

其实 Signature 的 getName 方法,就相当于拿到 Method 后再调 getName 方法了,小伙伴们可以自行测试一下。

至此,其实我们就可以完成前面说的需求了。

2.1.4 需求的改造

重新修改 addMoney 方法的逻辑,就可以很简单轻松的完成一开始说的 “不知道哪个日志打印是哪个方法触发” 的需求了:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println("Logger beforePrint run ......");
    System.out.println("被拦截的类:" + joinPoint.getTarget().getClass().getName());
    System.out.println("被拦截的方法:" + ((MethodSignature) joinPoint.getSignature()).getMethod().getName());
    System.out.println("被拦截的方法参数:" + Arrays.toString(joinPoint.getArgs()));
}

重新运行 main 方法,控制台打印出了我们预期的需求效果:

Logger beforePrint run ......
被拦截的类:com.linkedbear.spring.aop.c_joinpoint.service.FinanceService
被拦截的方法:addMoney
被拦截的方法参数:[123.45]
FinanceService 收钱 === 123.45

2.2 ProceedingJoinPoint的扩展

上一章中我们提前使用了 ProceedingJoinPoint 这个家伙,而它是基于 JoinPoint 的扩展,它扩展的方法只有 proceed 方法,也就是那个能让我们在环绕通知中显式执行目标对象的目标方法的那个 API 。

不过有一点要注意:proceed 方法还有一个带参数的重载方法:

public Object proceed(Object[] args) throws Throwable;

由此可以说明一点:在环绕通知中,可以自行替换掉原始目标方法执行时传入的参数列表

其实这个一点也不奇怪,想想在之前的动态代理案例中,咱不就是可以随便改参数的嘛。

2.3 返回通知和异常通知的特殊参数

之前我们在写返回通知和异常通知时,还有一个小问题没有解决:返回通知中我们要拿到方法的返回值,异常通知中我们要拿到具体的异常抛出。这个呢,其实非常容易解决。

咱先把之前的代码再拿出来:(顺便简化了一下)

@AfterReturning("execution(* com..FinanceService.subtractMoney(double))")
public void afterReturningPrint() {
    System.out.println("Logger afterReturningPrint run ......");
}

@AfterThrowing("defaultPointcut()")
public void afterThrowingPrint() {
    System.out.println("Logger afterThrowingPrint run ......");
}

想拿到返回值或者异常非常简单,两个步骤。

首先在方法的参数列表中声明一个 result 或者 e :

@AfterReturning("execution(* com..FinanceService.subtractMoney(double))")
public void afterReturningPrint(Object retval) {
    System.out.println("Logger afterReturningPrint run ......");
    System.out.println("返回的数据:" + retval);
}

@AfterThrowing("defaultPointcut()")
public void afterThrowingPrint(Exception e) {
    System.out.println("Logger afterThrowingPrint run ......");
}

注意只是这样写了之后,此时运行 main 方法是不好使的,是拿不到返回值的!我们还需要告诉 SpringFramework ,我拿了一个名叫 retval 的参数来接这个方法返回的异常,拿一个名叫 e 的参数来接收方法抛出的异常,反映到代码上就应该是这样:

@AfterReturning(value = "execution(* com..FinanceService.subtractMoney(double))", returning = "retval")
public void afterReturningPrint(Object retval) {
    System.out.println("Logger afterReturningPrint run ......");
    System.out.println("返回的数据:" + retval);
}

@AfterThrowing(value = "defaultPointcut()", throwing = "e")
public void afterThrowingPrint(Exception e) {
    System.out.println("Logger afterThrowingPrint run ......");
    System.out.println("抛出的异常:" + e.getMessage());
}

这样再运行 main 方法,控制台才会打印出方法的返回值:

FinanceService 付钱 === 543.21
Logger afterReturningPrint run ......
返回的数据:543.21

异常的信息接收同理,小伙伴们可以自行测试。

3. 多个切面的执行顺序【熟悉】

日常开发中,或许我们会碰到一些特殊的情况:一个方法被多个切面同时增强了,这个时候如何控制好各个切面的执行顺序,以保证最终的运行结果能符合最初设计,这个也是非常重要的,咱有必要来研究一下多个切面的执行顺序问题。

本节源码位置:com.linkedbear.spring.aop.d_order 。

3.1 代码准备

咱先把测试的代码准备一下,很简单,咱只声明两个切面和一个 Service 即可:

@Service
public class UserService {
    
    public void saveUser(String id) {
        System.out.println("UserService 保存用户" + id);
    }
}
@Component
@Aspect
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void printLog() {
        System.out.println("LogAspect 打印日志 ......");
    }
}
@Component
@Aspect
public class TransactionAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void beginTransaction() {
        System.out.println("TransactionAspect 开启事务 ......");
    }
}

然后,编写配置类,开启注解 AOP :

@Configuration
@ComponentScan("com.linkedbear.spring.aop.d_order")
@EnableAspectJAutoProxy
public class AspectOrderConfiguration {
    
}

最后,编写启动类,用上面的配置类驱动 IOC 容器:

public class AspectOrderApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AspectOrderConfiguration.class);
        UserService userService = ctx.getBean(UserService.class);
        userService.saveUser("abc");
    }
}

运行 main 方法,控制台可以打印出两个切面的前置通知:

LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
UserService 保存用户abc

3.2 预设的顺序?

观察这个打印的结果,它是打印日志在前,开启事务在后,这难不成是因为我先写的 LogAspect ,后写的 TransactionAspect ,它就按照我的顺序来了?那不可能啊,即便我后写 TransactionAspect ,也是日志打印在前啊!所以它一定有一个默认的预设规则。

小伙伴可以先猜测一下预设的规则是什么,小册在这里再写一个切面,想必写完这个切面后,基本上顺序就非常容易推理了:

@Component
@Aspect
public class AbcAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void abc() {
        System.out.println("abc abc abc");
    }
}

重新运行 main 方法,发现 AbcAspect 的前置通知打印在 LogAspect 之前!

由此是不是咱就可以推测出预设的顺序了:默认的切面执行顺序,是按照字母表的顺序来的

严谨一点,排序规则其实是根据切面类的 unicode 编码,按照十六进制排序得来的,unicode 编码靠前的,那自然就会排在前面。(作者个人习惯称其为字典表顺序)

3.3 显式声明执行顺序

那我们不能因为这个毛病,就搞得调整顺序就必须改类名吧,一定有更好的方案才对。

还记得之前在第 26 章,小册讲解 BeanPostProcessor 的 javadoc 中提到的 Ordered 接口吗?而且在 IOC 原理的 BeanPostProcessor 的初始化部分,也提到过有关排序的接口,也涉及到了这个 Ordered 接口。不过前面我们一直都没有实际演示 Ordered 接口的使用,这里咱就来搞一下。

现在咱希望让事务控制的切面提早执行,让它在所有切面之前,那么我们就可以这样写:给 TransactionAspect 实现 Ordered 接口,并声明 getOrder 的返回值:

@Component
@Aspect
public class TransactionAspect implements Ordered {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void beginTransaction() {
        System.out.println("TransactionAspect 开启事务 ......");
    }
    
    @Override
    public int getOrder() {
        return 0;
    }
}

这个值设置成多少呢?咱先放个 0 试试,运行 main 方法,观察控制台的打印:

TransactionAspect 开启事务 ......
abc abc abc
LogAspect 打印日志 ......
UserService 保存用户abc

咦,发现事务切面的前置通知已经提前执行了,说明 0 这个顺序已经是提早了的,那最晚的时机对应的 order 值是什么呢?

3.3.1 默认的排序值?

很简单,Integer 的最大值 2147483647 嘛!其实在 Ordered 接口中,就有这两个常量的定义:

public interface Ordered {
	int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
	int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

那我们把这个值调到最低试一下?

@Override
public int getOrder() {
    return Ordered.LOWEST_PRECEDENCE;
}

重新运行 main 方法,发现事务的切面又回去了:

abc abc abc
LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
UserService 保存用户abc

那到底默认值是啥呢?咱把 order 值往上调一个点试试?

@Override
public int getOrder() {
    return Ordered.LOWEST_PRECEDENCE - 1;
}

再次重新运行 main 方法,事务的切面打印又上去了。。。

所以得出结论:在不显式声明 order 排序值时,默认的排序值是 Integer.MAX_VALUE 。

3.3.2 另一种声明办法

除了使用 Ordered 接口,还有通过注解的方式声明:@Order 。

这次我们在 LogAspect 上标注 @Order 注解,并声明一下排序值:

@Component
@Aspect
@Order(0)
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void printLog() {
        System.out.println("LogAspect 打印日志 ......");
    }
}

重新运行 main 方法,发现日志切面的打印提到最早了:

LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
abc abc abc
UserService 保存用户abc

说明 @Order 注解也可以实现同样的效果。

4. 同切面的多个通知执行顺序【熟悉】

除了多个切面的顺序问题,如果同一个切面定义了多个相同类型的通知,它的执行顺序又是怎么样呢?咱也来研究一下。

这次编码的内容就少多了,直接在 AbsAspect 中添加一个方法 def 即可:

@Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
public void def() {
    System.out.println("def def def");
}

直接重新运行 main 方法,控制台先后打印了 abc 和 def 的内容:

LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
abc abc abc
def def def
UserService 保存用户abc

原因是什么呢?估计小伙伴们也能猜到了,跟上面的逻辑一样,都是根据unicode编码顺序(字典表顺序)来的。

至于怎么搞,小伙伴们立马想到办法了吧!直接在方法上标注 @Order 注解就 OK (方法没办法实现接口的嘛)!

@Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
@Order(Ordered.HIGHEST_PRECEDENCE)
public void def() {
    System.out.println("def def def");
}

运行 main 方法,发现 def 的通知内容并没有被提前执行。。。看来这个办法行不通。。。

那怎么办呢?哎,这还真没办法。。。只能靠方法名去区分了。。。(是不是很无奈)

好了,有关切面、通知的执行顺序的研究,咱就到这里了。

5. 代理对象调用自身的方法【熟悉】

有一些特殊的场景下,我们产生的这些代理对象,会出现自身调用自身的另外方法的。下面我们也来演示一下这个现象。

本节源码位置:com.linkedbear.spring.aop.e_aopcontext 。

5.1 代码准备

测试代码还是三个类,不再重复了:

@Service
public class UserService {
    
    public void update(String id, String name) {
        this.get(id);
        System.out.println("修改指定id的name。。。");
    }
    
    public void get(String id) {
        System.out.println("获取指定id的user。。。");
    }
}
@Component
@Aspect
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.e_aopcontext.service.UserService.*(..))")
    public void beforePrint() {
        System.out.println("LogAspect 前置通知 ......");
    }
}
@Configuration
@ComponentScan("com.linkedbear.spring.aop.e_aopcontext")
@EnableAspectJAutoProxy
public class AopContextConfiguration {
    
}

然后,依然是编写测试启动类:

public class AopContextApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AopContextConfiguration.class);
        UserService userService = ctx.getBean(UserService.class);
        userService.update("abc", "def");
    }
}

这样写完之后,运行 main 方法,发现控制台只打印了一次 LogAspect 的切面打印:

LogAspect 前置通知 ......
获取指定id的user。。。
修改指定id的name。。。

如果需求是每次调用 UserService 的方法都需要打印切面日志,应该怎么处理呢?

5.2 不优雅的解决方案

可能有的小伙伴想到了一个能行但不太优雅的解决方案,就是利用依赖注入的特性,把自己注入进来,之后不用 this.get ,换用 userService.get 方法:

@Service
public class UserService {
    
    @Autowired
    UserService userService;
    
    public void update(String id, String name) {
        // this.get(id);
        userService.get(id);
        System.out.println("修改指定id的name。。。");
    }

重新运行 main 方法,控制台确实打印了两次切面日志:

LogAspect 前置通知 ......
LogAspect 前置通知 ......
获取指定id的user。。。
修改指定id的name。。。

但是吧。。。这样写真的好吗。。。有木有感觉怪怪的。。。难不成SpringFramework 就没有考虑到这个问题吗?

5.3 正确的解决方案:AopContext

当然还是得有的,SpringFramework 从一开始就考虑到这个问题了,于是它提供了一个 AopContext 的类,使用这个类,可以在代理对象中取到自身,它的使用方法很简单:

public void update(String id, String name) {
    ((UserService) AopContext.currentProxy()).get(id);
    System.out.println("修改指定id的name。。。");
}

使用 AopContext.currentProxy() 方法就可以取到代理对象的 this 了。

不过这样直接写完之后,运行是不好使的,它会抛出一个异常:

Exception in thread "main" java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.

这个异常的大致含义是,没有开启一个 exposeProxy 的属性,导致无法暴露出代理对象,从而无法获取。那开启 exposeProxy 这个属性的位置在哪里呢?好巧不巧,它是在我们一开始学习注解 AOP 的那个 @EnableAspectJAutoProxy 上:

@Configuration
@ComponentScan("com.linkedbear.spring.aop.e_aopcontext")
@EnableAspectJAutoProxy(exposeProxy = true) // 暴露代理对象
public class AopContextConfiguration {
    
}

它的默认值是 false ,改为 true 之后,再运行 main 方法,就可以达到同样的效果了,控制台会打印两次切面日志。

通过前面两章的学习,其实 Spring AOP 的核心知识就全部讲解完了,不过咱小册的学习可不会止步于此,咱继续深入学习一些 Spring AOP 的延伸知识,以及扩展的进阶使用。

1. AOP联盟【了解】

在 SpringFramework 2.0 之前,它还没有整合 AspectJ ,当时的 SpringFramework 还有一套相对低层级的实现,它也是 SpringFramework 原生的实现,而我们要了解它,首先要先了解一个组织:AOP 联盟

早在很久之前,AOP 的概念就被提出来了。同之前的 EJB 一样,作为一个概念、思想,它要有一批人来制定规范,于是就有了这样一个 AOP 联盟。这个联盟的人将 AOP 的这些概念都整理好,形成了一个规范 AOP 框架底层实现的 API ,并最终总结出了 5 种 AOP 通知类型。

咱要了解的,就是 AOP 联盟提出的这 5 种通知类型。

1.1 AOP联盟制定的通知类型

5 种通知类型分别为:

  • 前置通知
  • 后置通知(返回通知
  • 异常通知
  • 环绕通知
  • 引介通知

注意它跟 AspectJ 规定的 5 种通知类型的区别:它多了一个引介通知,少了一个后置通知。而且还有一个要注意的,AOP 联盟定义的后置通知实际上是返回通知( after-returning ),而 AspectJ 的后置通知是真的后置通知,与返回通知是两码事。

1.2 SpringFramework中对应的通知接口

AOP 联盟定义的 5 种通知类型在 SpringFramework 中都有对应的接口定义:

  • 前置通知:org.springframework.aop.MethodBeforeAdvice
  • 返回通知:org.springframework.aop.AfterReturningAdvice
  • 异常通知:org.springframework.aop.ThrowsAdvice
  • 环绕通知:org.aopalliance.intercept.MethodInterceptor
  • 引介通知:org.springframework.aop.IntroductionAdvisor

注意!环绕通知的接口是 AOP 联盟原生定义的接口(不是 cglib 的那个 MethodInterceptor )!小伙伴们可以先思考一下为什么会是这样。

其实答案不难理解,由于 SpringFramework 是基于 AOP 联盟制定的规范来的,所以自然会去兼容原有的方案。又由于咱之前写过原生的动态代理,知道它其实就是环绕通知,所以 SpringFramework 要在环绕通知上拆解结构,自然也会保留原本环绕通知的接口支持。

了解这部分的知识,在后面咱分析 Spring AOP 的原理时,看到一些特殊的 API 接口时,就不会觉得奇怪或者陌生了,现在小伙伴们只是有个基本的印象即可。

2. 切面类的通知方法参数【掌握】

在上一章的环绕通知编写中,咱提到了一个特殊的接口 ProceedingJoinPoint ,它的具体使用,以及切面类中的通知方法参数等等,咱都有必要来学习一下。

其实在之前的代码中,或许有的小伙伴就已经产生很强的不适感了:这所有的日志打印都是一样的,我也不知道哪个日志打印是哪个方法触发的,这咋区分呢? 所以,我们得想个办法,把被增强的方法,以及对应的目标对象的信息拿到才行。(原生动态代理都行,到 AOP 就不行了?这肯定不合理)

将 b_aspectj 的代码完整的复制一份到 c_joinpoint 包下。

本节源码位置:com.linkedbear.spring.aop.c_joinpoint 。

2.1 JoinPoint的使用

其实切面类的通知方法,咱都可以在方法的参数列表上加上切入点的引用,就像这样:(咱以 beforePrint 方法为例)

@Before("execution(public * com..FinanceService.*(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println("Logger beforePrint run ......");
}

这样写之后,重新运行程序不会有任何错误,说明这样写是被允许的,但咱更关心的是,能从这个 JoinPoint 中得到什么呢?

直接在方法中调用 joinPoint 的方法,可以发现它能获取到这么多东西:

Spring AOP进阶-AOP的延伸知识和进阶使用

这么多内容,咱选几个比较重要的来讲解。

2.1.1 getTarget & getThis

getTarget 方法是最容易被理解的,咱可以简单的测试一下效果:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(joinPoint.getTarget());
    System.out.println("Logger beforePrint run ......");
}

运行 main 方法,控制台会打印 FinanceService 的信息:

com.linkedbear.spring.aop.c_joinpoint.service.FinanceService@4c309d4d
Logger beforePrint run ......
FinanceService 收钱 === 123.45

包括使用 Debug 打断点,识别到的 FinanceService 也是未经过代理的原始对象:

Spring AOP进阶-AOP的延伸知识和进阶使用

那相对的,getThis 方法返回的就是代理对象咯?咱也可以来打印一下:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(joinPoint.getTarget());
    System.out.println(joinPoint.getThis());
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台打印了两个一模一样的 FinanceService :

com.linkedbear.spring.aop.c_joinpoint.service.FinanceService@4c309d4d
com.linkedbear.spring.aop.c_joinpoint.service.FinanceService@4c309d4d
Logger beforePrint run ......
FinanceService 收钱 === 123.45

??????怎么个情况?难道是我们推理错了吗?用 Debug 看一眼:

Spring AOP进阶-AOP的延伸知识和进阶使用

诶呦吓一跳,getThis 肯定还是获取到代理对象才是啦。那为什么原始的目标对象,与代理对象的控制台打印结果是一样的呢?

其实从上面的截图中也能猜到端倪:它增强了 equals 方法,增强了 hashcode 方法,就是没有增强 toString 方法,那当然就执行目标对象的方法啦,自然也就打印原来的目标对象的全限定名了。

2.1.2 getArgs

这个方法也是超级好理解,它可以获取到被拦截的方法的参数列表。快速的来测试一下吧:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(Arrays.toString(joinPoint.getArgs()));
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台打印出了 addMoney 方法传入的 123.45 :

[123.45]
Logger beforePrint run ......
FinanceService 收钱 === 123.45

2.1.3 getSignature

这个方法,从名上看是获取签名,关键是这个签名是个啥?不知道,猜不出来,干脆先打印一把吧:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(joinPoint.getSignature());
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台打印的是被拦截的方法的全限定名等信息:

void com.linkedbear.spring.aop.c_joinpoint.service.FinanceService.addMoney(double)
Logger beforePrint run ......
FinanceService 收钱 === 123.45

哦,突然明白了,合着它打印的是这个被拦截的方法的签名啊!那是不是还可以顺便拿到方法的信息呢?

直接调用 getSignature() 的方法,发现这里面只有类的信息,没有方法的信息:

Spring AOP进阶-AOP的延伸知识和进阶使用

诶?那可奇了怪了,既然基于 AspectJ 的 AOP 是对方法的拦截,那理所应当的应该能拿到方法的信息才对呀!那当然,肯定能拿到,只是缺少了一点点步骤而已。

既然是基于方法的拦截,那获取到的 Signature 就应该可以强转为一种类似于 Method 的 Signature ,刚好还真就有这么一个 MethodSignature 的接口!

所以,咱就可以这样写了:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    
    System.out.println("Logger beforePrint run ......");
}

那既然是这样,MethodSignature 中一定能拿到方法的信息了!果不其然,这个接口还真就定义了获取 Method 的方法:

public interface MethodSignature extends CodeSignature {
    Class getReturnType();
	Method getMethod();
}

so ,我们就可以打印出这个方法的信息了:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    Method method = signature.getMethod();
    System.out.println(method.getName());
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台可以打印出方法的信息:

addMoney
Logger beforePrint run ......
FinanceService 收钱 === 123.45

其实 Signature 的 getName 方法,就相当于拿到 Method 后再调 getName 方法了,小伙伴们可以自行测试一下。

至此,其实我们就可以完成前面说的需求了。

2.1.4 需求的改造

重新修改 addMoney 方法的逻辑,就可以很简单轻松的完成一开始说的 “不知道哪个日志打印是哪个方法触发” 的需求了:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println("Logger beforePrint run ......");
    System.out.println("被拦截的类:" + joinPoint.getTarget().getClass().getName());
    System.out.println("被拦截的方法:" + ((MethodSignature) joinPoint.getSignature()).getMethod().getName());
    System.out.println("被拦截的方法参数:" + Arrays.toString(joinPoint.getArgs()));
}

重新运行 main 方法,控制台打印出了我们预期的需求效果:

Logger beforePrint run ......
被拦截的类:com.linkedbear.spring.aop.c_joinpoint.service.FinanceService
被拦截的方法:addMoney
被拦截的方法参数:[123.45]
FinanceService 收钱 === 123.45

2.2 ProceedingJoinPoint的扩展

上一章中我们提前使用了 ProceedingJoinPoint 这个家伙,而它是基于 JoinPoint 的扩展,它扩展的方法只有 proceed 方法,也就是那个能让我们在环绕通知中显式执行目标对象的目标方法的那个 API 。

不过有一点要注意:proceed 方法还有一个带参数的重载方法:

public Object proceed(Object[] args) throws Throwable;

由此可以说明一点:在环绕通知中,可以自行替换掉原始目标方法执行时传入的参数列表

其实这个一点也不奇怪,想想在之前的动态代理案例中,咱不就是可以随便改参数的嘛。

2.3 返回通知和异常通知的特殊参数

之前我们在写返回通知和异常通知时,还有一个小问题没有解决:返回通知中我们要拿到方法的返回值,异常通知中我们要拿到具体的异常抛出。这个呢,其实非常容易解决。

咱先把之前的代码再拿出来:(顺便简化了一下)

@AfterReturning("execution(* com..FinanceService.subtractMoney(double))")
public void afterReturningPrint() {
    System.out.println("Logger afterReturningPrint run ......");
}

@AfterThrowing("defaultPointcut()")
public void afterThrowingPrint() {
    System.out.println("Logger afterThrowingPrint run ......");
}

想拿到返回值或者异常非常简单,两个步骤。

首先在方法的参数列表中声明一个 result 或者 e :

@AfterReturning("execution(* com..FinanceService.subtractMoney(double))")
public void afterReturningPrint(Object retval) {
    System.out.println("Logger afterReturningPrint run ......");
    System.out.println("返回的数据:" + retval);
}

@AfterThrowing("defaultPointcut()")
public void afterThrowingPrint(Exception e) {
    System.out.println("Logger afterThrowingPrint run ......");
}

注意只是这样写了之后,此时运行 main 方法是不好使的,是拿不到返回值的!我们还需要告诉 SpringFramework ,我拿了一个名叫 retval 的参数来接这个方法返回的异常,拿一个名叫 e 的参数来接收方法抛出的异常,反映到代码上就应该是这样:

@AfterReturning(value = "execution(* com..FinanceService.subtractMoney(double))", returning = "retval")
public void afterReturningPrint(Object retval) {
    System.out.println("Logger afterReturningPrint run ......");
    System.out.println("返回的数据:" + retval);
}

@AfterThrowing(value = "defaultPointcut()", throwing = "e")
public void afterThrowingPrint(Exception e) {
    System.out.println("Logger afterThrowingPrint run ......");
    System.out.println("抛出的异常:" + e.getMessage());
}

这样再运行 main 方法,控制台才会打印出方法的返回值:

FinanceService 付钱 === 543.21
Logger afterReturningPrint run ......
返回的数据:543.21

异常的信息接收同理,小伙伴们可以自行测试。

3. 多个切面的执行顺序【熟悉】

日常开发中,或许我们会碰到一些特殊的情况:一个方法被多个切面同时增强了,这个时候如何控制好各个切面的执行顺序,以保证最终的运行结果能符合最初设计,这个也是非常重要的,咱有必要来研究一下多个切面的执行顺序问题。

本节源码位置:com.linkedbear.spring.aop.d_order 。

3.1 代码准备

咱先把测试的代码准备一下,很简单,咱只声明两个切面和一个 Service 即可:

@Service
public class UserService {
    
    public void saveUser(String id) {
        System.out.println("UserService 保存用户" + id);
    }
}
@Component
@Aspect
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void printLog() {
        System.out.println("LogAspect 打印日志 ......");
    }
}
@Component
@Aspect
public class TransactionAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void beginTransaction() {
        System.out.println("TransactionAspect 开启事务 ......");
    }
}

然后,编写配置类,开启注解 AOP :

@Configuration
@ComponentScan("com.linkedbear.spring.aop.d_order")
@EnableAspectJAutoProxy
public class AspectOrderConfiguration {
    
}

最后,编写启动类,用上面的配置类驱动 IOC 容器:

public class AspectOrderApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AspectOrderConfiguration.class);
        UserService userService = ctx.getBean(UserService.class);
        userService.saveUser("abc");
    }
}

运行 main 方法,控制台可以打印出两个切面的前置通知:

LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
UserService 保存用户abc

3.2 预设的顺序?

观察这个打印的结果,它是打印日志在前,开启事务在后,这难不成是因为我先写的 LogAspect ,后写的 TransactionAspect ,它就按照我的顺序来了?那不可能啊,即便我后写 TransactionAspect ,也是日志打印在前啊!所以它一定有一个默认的预设规则。

小伙伴可以先猜测一下预设的规则是什么,小册在这里再写一个切面,想必写完这个切面后,基本上顺序就非常容易推理了:

@Component
@Aspect
public class AbcAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void abc() {
        System.out.println("abc abc abc");
    }
}

重新运行 main 方法,发现 AbcAspect 的前置通知打印在 LogAspect 之前!

由此是不是咱就可以推测出预设的顺序了:默认的切面执行顺序,是按照字母表的顺序来的

严谨一点,排序规则其实是根据切面类的 unicode 编码,按照十六进制排序得来的,unicode 编码靠前的,那自然就会排在前面。(作者个人习惯称其为字典表顺序)

3.3 显式声明执行顺序

那我们不能因为这个毛病,就搞得调整顺序就必须改类名吧,一定有更好的方案才对。

还记得之前在第 26 章,小册讲解 BeanPostProcessor 的 javadoc 中提到的 Ordered 接口吗?而且在 IOC 原理的 BeanPostProcessor 的初始化部分,也提到过有关排序的接口,也涉及到了这个 Ordered 接口。不过前面我们一直都没有实际演示 Ordered 接口的使用,这里咱就来搞一下。

现在咱希望让事务控制的切面提早执行,让它在所有切面之前,那么我们就可以这样写:给 TransactionAspect 实现 Ordered 接口,并声明 getOrder 的返回值:

@Component
@Aspect
public class TransactionAspect implements Ordered {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void beginTransaction() {
        System.out.println("TransactionAspect 开启事务 ......");
    }
    
    @Override
    public int getOrder() {
        return 0;
    }
}

这个值设置成多少呢?咱先放个 0 试试,运行 main 方法,观察控制台的打印:

TransactionAspect 开启事务 ......
abc abc abc
LogAspect 打印日志 ......
UserService 保存用户abc

咦,发现事务切面的前置通知已经提前执行了,说明 0 这个顺序已经是提早了的,那最晚的时机对应的 order 值是什么呢?

3.3.1 默认的排序值?

很简单,Integer 的最大值 2147483647 嘛!其实在 Ordered 接口中,就有这两个常量的定义:

public interface Ordered {
	int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
	int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

那我们把这个值调到最低试一下?

@Override
public int getOrder() {
    return Ordered.LOWEST_PRECEDENCE;
}

重新运行 main 方法,发现事务的切面又回去了:

abc abc abc
LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
UserService 保存用户abc

那到底默认值是啥呢?咱把 order 值往上调一个点试试?

@Override
public int getOrder() {
    return Ordered.LOWEST_PRECEDENCE - 1;
}

再次重新运行 main 方法,事务的切面打印又上去了。。。

所以得出结论:在不显式声明 order 排序值时,默认的排序值是 Integer.MAX_VALUE 。

3.3.2 另一种声明办法

除了使用 Ordered 接口,还有通过注解的方式声明:@Order 。

这次我们在 LogAspect 上标注 @Order 注解,并声明一下排序值:

@Component
@Aspect
@Order(0)
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void printLog() {
        System.out.println("LogAspect 打印日志 ......");
    }
}

重新运行 main 方法,发现日志切面的打印提到最早了:

LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
abc abc abc
UserService 保存用户abc

说明 @Order 注解也可以实现同样的效果。

4. 同切面的多个通知执行顺序【熟悉】

除了多个切面的顺序问题,如果同一个切面定义了多个相同类型的通知,它的执行顺序又是怎么样呢?咱也来研究一下。

这次编码的内容就少多了,直接在 AbsAspect 中添加一个方法 def 即可:

@Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
public void def() {
    System.out.println("def def def");
}

直接重新运行 main 方法,控制台先后打印了 abc 和 def 的内容:

LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
abc abc abc
def def def
UserService 保存用户abc

原因是什么呢?估计小伙伴们也能猜到了,跟上面的逻辑一样,都是根据unicode编码顺序(字典表顺序)来的。

至于怎么搞,小伙伴们立马想到办法了吧!直接在方法上标注 @Order 注解就 OK (方法没办法实现接口的嘛)!

@Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
@Order(Ordered.HIGHEST_PRECEDENCE)
public void def() {
    System.out.println("def def def");
}

运行 main 方法,发现 def 的通知内容并没有被提前执行。。。看来这个办法行不通。。。

那怎么办呢?哎,这还真没办法。。。只能靠方法名去区分了。。。(是不是很无奈)

好了,有关切面、通知的执行顺序的研究,咱就到这里了。

5. 代理对象调用自身的方法【熟悉】

有一些特殊的场景下,我们产生的这些代理对象,会出现自身调用自身的另外方法的。下面我们也来演示一下这个现象。

本节源码位置:com.linkedbear.spring.aop.e_aopcontext 。

5.1 代码准备

测试代码还是三个类,不再重复了:

@Service
public class UserService {
    
    public void update(String id, String name) {
        this.get(id);
        System.out.println("修改指定id的name。。。");
    }
    
    public void get(String id) {
        System.out.println("获取指定id的user。。。");
    }
}
@Component
@Aspect
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.e_aopcontext.service.UserService.*(..))")
    public void beforePrint() {
        System.out.println("LogAspect 前置通知 ......");
    }
}
@Configuration
@ComponentScan("com.linkedbear.spring.aop.e_aopcontext")
@EnableAspectJAutoProxy
public class AopContextConfiguration {
    
}

然后,依然是编写测试启动类:

public class AopContextApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AopContextConfiguration.class);
        UserService userService = ctx.getBean(UserService.class);
        userService.update("abc", "def");
    }
}

这样写完之后,运行 main 方法,发现控制台只打印了一次 LogAspect 的切面打印:

LogAspect 前置通知 ......
获取指定id的user。。。
修改指定id的name。。。

如果需求是每次调用 UserService 的方法都需要打印切面日志,应该怎么处理呢?

5.2 不优雅的解决方案

可能有的小伙伴想到了一个能行但不太优雅的解决方案,就是利用依赖注入的特性,把自己注入进来,之后不用 this.get ,换用 userService.get 方法:

@Service
public class UserService {
    
    @Autowired
    UserService userService;
    
    public void update(String id, String name) {
        // this.get(id);
        userService.get(id);
        System.out.println("修改指定id的name。。。");
    }

重新运行 main 方法,控制台确实打印了两次切面日志:

LogAspect 前置通知 ......
LogAspect 前置通知 ......
获取指定id的user。。。
修改指定id的name。。。

但是吧。。。这样写真的好吗。。。有木有感觉怪怪的。。。难不成SpringFramework 就没有考虑到这个问题吗?

5.3 正确的解决方案:AopContext

当然还是得有的,SpringFramework 从一开始就考虑到这个问题了,于是它提供了一个 AopContext 的类,使用这个类,可以在代理对象中取到自身,它的使用方法很简单:

public void update(String id, String name) {
    ((UserService) AopContext.currentProxy()).get(id);
    System.out.println("修改指定id的name。。。");
}

使用 AopContext.currentProxy() 方法就可以取到代理对象的 this 了。

不过这样直接写完之后,运行是不好使的,它会抛出一个异常:

Exception in thread "main" java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.

这个异常的大致含义是,没有开启一个 exposeProxy 的属性,导致无法暴露出代理对象,从而无法获取。那开启 exposeProxy 这个属性的位置在哪里呢?好巧不巧,它是在我们一开始学习注解 AOP 的那个 @EnableAspectJAutoProxy 上:

@Configuration
@ComponentScan("com.linkedbear.spring.aop.e_aopcontext")
@EnableAspectJAutoProxy(exposeProxy = true) // 暴露代理对象
public class AopContextConfiguration {
    
}

它的默认值是 false ,改为 true 之后,再运行 main 方法,就可以达到同样的效果了,控制台会打印两次切面日志。

通过前面两章的学习,其实 Spring AOP 的核心知识就全部讲解完了,不过咱小册的学习可不会止步于此,咱继续深入学习一些 Spring AOP 的延伸知识,以及扩展的进阶使用。

1. AOP联盟【了解】

在 SpringFramework 2.0 之前,它还没有整合 AspectJ ,当时的 SpringFramework 还有一套相对低层级的实现,它也是 SpringFramework 原生的实现,而我们要了解它,首先要先了解一个组织:AOP 联盟

早在很久之前,AOP 的概念就被提出来了。同之前的 EJB 一样,作为一个概念、思想,它要有一批人来制定规范,于是就有了这样一个 AOP 联盟。这个联盟的人将 AOP 的这些概念都整理好,形成了一个规范 AOP 框架底层实现的 API ,并最终总结出了 5 种 AOP 通知类型。

咱要了解的,就是 AOP 联盟提出的这 5 种通知类型。

1.1 AOP联盟制定的通知类型

5 种通知类型分别为:

  • 前置通知
  • 后置通知(返回通知
  • 异常通知
  • 环绕通知
  • 引介通知

注意它跟 AspectJ 规定的 5 种通知类型的区别:它多了一个引介通知,少了一个后置通知。而且还有一个要注意的,AOP 联盟定义的后置通知实际上是返回通知( after-returning ),而 AspectJ 的后置通知是真的后置通知,与返回通知是两码事。

1.2 SpringFramework中对应的通知接口

AOP 联盟定义的 5 种通知类型在 SpringFramework 中都有对应的接口定义:

  • 前置通知:org.springframework.aop.MethodBeforeAdvice
  • 返回通知:org.springframework.aop.AfterReturningAdvice
  • 异常通知:org.springframework.aop.ThrowsAdvice
  • 环绕通知:org.aopalliance.intercept.MethodInterceptor
  • 引介通知:org.springframework.aop.IntroductionAdvisor

注意!环绕通知的接口是 AOP 联盟原生定义的接口(不是 cglib 的那个 MethodInterceptor )!小伙伴们可以先思考一下为什么会是这样。

其实答案不难理解,由于 SpringFramework 是基于 AOP 联盟制定的规范来的,所以自然会去兼容原有的方案。又由于咱之前写过原生的动态代理,知道它其实就是环绕通知,所以 SpringFramework 要在环绕通知上拆解结构,自然也会保留原本环绕通知的接口支持。

了解这部分的知识,在后面咱分析 Spring AOP 的原理时,看到一些特殊的 API 接口时,就不会觉得奇怪或者陌生了,现在小伙伴们只是有个基本的印象即可。

2. 切面类的通知方法参数【掌握】

在上一章的环绕通知编写中,咱提到了一个特殊的接口 ProceedingJoinPoint ,它的具体使用,以及切面类中的通知方法参数等等,咱都有必要来学习一下。

其实在之前的代码中,或许有的小伙伴就已经产生很强的不适感了:这所有的日志打印都是一样的,我也不知道哪个日志打印是哪个方法触发的,这咋区分呢? 所以,我们得想个办法,把被增强的方法,以及对应的目标对象的信息拿到才行。(原生动态代理都行,到 AOP 就不行了?这肯定不合理)

将 b_aspectj 的代码完整的复制一份到 c_joinpoint 包下。

本节源码位置:com.linkedbear.spring.aop.c_joinpoint 。

2.1 JoinPoint的使用

其实切面类的通知方法,咱都可以在方法的参数列表上加上切入点的引用,就像这样:(咱以 beforePrint 方法为例)

@Before("execution(public * com..FinanceService.*(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println("Logger beforePrint run ......");
}

这样写之后,重新运行程序不会有任何错误,说明这样写是被允许的,但咱更关心的是,能从这个 JoinPoint 中得到什么呢?

直接在方法中调用 joinPoint 的方法,可以发现它能获取到这么多东西:

Spring AOP进阶-AOP的延伸知识和进阶使用

这么多内容,咱选几个比较重要的来讲解。

2.1.1 getTarget & getThis

getTarget 方法是最容易被理解的,咱可以简单的测试一下效果:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(joinPoint.getTarget());
    System.out.println("Logger beforePrint run ......");
}

运行 main 方法,控制台会打印 FinanceService 的信息:

com.linkedbear.spring.aop.c_joinpoint.service.FinanceService@4c309d4d
Logger beforePrint run ......
FinanceService 收钱 === 123.45

包括使用 Debug 打断点,识别到的 FinanceService 也是未经过代理的原始对象:

Spring AOP进阶-AOP的延伸知识和进阶使用

那相对的,getThis 方法返回的就是代理对象咯?咱也可以来打印一下:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(joinPoint.getTarget());
    System.out.println(joinPoint.getThis());
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台打印了两个一模一样的 FinanceService :

com.linkedbear.spring.aop.c_joinpoint.service.FinanceService@4c309d4d
com.linkedbear.spring.aop.c_joinpoint.service.FinanceService@4c309d4d
Logger beforePrint run ......
FinanceService 收钱 === 123.45

??????怎么个情况?难道是我们推理错了吗?用 Debug 看一眼:

Spring AOP进阶-AOP的延伸知识和进阶使用

诶呦吓一跳,getThis 肯定还是获取到代理对象才是啦。那为什么原始的目标对象,与代理对象的控制台打印结果是一样的呢?

其实从上面的截图中也能猜到端倪:它增强了 equals 方法,增强了 hashcode 方法,就是没有增强 toString 方法,那当然就执行目标对象的方法啦,自然也就打印原来的目标对象的全限定名了。

2.1.2 getArgs

这个方法也是超级好理解,它可以获取到被拦截的方法的参数列表。快速的来测试一下吧:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(Arrays.toString(joinPoint.getArgs()));
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台打印出了 addMoney 方法传入的 123.45 :

[123.45]
Logger beforePrint run ......
FinanceService 收钱 === 123.45

2.1.3 getSignature

这个方法,从名上看是获取签名,关键是这个签名是个啥?不知道,猜不出来,干脆先打印一把吧:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println(joinPoint.getSignature());
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台打印的是被拦截的方法的全限定名等信息:

void com.linkedbear.spring.aop.c_joinpoint.service.FinanceService.addMoney(double)
Logger beforePrint run ......
FinanceService 收钱 === 123.45

哦,突然明白了,合着它打印的是这个被拦截的方法的签名啊!那是不是还可以顺便拿到方法的信息呢?

直接调用 getSignature() 的方法,发现这里面只有类的信息,没有方法的信息:

Spring AOP进阶-AOP的延伸知识和进阶使用

诶?那可奇了怪了,既然基于 AspectJ 的 AOP 是对方法的拦截,那理所应当的应该能拿到方法的信息才对呀!那当然,肯定能拿到,只是缺少了一点点步骤而已。

既然是基于方法的拦截,那获取到的 Signature 就应该可以强转为一种类似于 Method 的 Signature ,刚好还真就有这么一个 MethodSignature 的接口!

所以,咱就可以这样写了:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    
    System.out.println("Logger beforePrint run ......");
}

那既然是这样,MethodSignature 中一定能拿到方法的信息了!果不其然,这个接口还真就定义了获取 Method 的方法:

public interface MethodSignature extends CodeSignature {
    Class getReturnType();
	Method getMethod();
}

so ,我们就可以打印出这个方法的信息了:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    Method method = signature.getMethod();
    System.out.println(method.getName());
    System.out.println("Logger beforePrint run ......");
}

重新运行 main 方法,控制台可以打印出方法的信息:

addMoney
Logger beforePrint run ......
FinanceService 收钱 === 123.45

其实 Signature 的 getName 方法,就相当于拿到 Method 后再调 getName 方法了,小伙伴们可以自行测试一下。

至此,其实我们就可以完成前面说的需求了。

2.1.4 需求的改造

重新修改 addMoney 方法的逻辑,就可以很简单轻松的完成一开始说的 “不知道哪个日志打印是哪个方法触发” 的需求了:

@Before("execution(public * com..FinanceService.addMoney(..))")
public void beforePrint(JoinPoint joinPoint) {
    System.out.println("Logger beforePrint run ......");
    System.out.println("被拦截的类:" + joinPoint.getTarget().getClass().getName());
    System.out.println("被拦截的方法:" + ((MethodSignature) joinPoint.getSignature()).getMethod().getName());
    System.out.println("被拦截的方法参数:" + Arrays.toString(joinPoint.getArgs()));
}

重新运行 main 方法,控制台打印出了我们预期的需求效果:

Logger beforePrint run ......
被拦截的类:com.linkedbear.spring.aop.c_joinpoint.service.FinanceService
被拦截的方法:addMoney
被拦截的方法参数:[123.45]
FinanceService 收钱 === 123.45

2.2 ProceedingJoinPoint的扩展

上一章中我们提前使用了 ProceedingJoinPoint 这个家伙,而它是基于 JoinPoint 的扩展,它扩展的方法只有 proceed 方法,也就是那个能让我们在环绕通知中显式执行目标对象的目标方法的那个 API 。

不过有一点要注意:proceed 方法还有一个带参数的重载方法:

public Object proceed(Object[] args) throws Throwable;

由此可以说明一点:在环绕通知中,可以自行替换掉原始目标方法执行时传入的参数列表

其实这个一点也不奇怪,想想在之前的动态代理案例中,咱不就是可以随便改参数的嘛。

2.3 返回通知和异常通知的特殊参数

之前我们在写返回通知和异常通知时,还有一个小问题没有解决:返回通知中我们要拿到方法的返回值,异常通知中我们要拿到具体的异常抛出。这个呢,其实非常容易解决。

咱先把之前的代码再拿出来:(顺便简化了一下)

@AfterReturning("execution(* com..FinanceService.subtractMoney(double))")
public void afterReturningPrint() {
    System.out.println("Logger afterReturningPrint run ......");
}

@AfterThrowing("defaultPointcut()")
public void afterThrowingPrint() {
    System.out.println("Logger afterThrowingPrint run ......");
}

想拿到返回值或者异常非常简单,两个步骤。

首先在方法的参数列表中声明一个 result 或者 e :

@AfterReturning("execution(* com..FinanceService.subtractMoney(double))")
public void afterReturningPrint(Object retval) {
    System.out.println("Logger afterReturningPrint run ......");
    System.out.println("返回的数据:" + retval);
}

@AfterThrowing("defaultPointcut()")
public void afterThrowingPrint(Exception e) {
    System.out.println("Logger afterThrowingPrint run ......");
}

注意只是这样写了之后,此时运行 main 方法是不好使的,是拿不到返回值的!我们还需要告诉 SpringFramework ,我拿了一个名叫 retval 的参数来接这个方法返回的异常,拿一个名叫 e 的参数来接收方法抛出的异常,反映到代码上就应该是这样:

@AfterReturning(value = "execution(* com..FinanceService.subtractMoney(double))", returning = "retval")
public void afterReturningPrint(Object retval) {
    System.out.println("Logger afterReturningPrint run ......");
    System.out.println("返回的数据:" + retval);
}

@AfterThrowing(value = "defaultPointcut()", throwing = "e")
public void afterThrowingPrint(Exception e) {
    System.out.println("Logger afterThrowingPrint run ......");
    System.out.println("抛出的异常:" + e.getMessage());
}

这样再运行 main 方法,控制台才会打印出方法的返回值:

FinanceService 付钱 === 543.21
Logger afterReturningPrint run ......
返回的数据:543.21

异常的信息接收同理,小伙伴们可以自行测试。

3. 多个切面的执行顺序【熟悉】

日常开发中,或许我们会碰到一些特殊的情况:一个方法被多个切面同时增强了,这个时候如何控制好各个切面的执行顺序,以保证最终的运行结果能符合最初设计,这个也是非常重要的,咱有必要来研究一下多个切面的执行顺序问题。

本节源码位置:com.linkedbear.spring.aop.d_order 。

3.1 代码准备

咱先把测试的代码准备一下,很简单,咱只声明两个切面和一个 Service 即可:

@Service
public class UserService {
    
    public void saveUser(String id) {
        System.out.println("UserService 保存用户" + id);
    }
}
@Component
@Aspect
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void printLog() {
        System.out.println("LogAspect 打印日志 ......");
    }
}
@Component
@Aspect
public class TransactionAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void beginTransaction() {
        System.out.println("TransactionAspect 开启事务 ......");
    }
}

然后,编写配置类,开启注解 AOP :

@Configuration
@ComponentScan("com.linkedbear.spring.aop.d_order")
@EnableAspectJAutoProxy
public class AspectOrderConfiguration {
    
}

最后,编写启动类,用上面的配置类驱动 IOC 容器:

public class AspectOrderApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AspectOrderConfiguration.class);
        UserService userService = ctx.getBean(UserService.class);
        userService.saveUser("abc");
    }
}

运行 main 方法,控制台可以打印出两个切面的前置通知:

LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
UserService 保存用户abc

3.2 预设的顺序?

观察这个打印的结果,它是打印日志在前,开启事务在后,这难不成是因为我先写的 LogAspect ,后写的 TransactionAspect ,它就按照我的顺序来了?那不可能啊,即便我后写 TransactionAspect ,也是日志打印在前啊!所以它一定有一个默认的预设规则。

小伙伴可以先猜测一下预设的规则是什么,小册在这里再写一个切面,想必写完这个切面后,基本上顺序就非常容易推理了:

@Component
@Aspect
public class AbcAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void abc() {
        System.out.println("abc abc abc");
    }
}

重新运行 main 方法,发现 AbcAspect 的前置通知打印在 LogAspect 之前!

由此是不是咱就可以推测出预设的顺序了:默认的切面执行顺序,是按照字母表的顺序来的

严谨一点,排序规则其实是根据切面类的 unicode 编码,按照十六进制排序得来的,unicode 编码靠前的,那自然就会排在前面。(作者个人习惯称其为字典表顺序)

3.3 显式声明执行顺序

那我们不能因为这个毛病,就搞得调整顺序就必须改类名吧,一定有更好的方案才对。

还记得之前在第 26 章,小册讲解 BeanPostProcessor 的 javadoc 中提到的 Ordered 接口吗?而且在 IOC 原理的 BeanPostProcessor 的初始化部分,也提到过有关排序的接口,也涉及到了这个 Ordered 接口。不过前面我们一直都没有实际演示 Ordered 接口的使用,这里咱就来搞一下。

现在咱希望让事务控制的切面提早执行,让它在所有切面之前,那么我们就可以这样写:给 TransactionAspect 实现 Ordered 接口,并声明 getOrder 的返回值:

@Component
@Aspect
public class TransactionAspect implements Ordered {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void beginTransaction() {
        System.out.println("TransactionAspect 开启事务 ......");
    }
    
    @Override
    public int getOrder() {
        return 0;
    }
}

这个值设置成多少呢?咱先放个 0 试试,运行 main 方法,观察控制台的打印:

TransactionAspect 开启事务 ......
abc abc abc
LogAspect 打印日志 ......
UserService 保存用户abc

咦,发现事务切面的前置通知已经提前执行了,说明 0 这个顺序已经是提早了的,那最晚的时机对应的 order 值是什么呢?

3.3.1 默认的排序值?

很简单,Integer 的最大值 2147483647 嘛!其实在 Ordered 接口中,就有这两个常量的定义:

public interface Ordered {
	int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
	int LOWEST_PRECEDENCE = Integer.MAX_VALUE;

那我们把这个值调到最低试一下?

@Override
public int getOrder() {
    return Ordered.LOWEST_PRECEDENCE;
}

重新运行 main 方法,发现事务的切面又回去了:

abc abc abc
LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
UserService 保存用户abc

那到底默认值是啥呢?咱把 order 值往上调一个点试试?

@Override
public int getOrder() {
    return Ordered.LOWEST_PRECEDENCE - 1;
}

再次重新运行 main 方法,事务的切面打印又上去了。。。

所以得出结论:在不显式声明 order 排序值时,默认的排序值是 Integer.MAX_VALUE 。

3.3.2 另一种声明办法

除了使用 Ordered 接口,还有通过注解的方式声明:@Order 。

这次我们在 LogAspect 上标注 @Order 注解,并声明一下排序值:

@Component
@Aspect
@Order(0)
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
    public void printLog() {
        System.out.println("LogAspect 打印日志 ......");
    }
}

重新运行 main 方法,发现日志切面的打印提到最早了:

LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
abc abc abc
UserService 保存用户abc

说明 @Order 注解也可以实现同样的效果。

4. 同切面的多个通知执行顺序【熟悉】

除了多个切面的顺序问题,如果同一个切面定义了多个相同类型的通知,它的执行顺序又是怎么样呢?咱也来研究一下。

这次编码的内容就少多了,直接在 AbsAspect 中添加一个方法 def 即可:

@Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
public void def() {
    System.out.println("def def def");
}

直接重新运行 main 方法,控制台先后打印了 abc 和 def 的内容:

LogAspect 打印日志 ......
TransactionAspect 开启事务 ......
abc abc abc
def def def
UserService 保存用户abc

原因是什么呢?估计小伙伴们也能猜到了,跟上面的逻辑一样,都是根据unicode编码顺序(字典表顺序)来的。

至于怎么搞,小伙伴们立马想到办法了吧!直接在方法上标注 @Order 注解就 OK (方法没办法实现接口的嘛)!

@Before("execution(* com.linkedbear.spring.aop.d_order.service.UserService.*(..))")
@Order(Ordered.HIGHEST_PRECEDENCE)
public void def() {
    System.out.println("def def def");
}

运行 main 方法,发现 def 的通知内容并没有被提前执行。。。看来这个办法行不通。。。

那怎么办呢?哎,这还真没办法。。。只能靠方法名去区分了。。。(是不是很无奈)

好了,有关切面、通知的执行顺序的研究,咱就到这里了。

5. 代理对象调用自身的方法【熟悉】

有一些特殊的场景下,我们产生的这些代理对象,会出现自身调用自身的另外方法的。下面我们也来演示一下这个现象。

本节源码位置:com.linkedbear.spring.aop.e_aopcontext 。

5.1 代码准备

测试代码还是三个类,不再重复了:

@Service
public class UserService {
    
    public void update(String id, String name) {
        this.get(id);
        System.out.println("修改指定id的name。。。");
    }
    
    public void get(String id) {
        System.out.println("获取指定id的user。。。");
    }
}
@Component
@Aspect
public class LogAspect {
    
    @Before("execution(* com.linkedbear.spring.aop.e_aopcontext.service.UserService.*(..))")
    public void beforePrint() {
        System.out.println("LogAspect 前置通知 ......");
    }
}
@Configuration
@ComponentScan("com.linkedbear.spring.aop.e_aopcontext")
@EnableAspectJAutoProxy
public class AopContextConfiguration {
    
}

然后,依然是编写测试启动类:

public class AopContextApplication {
    
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AopContextConfiguration.class);
        UserService userService = ctx.getBean(UserService.class);
        userService.update("abc", "def");
    }
}

这样写完之后,运行 main 方法,发现控制台只打印了一次 LogAspect 的切面打印:

LogAspect 前置通知 ......
获取指定id的user。。。
修改指定id的name。。。

如果需求是每次调用 UserService 的方法都需要打印切面日志,应该怎么处理呢?

5.2 不优雅的解决方案

可能有的小伙伴想到了一个能行但不太优雅的解决方案,就是利用依赖注入的特性,把自己注入进来,之后不用 this.get ,换用 userService.get 方法:

@Service
public class UserService {
    
    @Autowired
    UserService userService;
    
    public void update(String id, String name) {
        // this.get(id);
        userService.get(id);
        System.out.println("修改指定id的name。。。");
    }

重新运行 main 方法,控制台确实打印了两次切面日志:

LogAspect 前置通知 ......
LogAspect 前置通知 ......
获取指定id的user。。。
修改指定id的name。。。

但是吧。。。这样写真的好吗。。。有木有感觉怪怪的。。。难不成SpringFramework 就没有考虑到这个问题吗?

5.3 正确的解决方案:AopContext

当然还是得有的,SpringFramework 从一开始就考虑到这个问题了,于是它提供了一个 AopContext 的类,使用这个类,可以在代理对象中取到自身,它的使用方法很简单:

public void update(String id, String name) {
    ((UserService) AopContext.currentProxy()).get(id);
    System.out.println("修改指定id的name。。。");
}

使用 AopContext.currentProxy() 方法就可以取到代理对象的 this 了。

不过这样直接写完之后,运行是不好使的,它会抛出一个异常:

Exception in thread "main" java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.

这个异常的大致含义是,没有开启一个 exposeProxy 的属性,导致无法暴露出代理对象,从而无法获取。那开启 exposeProxy 这个属性的位置在哪里呢?好巧不巧,它是在我们一开始学习注解 AOP 的那个 @EnableAspectJAutoProxy 上:

@Configuration
@ComponentScan("com.linkedbear.spring.aop.e_aopcontext")
@EnableAspectJAutoProxy(exposeProxy = true) // 暴露代理对象
public class AopContextConfiguration {
    
}

它的默认值是 false ,改为 true 之后,再运行 main 方法,就可以达到同样的效果了,控制台会打印两次切面日志。

 

免责声明:
1.本站所有内容由本站原创、网络转载、消息撰写、网友投稿等几部分组成。
2.本站原创文字内容若未经特别声明,则遵循协议CC3.0共享协议,转载请务必注明原文链接。
3.本站部分来源于网络转载的文章信息是出于传递更多信息之目的,不意味着赞同其观点。
4.本站所有源码与软件均为原作者提供,仅供学习和研究使用。
5.如您对本网站的相关版权有任何异议,或者认为侵犯了您的合法权益,请及时通知我们处理。
火焰兔 » Spring AOP进阶-AOP的延伸知识和进阶使用