在做springboot和shiro集成时,在度娘的多篇博文上相关DefaultAdvisorAutoProxyCreator有下图的描述,但实际测试时将usePrefix和proxyTargetClass二者任意一值设为true都可以解决无法映射请求的问题,此文即是基于此的拓展,有兴趣的童鞋可以在测试项目中进行测试。
猜测
要搞明白为这两个配置能够解决这个问题,可以先从DefaultAdvisorAutoProxyCreator开始看起,从类名上我们很容易得知这是spring自动代理创建器之一。该类可以对通知器进行过滤。
源码注释中表明,通过设置usePrefix值为true,类将仅在通知器bean名称为advisorBeanNamePrefix+.时生效。
而proxyTargetClass则配置了代理时是否时基于类代理。
由前面两点可以猜测
- usePrefix能够解决这个问题,是由于具体问题的发生场景和通知器bean名称有关,由于usePrefix使某个通知器被过滤使问题被解决。
- proxyTargetClass能够解决这个问题,是因为此配置修改了代理机制,使代理的冲突消失了。
猜测不一定是正确的,并且由于二者都可以解决这个问题,还需要探求二者之间的关联。
由请求开始
当aop与权限注解共存时,不对DefaultAdvisorAutoProxyCreator进行任何配置并跟踪请求,部分执行链如下:
1 | // {}表示同上 |
之所以跟踪这个请求,是由于在doDispatch方法中的HandlerExecutionChain类型变量是spring由容器中取得的实际处理请求的对象。
以下是测试控制器的部分代码
1 |
|
在AbstractHandlerMethodMapping.lookupHandlerMethod中,可以看到此处从一个映射注册表中的两个hashMap对象中为请求获取最合适的处理方法,而在映射注册表中根本不存在我们的自定义控制器,由此得知问题的根源在于映射注册表中控制器的缺失。
映射注册表初始化
在AbstractHandlerMethodMapping类中对类变量mappingRegistry进行查找,可以找到registerHandlerMethod方法,用于向注册表中注册处理方法。
而该方法的调用在detectHandlerMethods方法中,通过对该方法断点调试(启动时)并向上查找堆栈信息,我们可以得到这样一条调用链:
1 | . |
RequestMappingHandlerMapping是springboot WebMvcAutoConfiguration中预定义的,自动初始化的bean,用于处理被RequestMapping注解标记的方法。
而由于该类父类实现了InitializingBean接口,bean初始化完毕时会调用afterPropertiesSet方法,由调用链可以看出,映射注册表的初始化即是由这个逻辑促成的。跟踪正常映射的请求可以验证这一点:
AbstractHandlerMethodMapping.processCandidateBean方法中可以找到类能否被注册进映射注册表的逻辑:
spring从应用上下文中获取根据名称获取对应bean的类对象并根据类对象中是否含有Controller和RequestMapping注解来判断是否注册。
通过调试可以看到加入权限异常映射的bean,在容器中取到的类对象是代理对象,下面是几种情况取到的类对象:
可以看到前者取到的对象是由jdk动态代理的,而后两者取到的对象都是由cglib代理的。
除此之外当方法只被aop注解标记时也是获得的类对象也是由cglib代理的,当类路径中不存在Advice类时bean是原始类对象,此时权限注解不生效。
由以上情况又引申出了三个问题:
- 为什么在jdk动态代理的情况下认为不存在注解
- 使用哪种代理方式是如何判断的
- 为什么设置usePrefix时需要使用cglib代理
如何判断注解的存在
这里涉及到调用链:
1 | . |
在AnnotationsScanner.processClassHierarchy中可以看到spring首先判断类对象上是否存在注解,如果不存在则递归查找该类对象父类,所有的接口和内部类。
1 | /** |
在使用cglib代理的情况下,可以在父类中查找到原始类对象,并获取到注解。
而在jdk动态代理情况下获得的对象中无法从类的父子关系,接口实现,内部类中得到原始类对象,也就无法获得原始注解。
以上可以得知代理方式的变更会导致类无法注册入映射注册表,导致bean无法映射请求。
在上面的探究中对比两种代理类的实现接口还发现,获取到jdk动态代理类对象时似乎被二次代理了,其中的原因我们放到后面再探究。
代理方式的选择
bean的后置处理
要了解这个问题,需要对bean的创建过程进行跟踪:
1 | . |
在bean初始化完成后将在AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization方法进行后置处理,spring从容器中获取所有的后置处理器即所有实现BeanPostProcessor接口的bean对bean进行后置处理,在这里可以找到DefaultAdvisorAutoProxyCreator
在跟踪DefaultAdvisorAutoProxyCreator之前,先跟踪AnnotationAwareAspectJAutoProxyCreator,以了解一个正常执行的代理流程。
AnnotationAwareAspectJAutoProxyCreator的加载
AnnotationAwareAspectJAutoProxyCreator是spring aop工作的重要构建器,上文我们猜测异常bean可能被二次代理了,第一次代理便是来自spring aop的代理。
在AopAutoConfiguration可以看到在默认配置(且存在Advice类)下使用注解开启了Aspect J自动代理
在此注解存在的情况下AspectJAutoProxyRegistrar将工作,这里将进行AnnotationAwareAspectJAutoProxyCreator的注册工作:
1 | . |
可以看到此处为bean定义在注册表中指定了名称
并且在另一处自动配置中将proxyTargetClass值设为了true:
AbstractAutoProxyCreator.wrapIfNecessary可以跟踪到AnnotationAwareAspectJAutoProxyCreator的创建代理的工作流程:
1 | /** |
创建代理时用到了创建器本身的proxyTargetClass属性用以判断是否基于类代理,这里前面提到,在自动配置时就已经将该bean proxyTargetClass属性默认设置为true,因此由该类创建的代理对象默认都是基于cglib代理的:
如果在配置中配置默认代理方式不基于类(spring.aop.proxy-target-class =false),但类不存在合适的代理接口,spring仍可能选择cglib代理:
1 | /** |
这一点也解释了为何不引入spring aop的情况下权限注解可用,有兴趣的童鞋可以尝试下载代码进行设置跟踪。这里只讲结论:
- 不引入spring aop只有DefaultAdvisorAutoProxyCreator生效的情况下,bean实例仍是cglib代理的。
- 引入了spring aop时DefaultAdvisorAutoProxyCreator获得的类是已代理的,有了在此判断外的实现接口,被判断为适合接口代理,因此bean实例最终使用了jdk动态代理。
DefaultAdvisorAutoProxyCreator的工作
DefaultAdvisorAutoProxyCreator和AnnotationAwareAspectJAutoProxyCreator的工作过程基本类似。从以上工作过程中其实可以大概了解(实际也是如此),DefaultAdvisorAutoProxyCreator异常工作时,对bean再次进行了代理行为,由于该类的proxyTargetClass的默认值为false,且获取的代理对象被判断为适合接口代理,因此采用了jdk动态代理,从bean实例上来看就是被二次代理了。
另外需要提及的是当仅proxyTargetClass为true时虽然进行了两次代理,代理类上获取的直接父类还是原始类(而不是父类的父类才是原始类)。详见:
1 | . |
为什么设置usePrefix时需要使用cglib代理
这里的问题其实应该是,为什么设置usePrefix为true时不进行代理,事实上usePrefix为true时,DefaultAdvisorAutoProxyCreator是不工作的。
这里需要从getAdvicesAndAdvisorsForBean方法中进行跟踪,因为当设置usePrefix为true时,该方法取到的通知器是空的。
1 | . |
在BeanFactoryAdvisorRetrievalHelper.findAdvisorBeans中,从容器中取得的通知器是否要最终返回用于代理,存在isEligibleBean判断。
前面都是和构建器父层抽象类打交道,到这里终于和DefaultAdvisorAutoProxyCreator本身打上了交道。
这个判断最终使用了DefaultAdvisorAutoProxyCreator.isEligibleAdvisorBean的返回结果。
在DefaultAdvisorAutoProxyCreator.isEligibleAdvisorBean中得知,当usePrefix为false时该方法总是返回true,而当usePrefix为true时需要判断bean名称是否以类属性advisorBeanNamePrefix+.开头判断如何返回。
当只定义了usePrefix为true而未定义advisorBeanNamePrefix时,大部分情况下bean名称是无法匹配的,因此通知器无法返回,也就不会执行具体的代理行为。而定义usePrefix为false时代理行为总是执行的。
总结
再次提出之前的两条猜测:
- usePrefix能够解决这个问题,是由于具体问题的发生场景和通知器bean名称有关,由于usePrefix使某个通知器被过滤使问题被解决。
- proxyTargetClass能够解决这个问题,是因为此配置修改了代理机制,使代理的冲突消失了。
现在看,两个猜测都是部分正确的,并且以上的探究对其进行了拓展,在探究的过程中我们得知,冲突的本质在于是否使用jdk动态代理,只要bean没有被jdk动态代理,这个映射问题就不会存在。下面是各种场景下的运行情况:
开启AOP | usePrefix | proxyTargetClass | 代理情况 | 运行情况 |
---|---|---|---|---|
是 | false | false | jdk+cglib | 404 |
是 | false | true | cglib+cglib | 权限生效 |
是 | true | false | cglib | 权限生效 |
是 | true | true | cglib | 权限生效 |
否 | false | false | cglib | 权限生效 |
否 | false | true | cglib | 权限生效 |
否 | true | false | 无 | 权限无效 |
否 | true | true | 无 | 权限无效 |
最后总结usePrefix,proxyTargetClass解决的问题:
userPrefix属性设为true,阻止了DefaultAdvisorAutoProxyCreator代理创建行为。
proxyTargetClass为true,使类对象在被二次代理时,仍旧能够找到原始类对象,并且被成功放入映射注册表。