什么? 使用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了
相关推荐
Flittly5 小时前
【AgentScope Java新手村系列】(14)人机交互
java·spring boot·spring
Flynt1 天前
从Spring Boot 4.0升到4.1,我在Maven和gRPC上栽了跟头
java·spring boot·后端
掉鱼的猫2 天前
Spring Boot → Solon 注解迁移实战指南:一张对照表说清楚
java·spring boot
人活一口气3 天前
Spring Boot与AIGC的完美结合:从零搭建智能内容生成平台
java·spring boot·aigc
java小白小6 天前
SpringBoot(01): 初识SpringBoot,从Spring的痛点说起
spring boot
用户3169353811836 天前
如何从零编写一个 Spring Boot Starter
spring boot
程序员晓琪7 天前
约定大于配置:基于 Java 包名自动生成 API 版本路由的最佳实践
java·spring boot·后端
Flittly7 天前
【AgentScope Java新手村系列】(11)中断与恢复
java·spring boot·spring
把马铃薯变成土豆7 天前
前端Stripe跨境支付对接感想
前端·源码
用户3521802454758 天前
🎆从 Prompt 到 Skill:让 Spring AI Agent 学会"装新技能"
人工智能·spring boot·ai编程