什么? 使用Java JDK动态代理竟然会导致注解失效

2023年 10月 16日 66.8k 0

1. 背景分析

如果我们想要在SpringBoot中使用动态代理十分简单,只需要两步

  • 创建切面类
    image.png

  • 配置AOP

    image.png

这里会使用@EnableAspectJAutoProxy作为动态代理的配置,目的就是为了注册一个AspectJAutoProxyRegistrar

@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {

   /**
    * 是否直接使用Cglib,而不是根据被代理对象确定代理方式
    */
   boolean proxyTargetClass() default false;

   /**
    * 代理类是否需要暴露在{@link org.springframework.aop.framework.AopContext AopContext} 中
    */
   boolean exposeProxy() default false;

}

我们再来看这个AspectJAutoProxyRegistrar想要干嘛,由于我们没有对@EnableAspectJAutoProxy做任何配置,所以他的两个属性都为false,也就是没有注册任何类

image.png

但由于我使用的SpringBoot版本为2.2.6所以说会有一个关于AOP的自动配置类

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "spring.aop", name = "auto", havingValue = "true", matchIfMissing = true)
public class AopAutoConfiguration {

   @Configuration(proxyBeanMethods = false)
   @ConditionalOnClass(Advice.class)
   static class AspectJAutoProxyingConfiguration {

      /**
       * JDK, 重点就代理模式是否根据具体的类型判断
       */
      @Configuration(proxyBeanMethods = false)
      @EnableAspectJAutoProxy(proxyTargetClass = false)
      @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "false",
            matchIfMissing = false)
      static class JdkDynamicAutoProxyConfiguration {

      }

      /**
       * Cglib, 重点就代理模式直接设置为Cglib
       */
      @Configuration(proxyBeanMethods = false)
      // 开启自动配置
      @EnableAspectJAutoProxy(proxyTargetClass = true)
      @ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", havingValue = "true",
            matchIfMissing = true)
      static class CglibAutoProxyConfiguration {

      }

   }
}

分析上面的源码可以看出:当没有在配置文件中配置spring.aop.proxy-target-class = flase的时候,默认使用Cglib作为动态代理的实现,最终只是配置一个属性而已,但是这个属性非常重要

image.png

而这个属性最终是加载在AnnotationAwareAspectJAutoProxyCreator类上,为什么我说是这个类呢,是因为 AspectJAutoProxyRegistrar中默认注册了的,而且在 AopConfigUtils中做了排序的,

image.png

image.png

2、测试例子

2.1 Cglib实现

我们上来还是正常使用Cglib,创建一个切面类,以及一个Controller

image.png

image.png

这个时候我们启动项目,然后访问接口地址,很正常的一个返回

image.png

2.2 JDK实现

上面一节我们分析了,默认SpringBoot是采用Cglib作为动态代理的实现,现在我们要需要指定为JDK,我们要先在配置文件设置如下

spring:
  aop:
    proxy-target-class: false

然后由于JDK是靠接口实现动态代理的,所以说还需要HelloController实现一个接口

image.png

然后再启动项目,访问原来的接口地址,发现竟然404了

image.png

3. 分析问题
3.1 为什么JDK不行

我们要想知道这个问题的原理,要先知道两种不同动态代理的原理,chatgpt还是给出了很多不同点,其中重点就是第一点

image.png

Cglib是基于继承的,JDK是基于接口的,说句人话就是Cglib创建的代理类是继承被代理对象的,而JDK创建的代理对象中只是有一个对象引用指向了被代理对象

然后我们再来看SpringMVC是如何注册接口地址到目标方法映射关系的

由于我是使用@Controller注册接口的,所以对应的就是RequestMappingHandlerMapping, 这个类是靠InitializingBean接口实现初始化阶段的回调的, 大家可以按照我下面的顺序执行代码,会来到isHandler()方法中

afterPropertiesSet() -> initHandlerMethods() -> processCandidateBean() -> isHandler()

我们会发现判断一个类是否是一个处理器的时候,会根据类上是否带有@Controller或者@RequestMapping作为判断依据

image.png

但是JDK是通过实现和被代理类实现相同的接口来实现动态代理的

举个例子,A 是 接口,B 是被代理类,C 是代理类,B 和 C 都实现了 A,但是B 和 C 有直接联系吗? 仅仅是C类上有一个B类的引用而已

在这种情况下,SpringBoot自然是无法知道此类携带哪些注解

3.2 为什么Cglib就可以

Cglib由于是基于继承的实现,所以说就算SpringBoot在当前类上找不到这个注解,也会尝试去找父类看看,其原理就在下面的方法中

abstract class AnnotationsScanner
    ...
    private static  R processClassHierarchy(...) {
        ...
        Class superclass = source.getSuperclass();
        if (superclass != Object.class && superclass != null) {
           R superclassResult = processClassHierarchy(context, aggregateIndex,
              superclass, processor, classFilter, includeInterfaces, includeEnclosing);
           if (superclassResult != null) {
              return superclassResult;
           }
        }
        ...
    }
    ...
}

4. 总结

  • 由于JDK是基于实现相同的接口来实现动态代理的,实际上和被代理对象是没有直接关联的,所以导致SpringBoot是无法获取被代理对象的信息的,导致无法扫描到具体的注解
  • 当然也不仅仅是我举的这一个注解会失效,像@ControllerAdvice等等注解的扫描都会失效
  • 所以说不要没事强行使用JDK, SpringBoot已经默认启动Cglib了

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论