什么? 使用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了
相关推荐
一勺菠萝丶1 小时前
Spring Boot + MyBatis/MyBatis Plus:XML中循环处理List参数的终极指南
xml·spring boot·mybatis
RainbowSea2 小时前
问题:后端由于字符内容过长,前端展示精度丢失修复
java·spring boot·后端
风象南3 小时前
SpringBoot 控制器的动态注册与卸载
java·spring boot·后端
我是一只代码狗3 小时前
springboot中使用线程池
java·spring boot·后端
hello早上好3 小时前
JDK 代理原理
java·spring boot·spring
PanZonghui3 小时前
Centos项目部署之运行SpringBoot打包后的jar文件
linux·spring boot
沉着的码农4 小时前
【设计模式】基于责任链模式的参数校验
java·spring boot·分布式
zyxzyx6664 小时前
Flyway 介绍以及与 Spring Boot 集成指南
spring boot·笔记
一头生产的驴6 小时前
java整合itext pdf实现自定义PDF文件格式导出
java·spring boot·pdf·itextpdf
程序员张38 小时前
SpringBoot计时一次请求耗时
java·spring boot·后端