什么? 使用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了
相关推荐
皮皮林5512 小时前
拒绝写重复代码,试试这套开源的 SpringBoot 组件,效率翻倍~
java·spring boot
用户908324602733 天前
Spring AI 1.1.2 + Neo4j:用知识图谱增强 RAG 检索(上篇:图谱构建)
java·spring boot
用户8307196840823 天前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解3 天前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解3 天前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记4 天前
Spring Boot Web MVC配置详解
spring boot·后端
初次攀爬者4 天前
Kafka 基础介绍
spring boot·kafka·消息队列
用户8307196840824 天前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
Java水解4 天前
SpringBoot3全栈开发实战:从入门到精通的完整指南
spring boot·后端
初次攀爬者5 天前
RocketMQ在Spring Boot上的基础使用
java·spring boot·rocketmq