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

1. 背景分析

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

  • 创建切面类

  • 配置AOP

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

java 复制代码
@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,也就是没有注册任何类

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

java 复制代码
@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 作为动态代理的实现,最终只是配置一个属性而已,但是这个属性非常重要

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

2、测试例子

2.1 Cglib实现

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

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

2.2 JDK实现

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

java 复制代码
spring:
  aop:
    proxy-target-class: false

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

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

3. 分析问题

3.1 为什么JDK不行

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

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

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

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

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

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

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

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

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

3.2 为什么Cglib就可以

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

java 复制代码
abstract class AnnotationsScanner
    ...
    private static <C, R> 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了
相关推荐
你知道“铁甲小宝”吗丶4 分钟前
【第33章】Spring Cloud之SkyWalking服务链路追踪
java·spring boot·spring·spring cloud·skywalking
coffee_baby16 分钟前
化繁为简:中介者模式如何管理复杂对象交互
java·spring boot·microsoft·交互·中介者模式
ღ᭄ꦿ࿐Never say never꧂20 分钟前
微服务架构中的负载均衡与服务注册中心(Nacos)
java·spring boot·后端·spring cloud·微服务·架构·负载均衡
.生产的驴29 分钟前
SpringBoot 消息队列RabbitMQ 消息确认机制确保消息发送成功和失败 生产者确认
java·javascript·spring boot·后端·rabbitmq·负载均衡·java-rabbitmq
.生产的驴29 分钟前
SpringBoot 消息队列RabbitMQ在代码中声明 交换机 与 队列使用注解创建
java·spring boot·分布式·servlet·kafka·rabbitmq·java-rabbitmq
听封1 小时前
Thymeleaf 的创建
java·spring boot·spring·maven
你知道“铁甲小宝”吗丶2 小时前
【第34章】Spring Cloud之SkyWalking分布式日志
java·spring boot·spring·spring cloud·skywalking
讓丄帝愛伱5 小时前
spring boot启动报错:so that it conforms to the canonical names requirements
java·spring boot·后端
weixin_586062025 小时前
Spring Boot 入门指南
java·spring boot·后端
雷袭月启5 小时前
SpringBoot实现OAuth客户端
spring boot·oauth客户端