热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

查找注解处理方法_Shiro权限注解与Aop冲突问题探究

在做springboot和shiro集成时,在度娘的多篇博文上相关DefaultAdvisorAutoProxyCreator有下图的描述,但实际测试时将usePrefix和proxyTargetCl

f77918cdd0e46d484acdb80dac99b6dc.png

在做springboot和shiro集成时,在度娘的多篇博文上相关DefaultAdvisorAutoProxyCreator有下图的描述,但实际测试时将usePrefix和proxyTargetClass二者任意一值设为true都可以解决无法映射请求的问题,此文即是基于此的拓展,有兴趣的童鞋可以在测试项目中进行测试。

ef9c4d5425793d7657344818185bc415.png

猜测

要搞明白为这两个配置能够解决这个问题,可以先从DefaultAdvisorAutoProxyCreator开始看起,从类名上我们很容易得知这是spring自动代理创建器之一。该类可以对通知器进行过滤。

源码注释中表明,通过设置usePrefix值为true,类将仅在通知器bean名称为advisorBeanNamePrefix+.时生效。

18bdd13e934ea0e9f1caba404171b0ce.png

而proxyTargetClass则配置了代理时是否时基于类代理。

由前面两点可以猜测

  • usePrefix能够解决这个问题,是由于具体问题的发生场景和通知器bean名称有关,由于usePrefix使某个通知器被过滤使问题被解决。
  • proxyTargetClass能够解决这个问题,是因为此配置修改了代理机制,使代理的冲突消失了。

猜测不一定是正确的,并且由于二者都可以解决这个问题,还需要探求二者之间的关联。

由请求开始

当aop与权限注解共存时,不对DefaultAdvisorAutoProxyCreator进行任何配置并跟踪请求,部分执行链如下:

// {}表示同上
.
├── DispatcherServlet.doService
│ ├── DispatcherServlet.doDispatch
│ ├── DispatcherServlet.getHandler
│ ├── AbstractHandlerMapping.getHandler
│ ├── RequestMappingInfoHandlerMapping.getHandlerInternal
│ └── AbstractHandlerMethodMapping.lookupHandlerMethod

之所以跟踪这个请求,是由于在doDispatch方法中的HandlerExecutionChain类型变量是spring由容器中取得的实际处理请求的对象。

941c8ef78bd33447ce80b0e422bed9be.png

以下是测试控制器的部分代码

@RestController
public class IndexController {
@GetMapping
@RequiresPermissions("admin")
public String empty() {
return "hello";
}
}

AbstractHandlerMethodMapping.lookupHandlerMethod中,可以看到此处从一个映射注册表中的两个hashMap对象中为请求获取最合适的处理方法,而在映射注册表中根本不存在我们的自定义控制器,由此得知问题的根源在于映射注册表中控制器的缺失。

5bd955e0065584c46ccdf04e4e309cc2.png

4368d8765678666778277271895c2e35.png

映射注册表初始化

AbstractHandlerMethodMapping类中对类变量mappingRegistry进行查找,可以找到registerHandlerMethod方法,用于向注册表中注册处理方法。

9fd0c39988dec1e008decc6fef4ab137.png

而该方法的调用在detectHandlerMethods方法中,通过对该方法断点调试(启动时)并向上查找堆栈信息,我们可以得到这样一条调用链:

.
├── AbstractAutowireCapableBeanFactory.invokeInitMethods
│ ├── RequestMappingHandlerMapping.afterPropertiesSet
│ ├── AbstractHandlerMethodMapping.afterPropertiesSet
│ ├── AbstractHandlerMethodMapping.initHandlerMethods
│ ├── AbstractHandlerMethodMapping.processCandidateBean
│ └── AbstractHandlerMethodMapping.detectHandlerMethods

RequestMappingHandlerMapping是springboot WebMvcAutoConfiguration中预定义的,自动初始化的bean,用于处理被RequestMapping注解标记的方法。

1661b36eb3ea599fc92920726ebbdf9c.png

而由于该类父类实现了InitializingBean接口,bean初始化完毕时会调用afterPropertiesSet方法,由调用链可以看出,映射注册表的初始化即是由这个逻辑促成的。跟踪正常映射的请求可以验证这一点:

de3375f0d61f4ef0417adb0cfd9b8361.png

AbstractHandlerMethodMapping.processCandidateBean方法中可以找到类能否被注册进映射注册表的逻辑:

38b928a4254b59f6b41b49fb47f9f28a.png

spring从应用上下文中获取根据名称获取对应bean的类对象并根据类对象中是否含有Controller和RequestMapping注解来判断是否注册。

d181a01a05aef233e6bc31f2998517c8.png

通过调试可以看到加入权限异常映射的bean,在容器中取到的类对象是代理对象,下面是几种情况取到的类对象:

438532afa876f9cfd9971a493a75076e.png

可以看到前者取到的对象是由jdk动态代理的,而后两者取到的对象都是由cglib代理的。

由以上情况又引申出了三个问题:

  • 为什么在jdk动态代理的情况下认为不存在注解
  • 使用哪种代理方式是如何判断的
  • 为什么设置usePrefix时需要使用cglib代理

如何判断注解的存在

这里涉及到调用链:

.
├── AnnotatedElementUtils.hasAnnotation
│ ├── TypeMappedAnnotations.isPresent
│ ├── TypeMappedAnnotations.scan
│ ├── AnnotationsScanner.scan
│ ├── AnnotationsScanner.scan
│ ├── AnnotationsScanner.process
│ ├── AnnotationsScanner.processClass
| └── AnnotationsScanner.processClassHierarchy

AnnotationsScanner.processClassHierarchy中可以看到spring首先判断类对象上是否存在注解,如果不存在则递归查找该类对象父类,所有的接口和内部类。

/**
* 按层次执行类查找
*
* @param context 需要寻找的注解
* @param aggregateIndex
* @param source 类对象
* @param processor 注解处理器
* @param classFilter 类过滤(判断是否要忽略某些类查找)
* @param includeInterfaces 是否包含接口
* @param includeEnclosing 是否包含匿名内部类
* @param 此处为Boolean
* @return 匹配成功返回true 否则返回false或者null
*/
private static R processClassHierarchy(C context, int[] aggregateIndex, Class source, AnnotationsProcessor processor, @Nullable BiPredicate> classFilter, boolean includeInterfaces, boolean includeEnclosing) {
try {
R result = processor.doWithAggregate(context, aggregateIndex[0]);
if (result != null) {
return result;
}
// 是否仅有普通的java注解
if (hasPlainJavaAnnotationsOnly(source)) {
return null;
}
// 判断类本身是否存在注解
Annotation[] annotatiOns= getDeclaredAnnotations(context, source, classFilter, false);
result = processor.doWithAnnotations(context, aggregateIndex[0], source, annotations);
if (result != null) {
return result;
}
aggregateIndex[0]++;
// 如果存在实现接口则遍历接口执行查找 判断接口上是否存在该注解
if (includeInterfaces) {
for (Class interfaceType : source.getInterfaces()) {
R interfacesResult = processClassHierarchy(context, aggregateIndex,
interfaceType, processor, classFilter, true, includeEnclosing);
if (interfacesResult != null) {
return interfacesResult;
}
}
}
// 向上查找所有父类
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;
}
}
// 查找内部类
if (includeEnclosing) {
// Since merely attempting to load the enclosing class may result in
// automatic loading of sibling nested classes that in turn results
// in an exception such as NoClassDefFoundError, we wrap the following
// in its own dedicated try-catch block in order not to preemptively
// halt the annotation scanning process.
try {
Class enclosingClass = source.getEnclosingClass();
if (enclosingClass != null) {
R enclosingResult = processClassHierarchy(context, aggregateIndex, enclosingClass, processor, classFilter, includeInterfaces, true);
if (enclosingResult != null) {
return enclosingResult;
}
}
}
catch (Throwable ex) {
AnnotationUtils.handleIntrospectionFailure(source, ex);
}
}
}
catch (Throwable ex) {
AnnotationUtils.handleIntrospectionFailure(source, ex);
}
return null;
}

在使用cglib代理的情况下,可以在父类中查找到原始类对象,并获取到注解。

512392f06019bc1364add2e10ce155d6.png

而在jdk动态代理情况下获得的对象中无法从类的父子关系,接口实现,内部类中得到原始类对象,也就无法获得原始注解。

以上可以得知代理方式的变更会导致类无法注册入映射注册表,导致bean无法映射请求。

在上面的探究中对比两种代理类的实现接口还发现,获取到jdk动态代理类对象时似乎被二次代理了,其中的原因我们放到后面再探究。

c6363dfb036f054cdd5aa56ea266adb6.png

代理方式的选择

bean的后置处理

要了解这个问题,需要对bean的创建过程进行跟踪:

.
├── AbstractAutowireCapableBeanFactory.doCreateBean
│ ├── {}.initializeBean
│ ├── {}.applyBeanPostProcessorsAfterInitialization
│ ├── AbstractAutoProxyCreator.postProcessAfterInitialization
│ ├── AbstractAutoProxyCreator.wrapIfNecessary
│ ├── {}.createProxy
│ ├── AnnotationsScanner.processClass
| └── AnnotationsScanner.processClassHierarchy

在bean初始化完成后将在AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization方法进行后置处理,spring从容器中获取所有的后置处理器即所有实现BeanPostProcessor接口的bean对bean进行后置处理,在这里可以找到DefaultAdvisorAutoProxyCreator

b8ca25bd7545aa84f254f357411fff28.png

在跟踪DefaultAdvisorAutoProxyCreator之前,先跟踪AnnotationAwareAspectJAutoProxyCreator,以了解一个正常执行的代理流程。

AnnotationAwareAspectJAutoProxyCreator的加载

AnnotationAwareAspectJAutoProxyCreator是spring aop工作的重要构建器,上文我们猜测异常bean可能被二次代理了,第一次代理便是来自spring aop的代理。

在AopAutoConfiguration可以看到在默认配置(且存在Advice类)下使用注解开启了Aspect J自动代理

ec65e13de460ffb36a47526df74a6abc.png

在此注解存在的情况下AspectJAutoProxyRegistrar将工作,这里将进行AnnotationAwareAspectJAutoProxyCreator的注册工作:

.
├── AspectJAutoProxyRegistrar.registerBeanDefinitions
│ ├── AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary
│ └── {}.registerOrEscalateApcAsRequired

29539e90a6afaba88fcc1c3c04d4d08d.png

可以看到此处为bean定义在注册表中指定了名称

aa410f81a8ada8f4d21b21022b9d9f7b.png

并且在另一处自动配置中将proxyTargetClass值设为了true:

9abd8604ca869f385e342b01000c9462.png

0c49d3018b5aa967d8f07c30ca1e8319.png

AbstractAutoProxyCreator.wrapIfNecessary可以跟踪到AnnotationAwareAspectJAutoProxyCreator的创建代理的工作流程:

/**
* 按需包装(代理)bean
*
* @param bean bean
* @param beanName bean名称
* @param cacheKey 缓存key 用以标记该bean是否需要被通知
* @return 包装(代理)后的bean
*/
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
// this.advisedBeans在postProcessBeforeInstantiation即前置操作中初始化过一次
// 这里是跳过已经预先知道不需要被通知的bean
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
// 在bean的前置处理时其实已经做过类似的判断
// isInfrastructureClass是判断是否为aop的基础设施类 诸如Advice,Pointcut,Advisor等
// shouldSkip是由bean名称判断是否为原始示例 如果为是(名称为bean类名+.ORIGINAL)则跳过
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
// Create proxy if we have advice.
// 获取所有的通知bean 存在则进行代理操作
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 创建代理
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}
// 不存在通知类 则标记为不需要通知
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

创建代理时用到了创建器本身的proxyTargetClass属性用以判断是否基于类代理,这里前面提到,在自动配置时就已经将该bean proxyTargetClass属性默认设置为true,因此由该类创建的代理对象默认都是基于cglib代理的:

6a60ae3cf2d7d044bf0104cf92ed4ff9.png

如果在配置中配置默认代理方式不基于类(spring.aop.proxy-target-class =false),但类不存在合适的代理接口,spring仍可能选择cglib代理:

/**
* 评估接口是否适合代理
*
* @param beanClass bean类对象
* @param proxyFactory 代理工厂
*/
protected void evaluateProxyInterfaces(Class beanClass, ProxyFactory proxyFactory) {
// 获取所有实现接口
Class[] targetInterfaces = ClassUtils.getAllInterfacesForClass(beanClass, getProxyClassLoader());
boolean hasReasOnableProxyInterface= false;
for (Class ifc : targetInterfaces) {
// isConfigurationCallbackInterface判断接口是否配置回调接口 例如:InitializingBean,Closeable等
// isInternalLanguageInterface判断接口是否内部语言接口 例如:GroovyObject,cglib代理工厂接口等
if (!isConfigurationCallbackInterface(ifc) && !isInternalLanguageInterface(ifc) &&
ifc.getMethods().length > 0) {
hasReasOnableProxyInterface= true;
break;
}
}
if (hasReasonableProxyInterface) {
// Must allow for introductions; can't just set interfaces to the target's interfaces only.
for (Class ifc : targetInterfaces) {
proxyFactory.addInterface(ifc);
}
} else {
// 没有合适的实现接口则也设置为true
proxyFactory.setProxyTargetClass(true);
}
}

这一点也解释了为何不引入spring aop的情况下权限注解可用,有兴趣的童鞋可以尝试下载代码进行设置跟踪。这里只讲结论:

  • 不引入spring aop只有DefaultAdvisorAutoProxyCreator生效的情况下,bean实例仍是cglib代理的。
  • 引入了spring aop时DefaultAdvisorAutoProxyCreator获得的类是已代理的,有了在此判断外的实现接口,被判断为适合接口代理,因此bean实例最终使用了jdk动态代理。

DefaultAdvisorAutoProxyCreator的工作

DefaultAdvisorAutoProxyCreatorAnnotationAwareAspectJAutoProxyCreator的工作过程基本类似。从以上工作过程中其实可以大概了解(实际也是如此),DefaultAdvisorAutoProxyCreator异常工作时,对bean再次进行了代理行为,由于该类的proxyTargetClass的默认值为false,且获取的代理对象被判断为适合接口代理,因此采用了jdk动态代理,从bean实例上来看就是被二次代理了。

445f54ab8383597b12219e2c31f62308.png

另外需要提及的是当仅proxyTargetClass为true时虽然进行了两次代理,代理类上获取的直接父类还是原始类(而不是父类的父类才是原始类)。详见:

.
├── AbstractAutoProxyCreator.wrapIfNecessary
│ ├── {}.createProxy
│ ├── ProxyFactory.getProxy
| └── CglibAopProxy.getProxy

66b7d8520e5235e62e223291471c7e55.png

为什么设置usePrefix时需要使用cglib代理

这里的问题其实应该是,为什么设置usePrefix为true时不进行代理,事实上usePrefix为true时,DefaultAdvisorAutoProxyCreator是不工作的。

这里需要从getAdvicesAndAdvisorsForBean方法中进行跟踪,因为当设置usePrefix为true时,该方法取到的通知器是空的。

.
├── AbstractAutoProxyCreator.wrapIfNecessary
│ ├── {}.getAdvicesAndAdvisorsForBean
│ ├── AbstractAdvisorAutoProxyCreator.findEligibleAdvisors
│ ├── {}.findCandidateAdvisors
│ └── BeanFactoryAdvisorRetrievalHelper.findAdvisorBeans

BeanFactoryAdvisorRetrievalHelper.findAdvisorBeans中,从容器中取得的通知器是否要最终返回用于代理,存在isEligibleBean判断。

6a77e15fbe5766370acec44d43e6aed4.png

前面都是和构建器父层抽象类打交道,到这里终于和DefaultAdvisorAutoProxyCreator本身打上了交道。

这个判断最终使用了DefaultAdvisorAutoProxyCreator.isEligibleAdvisorBean的返回结果。

4f82588a5c882b5cf08a22fa02ebe20d.png

DefaultAdvisorAutoProxyCreator.isEligibleAdvisorBean中得知,当usePrefix为false时该方法总是返回true,而当usePrefix为true时需要判断bean名称是否以类属性advisorBeanNamePrefix+.开头判断如何返回。

1855c33bff781a71190cfce49c00ee6c.png

当只定义了usePrefix为true而未定义advisorBeanNamePrefix时,大部分情况下bean名称是无法匹配的,因此通知器无法返回,也就不会执行具体的代理行为。而定义usePrefix为false时代理行为总是执行的。

总结

再次提出之前的两条猜测:

  • usePrefix能够解决这个问题,是由于具体问题的发生场景和通知器bean名称有关,由于usePrefix使某个通知器被过滤使问题被解决。
  • proxyTargetClass能够解决这个问题,是因为此配置修改了代理机制,使代理的冲突消失了。

现在看,两个猜测都是部分正确的,并且以上的探究对其进行了拓展,在探究的过程中我们得知,冲突的本质在于是否使用jdk动态代理,只要bean没有被jdk动态代理,这个映射问题就不会存在。下面是各种场景下的运行情况:

72f380bcc62f8b7febd750ab65ae973d.png

最后总结usePrefix,proxyTargetClass解决的问题:

  • userPrefix属性设为true,阻止了DefaultAdvisorAutoProxyCreator代理创建行为。
  • proxyTargetClass为true,使类对象在被二次代理时,仍旧能够找到原始类对象,并且被成功放入映射注册表。

以上为本文全部内容,本文首发于Maples Blog

8353b0b23d7a5fa3a9a76a9aa3444d37.png

推荐阅读
  • Java容器中的compareto方法排序原理解析
    本文从源码解析Java容器中的compareto方法的排序原理,讲解了在使用数组存储数据时的限制以及存储效率的问题。同时提到了Redis的五大数据结构和list、set等知识点,回忆了作者大学时代的Java学习经历。文章以作者做的思维导图作为目录,展示了整个讲解过程。 ... [详细]
  • Spring源码解密之默认标签的解析方式分析
    本文分析了Spring源码解密中默认标签的解析方式。通过对命名空间的判断,区分默认命名空间和自定义命名空间,并采用不同的解析方式。其中,bean标签的解析最为复杂和重要。 ... [详细]
  • 如何自行分析定位SAP BSP错误
    The“BSPtag”Imentionedintheblogtitlemeansforexamplethetagchtmlb:configCelleratorbelowwhichi ... [详细]
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 本文介绍了一个Java猜拳小游戏的代码,通过使用Scanner类获取用户输入的拳的数字,并随机生成计算机的拳,然后判断胜负。该游戏可以选择剪刀、石头、布三种拳,通过比较两者的拳来决定胜负。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • 有没有一种方法可以在不继承UIAlertController的子类或不涉及UIAlertActions的情况下 ... [详细]
  • 高质量SQL书写的30条建议
    本文提供了30条关于优化SQL的建议,包括避免使用select *,使用具体字段,以及使用limit 1等。这些建议是基于实际开发经验总结出来的,旨在帮助读者优化SQL查询。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
author-avatar
mobiledu2502890451
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有