MENU

Spring经过动态代理后注解失效原因及解决方法

May 15, 2022 • 学习笔记

关于Spring项目中,AOP注解失效的原因与解决方法

前言

今天尝试使用了Spring中AOP面向切面的编程方法,遇到了一个比较坑的问题。

代码如下:

  1. Time注解类:

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD,ElementType.TYPE,ElementType.FIELD})
    public @interface Time {}
  2. Aspect拦截类

    @Aspect
    @Component
    public class TimeAspect {
        @Around("@annotation(com.example.demo.annotations.Time)")
        public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
            System.out.println("doAround");
            return proceedingJoinPoint.proceed();
        }
    }
  3. Controller类

    @Controller
    public class TestController {
        @RequestMapping(path = "/index", method = RequestMethod.GET)
        public String index(){
            test();
            return "login";
        }
        
        @Time            //在此加了自定义注释
        public void test() throws Exception {
            System.out.println("test");
        }
    }

发现在这样一个例子中test方法上的@Time注解始终处于无效状态(但是如果将注解标注在@RequestMapping的方法上就可以正常通过Aspect拦截),查看编译后的代码,注解仍然存在,没有问题。

接着尝试使用反射调用方法,一样没有效果,并且发现获取不到方法上的注解了。

image-20191230170231784

这就很神奇了,注解哪去了呢。

开始找原因

开始找丢失的注解,首先顺着Spring的源码一层一层的看,看了很久终于发现了一些线索。

首先为什么标注在@RequestMapping注解上的自定义注解可以正常运行

根据源码显示,SpringBoot项目初始化的时候会扫描带有@Controller@Service等Spring自带的注解,并且会将被@RequestMapping标注的Method封装在org.springframework.web.method.HandlerMethod中,并且统一存放在org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistryMap<T, MappingRegistration<T>> registry中,这里存储的Method时能够获取到它的Annotation的,然后在请求过程中会调用InvokeHandler这个方法,所以被@RequestMapping标记的方法上的自定义注解可以被正常拦截。

那么为什么在接着调用test()方法自定义注解就无效了呢

又顺着Spring源码接着找,发现Spring在启动过程中,会将标注Spring注解的类进行动态代理,使用JDK的Proxy或者CGLib,由于进行了动态代理,这时在启动完成运行的状态下class已经不再是原先的class了,而是使用了被代理的class。

举个例子:

如果你想要通过类X的对象直接调用其中带注解的A方法,此注解是有效的。因为此时,Spring会判断你将要调用的方法上存在AOP注解,那么会使用类X的代理对象调用A方法。

但是假设类X中的A方法会调用带注解的B方法,而你依然想要通过类X对象调用A方法,那么B方法上的注解是无效的。因为此时Spring判断你调用的A并无注解,所以使用的还是原对象而非代理对象。接下来A再调用B时,在原对象内B方法的注解当然无效了。

简而言之:就是如果在一个类中通过A方法调用带注解的B方法就会失败,因为这时使用的是原来的类实例,而不是代理过后的类实例,所以Aspect不会生效。

解决方法

既然知道了原因,那么要解决也就比较简单了。

  1. 使用@Autowired注解自动装填当前对象,这时获取的是当前对象的代理对象。

        @Autowired
        private TestController testController;
        
        testController.test();
  2. Spring提供了一个自动获取当前对象的代理对象的工具方法AopContext.currentProxy()

    ((TestController)AopContext.currentProxy()).test();