当前位置: 首页 > 科技观察

为什么说在SpringAOP中,不要用This来调用方法呢?

时间:2023-03-13 12:48:30 科技观察

SpringAOP是Spring中除了依赖注入之外最核心的功能。其原理是利用CGlib和JDK动态代理实现运行时动态方法增强,从而降低系统耦合,提高代码复用性。然而,在享受AOP强大功能和便捷的同时,我们也会经常遇到一些看似莫名其妙的bug。今天我们就来说说为什么在AOP的方法中,不用this来方便的调用方法?使用它时会发生什么?其背后的原理是什么?如何解决?废话不多说,直接上实际代码。场景复现假设我们有一个核心支付类,它有pay()支付功能,会通过record()方法记录哪些用户访问了这个核心功能。@ServicepublicclassPayService{publicvoidpay(){System.out.println("执行一些核心支付业务操作");//记录用户访问日志this.record();}publicvoidrecord(){try{System.out.println("模拟将操作记录发布到日志系统需要100ms");TimeUnit.MICROSECONDS.sleep(100);}catch(InterruptedExceptione){thrownewRuntimeException(e);随着业务的不断扩大,我们需要统计保存访问日志这个耗时动作,看是否会对核心支付功能产生更大的影响。因此,我们使用SpringAOP进行切面处理。@Aspect@ComponentpublicclassLogAspect{@Around(value="execution(*com.shishan.demo2023.service.PayService.record()))")publicObjectrecord(ProceedingJoinPointproceedingJoinPoint)throwsThrowable{longbegin=System.currentTimeMillis();对象proceed=proceedingJoinPoint.proceed();System.out.println("日志耗时:"+(System.currentTimeMillis()-begin));返回进行;}}aspect类很简单,通过@Around方法切入切出record方法,记录方法的执行时间。看起来很完美不是吗?旧的代码不需要改动,只需要增加一个新的切面就可以满足新的需求。让我们创建一个新的控制器,看看我们的切面类是否生效。@RestController@RequestMapping(value="/demo")publicclassDemoController{@ResourceprivatePayServicepayService;@RequestMapping(value="/pay")publicResponseEntitypay(){this.payService.pay();返回ResponseEntity.ok().build();}}启动服务,访问http://localhost:8080/demo/pay。出现问题。根据上面的代码,打印业务日志后,应该打印一行记录耗时日志。但是控制台是空的,说明我们的切面类没有生效。为什么定义的切面没有执行?问题是pay()方法中的this调用。我们可以看到图中this指向的是一个普通的PayService对象,并不是Spring增强的bean。而SpringAOP的工作原理是什么:Spring通过JDK动态代理和CGlib代理为目标类生成代理类,并在代理类中增强功能。再看一下controller中的PayService:可以看到controller中的payService是一个SpringCLlib增强的代理类,而我们通过这个引用的只是一个普通的Spring的bean对象,自然无法实现AOP函数。Spring什么时候代理一个对象?Spring会在创建bean的时候判断是否进行代理。核心类是AnnotationAwareAspectJAutoProxyCreator,本质上是一个BeanPostProcessor。当需要使用AOP时,它会将创建的原始Bean对象包装成一个代理对象,并作为Bean返回。因此,最后的结论是:只有动态代理的对象才能被Spring增强,才具备AOP能力。解决方案既然问题找到了,那么如何解决这个调用导致的AOP失败呢?有两种方法。一、引用自己,直接将自己注入到当前类中,这样Spring会代理类中的属性,生成一个payService代理类。需要注意的是,这实际上是在人为地制造循环依赖。在更高版本的Springboot中,循环依赖默认是关闭的。如果要开启循环依赖,需要配置spring.main.allow-circular-references=true。二、通过AopContextAopContext内部维护了一个保存代理的ThreadLocal。简单的说就是通过一个ThreadLocal把proxy绑定到当前线程,这样绑定到当前线程的Proxy就可以随时取出来了。如果使用该方法,需要在@EnableAspectJAutoProxy中增加一个配置项exposeProxy=true。通过方法一修改代码,看AOP是否生效。可以看到耗时日志成功打印出来了。总结SpringAOP其实会自动为我们创建一个Proxy,让调用者在不知不觉中调用指定的方法,本质上是一个动态代理。只有访问这些代理对象的方法才能获取AOP实现的功能,所以不可能通过这种引用来正确使用AOP功能。