AOP 是日志记录、监控管理、性能统计、异常处理、权限管理、统一认证等各个方面被广泛使用的技术。
我们之所以能无感知地在容器对象方法前后任意添加代码片段,那是由于 Spring 在运行期帮我们把切面中的代码逻辑动态“织入”到了容器对象方法内,所以说 AOP 本质上就是一个代理模式。
一、案例场景
假设我们正在开发一个宿舍管理系统,这个模块包含一个负责电费充值的类 ElectricService,它含有一个充电方法 charge():
@Service
public class ElectricService {
public void charge() throws Exception {
System.out.println("Electric charging ...");
this.pay();
}
public void pay() throws Exception {
System.out.println("Pay with alipay ...");
Thread.sleep(1000);
}
}
因为支付宝支付 pay() 是第三方接口,我们需要记录下接口调用时间。这时候我们就引入了一个 @Around 的增强 ,分别记录在 pay() 方法执行前后的时间,并计算出执行 pay() 方法的耗时。
@Aspect
@Service
@Slf4j
public class AopConfig {
@Around("execution(* com.spring.puzzle.class5.example1.ElectricService.pay())")
public void recordPayPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
joinPoint.proceed();
long end = System.currentTimeMillis();
System.out.println("Pay method time cost(ms): " + (end - start));
}
}
最后我们再通过定义一个 Controller 来提供电费充值接口,定义如下:
@RestController
public class HelloWorldController {
@Autowired
ElectricService electricService;
@RequestMapping(path = "charge", method = RequestMethod.GET)
public void charge() throws Exception {
electricService.charge();
}
}
完成代码后,我们访问上述接口,会发现这段计算时间的切面并没有执行到,输出日志如下:
Electric charging ...
Pay with alipay ...
二、案例解析
我们可以从源码中找到真相。首先来设置个断点,调试看看 this 对应的对象是什么样的:
可以看到,this 对应的就是一个普通的 ElectricService 对象,并没有什么特别的地方。再看看在 Controller 层中自动装配的 ElectricService 对象是什么样:
可以看到,这是一个被 Spring 增强过的 Bean。而 this 对应的对象只是一个普通的对象,并没有做任何额外的增强。
为什么 this 引用的对象只是一个普通对象呢?这还要从 Spring AOP 增强对象的过程来看。我们具体看下创建代理对象的过程。先来看下调用栈:
创建代理对象的时机就是创建一个 Bean 的时候,而创建的的关键工作其实是由 AnnotationAwareAspectJAutoProxyCreator 完成的。它本质上是一种 BeanPostProcessor。所以它的执行是在完成原始 Bean 构建后的初始化 Bean(initializeBean)过程中。
三、问题修正
从上述案例解析中,我们知道,只有引用的是被动态代理创建出来的对象,才会被 Spring 增强,具备 AOP 该有的功能。那什么样的对象具备这样的条件呢?
有两种。一种是被 @Autowired 注解的,于是我们的代码可以改成这样,即通过 @Autowired 的方式,在类的内部,自己引用自己:
@Service
public class ElectricService {
@Autowired
ElectricService electricService;
public void charge() throws Exception {
System.out.println("Electric charging ...");
//this.pay();
electricService.pay();
}
public void pay() throws Exception {
System.out.println("Pay with alipay ...");
Thread.sleep(1000);
}
}
另一种方法就是直接从 AopContext 获取当前的 Proxy。那你可能会问了,AopContext 是什么?简单说,它的核心就是通过一个 ThreadLocal 来将 Proxy 和线程绑定起来,这样就可以随时拿出当前线程绑定的 Proxy。
不过使用这种方法有个小前提,就是需要在 @EnableAspectJAutoProxy 里加一个配置项 exposeProxy = true,表示将代理对象放入到 ThreadLocal,这样才可以直接通过 AopContext.currentProxy() 的方式获取到,否则会报错如下:
按这个思路,我们修改下相关代码:
@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {
// 省略非关键代码
}
业务代码如下:
@Service
public class ElectricService {
public void charge() throws Exception {
System.out.println("Electric charging ...");
ElectricService electric = ((ElectricService) AopContext.currentProxy());
electric.pay();
}
public void pay() throws Exception {
System.out.println("Pay with alipay ...");
Thread.sleep(1000);
}
}