1. 背景分析
如果我们想要在SpringBoot中使用动态代理十分简单,只需要两步
-
创建切面类
-
配置AOP
这里会使用@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,也就是没有注册任何类
但由于我使用的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作为动态代理的实现,最终只是配置一个属性而已,但是这个属性非常重要
而这个属性最终是加载在AnnotationAwareAspectJAutoProxyCreator
类上,为什么我说是这个类呢,是因为 AspectJAutoProxyRegistrar
中默认注册了的,而且在 AopConfigUtils
中做了排序的,
2、测试例子
2.1 Cglib实现
我们上来还是正常使用Cglib,创建一个切面类,以及一个Controller
这个时候我们启动项目,然后访问接口地址,很正常的一个返回
2.2 JDK实现
上面一节我们分析了,默认SpringBoot是采用Cglib作为动态代理的实现,现在我们要需要指定为JDK,我们要先在配置文件设置如下
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在当前类上找不到这个注解,也会尝试去找父类看看,其原理就在下面的方法中
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了