之前在一次项目中有同事吐槽说使用了注解切面,但是在debug的时候神奇的发现,注解失效了,神奇的地方是,同一个调用链上的方法有的方法回生效 ,有的则不会会。这是一个比较有意思的问题,下面就针对这个奇怪的问题做一次复盘分析。
解决问题的四要素就得搬出来了,复现--猜测--求证--结论
场景复现
注解
@Documented
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value =ElementType.METHOD)
public @interface PrintLog {
}
切面
@Aspect
@Slf4j
@Component
public class LogAspect {
@Pointcut("@annotation(annotationAspect.annotations.PrintLog)")
public void aspectAction(){}
@Before("aspectAction()&&@annotation(printLog)")
public void before(JoinPoint joinPoint, PrintLog printLog){
log.info("注解作用的方法名: " + joinPoint.getSignature().getName());
log.info("所在类的简单类名: " +
joinPoint.getSignature().getDeclaringType().getSimpleName());
}
}
控制层
public class LogController {
@Resource
private LogService logService;
@RequestMapping("log")
public Response aspect(@RequestBody Log log) {
return logService.aspect(log);
}
}
业务层
@Service
@Slf4j
public class LogService {
@PrintLog
public Response
doSomething(log);
return Response.success(log);
}
@PrintLog
public void doSomething(Log logs){
log.info("doSomething");
log.info("{}",logs);
}
}
启动后预想的日志效果应该是这样:
注解作用的方法名: aspect
所在类的简单类名: LogService
注解作用的方法名: doSomething
所在类的简单类名: LogService
doSomething
Log(address=102002111659, time=20220618, line=102, token=aspect)
但是实际的效果是:
注解作用的方法名: aspect
所在类的简单类名: LogService
doSomething
Log(address=102002111659, time=20220618, line=102, token=aspect)
大胆猜测
现象告诉我们doSomething 这个方法虽然贴了注解,但是并没有生效,但是aspect这个方法贴的注解是生效了的,所以猜测注解本身存在问题。
排除注解本身不生效问题,我们大胆猜测:
- Logservice.dosomething 方法没有被代理,先不管对不对,先验,不对我们回头再重新开始。
小心求证
那么为什么Logservice.dosomething没有被代理上,而Logservice.aspect方法被代理上了?
我们先通过debug来获取一些信息--
1.下面这张图中控制层中这个logservice是一个代理对象;
下面两张图中已经进入业务层代码了,
2.LogService .aspect方法的切面注解已经生效了,打印了日志,下一步要执行的是dosomething方法;
3.调用dosomething方法的对象可以看到是this对象,即,LogService 的实例;
4.注意看左边,明显和调用 logservice .aspect方法状态不同,调用logservice .aspect方法之前还经历了两个步骤:
LogService$$EnhancerBySpringCGLIB$$bd77074 (annotationAspect.services)
LogService$$FastClassBySpringCGLIB$$86f0c27a (annotationAspect.services)
收获四条有用的信息,根据这些信息我们可以得出,logservice .aspect方法之所以能够打印出日志是因为这个方法执行了切面方法,而logservice .dosomething则没有,为什么呢?因为调用logservice .aspect方法是通过代理对象调用的,而logservice .dosomething则是由this调用的,就是这点不同导致了logservice .aspect方法打印了日志,logservice .dosomething没有打印日志。
结论
代理的理念是对方法功能进行增强,而不影响当前类,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。即,增强的代码不会出现在目标类字节码文件中,spring的AOP本身就是基于代理模式实现的,如本文中的日志切面代理,打印日志的代码只会存在于代理类的字节码文件中,不会被添加到LogService类中去,所以当使用this调用 dosomething方法时只是一个普通的调用并没有经过日志切面,也就不会打印日志了。
其实我们还可以看出当前代理模式使用的是CGLIB 代理,CGLIB代理是基于继承来实现的,还有一种代理模式 JDK代理,它是基于接口实现的,优缺点各不同。
Java提倡面向接口编程,而JDK代理非常契合这个理念,但是呢,使用JDK代理时目标类必须要继承一个接口,这使得程序猿在写BUG时十分不爽, 所以项目中使用CGLIB的比JDK代理的要多,这是因为spring针对代理做了策略---默认使用JDK代理(看来是支持面向接口编程的),但是如果目标类没有接口则使用CGLIB代理。
各位小伙伴要注意一点的是,CGLIB还是存在坑的,因为它是基于继承的,所以如果目标类中的方法不能被继承那么也是无法被代理的,比如,final。。。



