剑客
关注科技互联网

切面编程(三):AspectJ与Shiro不兼容和Spring二次代理错误分析

Shiro与AspectJ的配置

Shiro的配置

根据 我的BLOG文章和官方文档
我们可以得知Shiro在使用注解的时的配置是

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>

其基于注解的权限控制功能

  1. 根据类名可以推断是通过切面的Advisor来完成的(AuthorizationAttributeSourceAdvisor)
  2. 所以说它需要创建自己的动态代理类,是由Spring的DefaultAdvisorAutoProxyCreator动态代理创建的

SpringMVC中AspectJ的配置

根据上文,启用基于注解的AspectJ就很简单了,因为基于注解的Shiro一般也在SpringMVC的Context里,我们采用如下配置

<aop:aspectj-autoproxy proxy-target-class="true"/>

在两者分别配置的时候,配置方法都是对的但是一旦公用,会发现AspectJ会失效

失效的解决办法

方法一

解决办法也很简单,注释掉Shiro配置中的第一句

<!--<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/>-->

注释之后Shiro的注解权限管理功能并不会失效,具体原因我们来细细分析

方法二

给DefaultAdvisorAutoProxyCreator加入参数proxyTargetClass为true

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor">
<property name="proxyTargetClass" value="true"/>
</bean>

配置失效的原因

原因在于二次代理

  1. 由于使用了aop:aspectj-autoproxy强制了proxy-target-class
  2. 也就是说对Web层的Class(主要是Controller)使用了CGLib代理
  3. 然后在Shiro进行代理时使用DefaultAdvisorAutoProxyCreator
  4. 原本应该判断Controller,发现没有任何接口,所以使用CGLib来代理
  5. 但是由于Controller已经被CGLib代理过一次了
  6. DefaultAdvisorAutoProxyCreator拿到对不是Contoller本身,而是CGLib的代理结果
  7. CGLib的代理结果本身是有接口的,干扰了DefaultAdvisorAutoProxyCreator的内部判断
  8. 使用JDK去代理CGLib的代理结果
  9. 结果Controller的函数时去了CGLib的接口中找方法名,发现方法不存在,导致代理失败

为什么会确定是这个原因,废了我好大功夫,详情请看代码追踪。。。。没精力就别看了

代码的追踪(可以不看)

aspectj-autoproxy

在配置文件中的 aop:aspectj-autoproxy
会最终交给名为 AopNamespaceHandler
的类进行处理,进入该类(直接在工程全局搜)我们可以看到

public class AopNamespaceHandler extends NamespaceHandlerSupport {
public AopNamespaceHandler() {
}

public void init() {
this.registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser());
this.registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser());
this.registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator());
this.registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser());
}
}

aspectj-autoproxy 是由 AspectJAutoProxy
BeanDefinitionParser 注册的,然后根据 该Blog提出的说法

  1. 进入AspectJAutoProxyBeanDefinitionParser
  2. 找到parse函数,看到其调用registerAspectJAnnotationAutoProxyCreatorIfNecessary来注册BeanDefinition
  3. 进入registerAspectJAnnotationAutoProxyCreatorIfNecessary可以看到内部的三个register方法
  4. 三个register方法分别对应上述代码中第一个参数的输入,但是均调用了AopConfigUtils
  5. 点击AopConfigUtils,可以发现所有的方法都最终调用registerOrEscalateApcAsRequired
  6. registerOrEscalateApcAsRequired中有个简单的if…else判断
  7. 主要控制了逻辑,如果存在internalAutoProxyCreator则不进行创建新的

该Blog作者提出,由于

<aop:aspectj-autoproxy/> 方式对应的注册AutoProxyCreator 的方法是:registerAspectJAnnotationAutoProxyCreatorIfNecessary
DefaultAdvisorAutoProxyCreator 方式对应的注册AutoProxyCreator 的方法是:registerAutoProxyCreatorIfNecessary;

最终会在第七步骤的函数里,由于这个if…else判断导致不能存在两个代理,所以不能混合使用 DefaultAdvisorAutoProxyCreator
aop:aspectj-autoproxy

这个结论是错误的,是因为DefaultAdvisorAutoProxyCreator不会调用registerAutoProxyCreatorIfNecessary,产生错误的原因是
二次代理
作者追踪错了代码,只是恰巧改对了

DefaultAdvisorAutoProxyCreator

来,跟着我找 DefaultAdvisorAutoProxyCreator 如何创建代理的代码

  1. 进入DefaultAdvisorAutoProxyCreator(直接Command或者Ctrl+点击进入)
  2. 看到其继承于AbstractAdvisorAutoProxyCreator,进入
  3. 可以看到AbstractAdvisorAutoProxyCreator继承于AbstractAutoProxyCreator继续进入
  4. AbstractAutoProxyCreator中有一个方法叫做createProxy,名字太直接了,叫做创建代理
  5. 在createProxy中有一个ProxyFactory对象,就是代理的工厂模式(工厂模式请自行学习)
  6. ProxyFactory对象继承于ProxyCreatorSupport
  7. ProxyCreatorSupport中有对象aopProxyFactory用来创建AOP代理(就是切面代理,注意切面代理是属于动态代理的一种)
  8. ProxyCreatorSupport在构造函数中new了一个DefaultAopProxyFactory给aopProxyFactory赋值
  9. 进入DefaultAopProxyFactory可以看到切面代理创建方法createAopProxy
  10. 在判断条件包含 !config.isProxyTargetClass() 时,也就是不使用针对Class的代理的时候,看下一句
  11. return new JdkDynamicAopProxy(config) 根据config配置返回JDK动态代理

这也是我们在文章一中所说的,通常情况下Spring使用针对接口的JDK代理进行动态代理,绕了这么久,我们回到第五步,看 createProxy
方法如何使用 ProxyFactory对象

  1. 可以看到createProxy 返回值 return proxyFactory.getProxy(this.getProxyClassLoader())
  2. 进入ProxyFactory对象的getProxy方法
  3. 可以看到 return this.createAopProxy().getProxy(classLoader)
  4. 首先调用了createAopProxy,发现ProxyFactory没有这个方法,所以来自父类 ProxyCreatorSupport
  5. 点击进入 看到createAopProxy 内包含this.getAopProxyFactory().createAopProxy(this)
  6. 首先拿到了上文第九步创建的DefaultAopProxyFactory,然后调用了其createAopProxy

以上完成了 DefaultAdvisorAutoProxyCreator 动态代理的创建

两次代理的内部细节

有一些文章分析给出 无法确定二次代理的情况下哪个代理成功
,但是根据我本身追踪日志发现,在Spring启动时,我的切面类日志输出成功

[2016-10-23 14:00:13][qtp1938298155-28][INFO ][] c.b.psas.web.aspect.ControllerLogAspect 28 -- 网络请求开始 RequestURL: / RequestVO参数: null
[2016-10-23 14:00:13][qtp1938298155-28][INFO ][] c.b.psas.web.aspect.ControllerLogAspect 33 -- 网络请求结束 RequestURL: / RequestVO参数: null

但是在调用接口时,开启DEBUG日志,会发现Shiro抛出的异常为

[2016-10-23 13:38:52][qtp1386909980-27][DEBUG][] o.s.web.servlet.DispatcherServlet 1197 -- Handler execution resulted in exception - forwarding to resolved error view: ModelAndView: reference to view with name '/errors/500'; model is {exception=java.lang.IllegalStateException: The mapped controller method class 'com.bestpay.psas.web.controller.manage.LoginController' is not an instance of the actual controller bean class 'com.sun.proxy.$Proxy103'. If the controller requires proxying (e.g. due to @Transactional), please use class-based proxying.
HandlerMethod details:
Controller [com.bestpay.psas.web.controller.manage.LoginController]
Method [public java.lang.String com.bestpay.psas.web.controller.manage.LoginController.login()]
Resolved arguments:
}
java.lang.IllegalStateException: The mapped controller method class 'com.bestpay.psas.web.controller.manage.LoginController' is not an instance of the actual controller bean class 'com.sun.proxy.$Proxy103'. If the controller requires proxying (e.g. due to @Transactional), please use class-based proxying.
HandlerMethod details:
Controller [com.bestpay.psas.web.controller.manage.LoginController]
Method [public java.lang.String com.bestpay.psas.web.controller.manage.LoginController.login()]
Resolved arguments:

at org.springframework.web.method.support.InvocableHandlerMethod.assertTargetBean(InvocableHandlerMethod.java:262) ~[spring-web-4.1.9.RELEASE.jar:4.1.9.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:225) ~[spring-web-4.1.9.RELEASE.jar:4.1.9.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:137) ~[spring-web-4.1.9.RELEASE.jar:4.1.9.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:110) ~[spring-webmvc-4.1.9.RELEASE.jar:4.1.9.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:775) ~[spring-webmvc-4.1.9.RELEASE.jar:4.1.9.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:705) ~[spring-webmvc-4.1.9.RELEASE.jar:4.1.9.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:85) ~[spring-webmvc-4.1.9.RELEASE.jar:4.1.9.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:959) [spring-webmvc-4.1.9.RELEASE.jar:4.1.9.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:893) [spring-webmvc-4.1.9.RELEASE.jar:4.1.9.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:965) [spring-webmvc-4.1.9.RELEASE.jar:4.1.9.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:856) [spring-webmvc-4.1.9.RELEASE.jar:4.1.9.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:735) [servlet-api-3.0.jar:na]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:841) [spring-webmvc-4.1.9.RELEASE.jar:4.1.9.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:848) [servlet-api-3.0.jar:na]
at org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:684) [jetty-servlet-8.1.17.v20150415.jar:8.1.17.v20150415]
at org.eclipse.jetty.servlet.ServletHandler$CachedChain.doFilter(ServletHandler.java:1496) [jetty-servlet-8.1.17.v20150415.jar:8.1.17.v20150415]
at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:449) [shiro-web-1.2.4.jar:1.2.4]
at org.apache.shiro.web.servlet.AbstractShiroFilter$1.call(AbstractShiroFilter.java:365) [shiro-web-1.2.4.jar:1.2.4]
at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:90) [shiro-core-1.2.4.jar:1.2.4]
at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:83) [shiro-core-1.2.4.jar:1.2.4]
at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:383) [shiro-core-1.2.4.jar:1.2.4]
at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:362) [shiro-web-1.2.4.jar:1.2.4]
at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:125) [shiro-web-1.2.4.jar:1.2.4]
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:344) [spring-web-4.1.9.RELEASE.jar:4.1.9.RELEASE]
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:261) [spring-web-4.1.9.RELEASE.jar:4.1.9.RELEASE]

可以在日志中发现几个关键字

  1. org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain
  2. com.sun.proxy.$Proxy103
  3. InvocableHandlerMethod.doInvoke

三个关键字分别说明了

  1. 是Shiro产生了异常,而不是AOP
  2. Shiro尝试去采用了JDK代理
  3. 报错在Invoke方法时

解决方法生效的原因

方法一

因为Shiro也是基于Spring的AOP类的,如果找不到合适的配置,就是默认采用同一个Context下的AOP代理配置,我们给了其proxy-target-class为true,自然就在第二次代理的时候找得到方法

方法二

方法二就更直接了,告诉DefaultAdvisorAutoProxyCreator为True就好

杂想

我以前写过另一个文章,讨论了 SpringMVC和Spring公用的情况下Transactional失效的问题
很有可能底层原因也是二次代理

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址