由來
項目中需要實現某個訂單的狀態改變后然后推送給第三方的功能,由於更改狀態的項目和推送的項目不是同一個項目,所以為了不改變原項目的代碼,我們考慮用spring的aop來實現。
項目用的是springmvc + spring + mybatis 的架構,我們知道spring實現了兩種代理方式:JDK動態代理和CGLB動態代理。所以spring對接口和類都可以實現代理。所以只需要考慮在DAO接口的相關update狀態的方法上加aop就可以了。整理了下共有六個地方對訂單的status做了update。所以配置如下:
<!-- 聲明通知類 --> <bean id="aspectBean" class="com.info.web.service.BorrowOrderStatusAspect"></bean> <aop:config> <aop:aspect id="myAspect" ref="aspectBean"> <aop:pointcut expression="execution(public * com.info.risk.dao.IRiskCreditUserDao.updateAssetsSuc(..))
|| execution(public * com.info.risk.dao.IRiskCreditUserDao.updateAssetsFail(..))
|| execution(public * com.info.web.dao.IBorrowOrderDao.updateByPrimaryKeySelective(..))
|| execution(public * com.info.web.dao.IBorrowOrderDao.updateByPrimaryKeyWithBLOBs(..))
|| execution(public * com.info.web.dao.IBorrowOrderDao.updateByPrimaryKey(..))" id="servicePointcut" /> <aop:after-returning method="doAfter" pointcut-ref="servicePointcut" /> </aop:aspect> </aop:config>
可是啟動項目的時候發現,啟動失敗,報錯信息如下:
……………………………… Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'repaymentService': Injection of autowired dependencies failed; nested exception is org.springframework.beans.factory.BeanCreationException: Could not autowire field: private com.info.web.dao.IBorrowOrderDao com.info.web.service.RepaymentService.borrowOrderDao; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'IBorrowOrderDao': Post-processing of FactoryBean's singleton object failed; nested exception is org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class com.sun.proxy.$Proxy39]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Cannot subclass final class class com.sun.proxy.$Proxy39 at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:334) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1210) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:537) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:476) at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:303) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:299) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194) at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1120) at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1044) at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:942) at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:533) ... 37 more Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field: private com.info.web.dao.IBorrowOrderDao com.info.web.service.RepaymentService.borrowOrderDao; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'IBorrowOrderDao': Post-processing of FactoryBean's singleton object failed; nested exception is org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class com.sun.proxy.$Proxy39]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Cannot subclass final class class com.sun.proxy.$Proxy39 at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:561) at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88) at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:331) ... 48 more Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'IBorrowOrderDao': Post-processing of FactoryBean's singleton object failed; nested exception is org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class com.sun.proxy.$Proxy39]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Cannot subclass final class class com.sun.proxy.$Proxy39 at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.getObjectFromFactoryBean(FactoryBeanRegistrySupport.java:116) at org.springframework.beans.factory.support.AbstractBeanFactory.getObjectForBeanInstance(AbstractBeanFactory.java:1517) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:251) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194) at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1120) at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1044) at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:942) at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:533) ... 50 more Caused by: org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class com.sun.proxy.$Proxy39]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Cannot subclass final class class com.sun.proxy.$Proxy39 at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:212) at org.springframework.aop.framework.ProxyFactory.getProxy(ProxyFactory.java:109) at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.createProxy(AbstractAutoProxyCreator.java:447) at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:333) at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.postProcessAfterInitialization(AbstractAutoProxyCreator.java:293) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsAfterInitialization(AbstractAutowireCapableBeanFactory.java:422) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.postProcessObjectFromFactoryBean(AbstractAutowireCapableBeanFactory.java:1719) at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.getObjectFromFactoryBean(FactoryBeanRegistrySupport.java:113) ... 57 more Caused by: java.lang.IllegalArgumentException: Cannot subclass final class class com.sun.proxy.$Proxy39 at org.springframework.cglib.proxy.Enhancer.generateClass(Enhancer.java:446) at org.springframework.cglib.transform.TransformingClassGenerator.generateClass(TransformingClassGenerator.java:33) at org.springframework.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25) at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:216) at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:377) at org.springframework.cglib.proxy.Enhancer.createClass(Enhancer.java:317) at org.springframework.aop.framework.ObjenesisCglibAopProxy.createProxyClassAndInstance(ObjenesisCglibAopProxy.java:57) at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:202) ... 64 more
分析原因
從報錯信息可以了解說是代理了final修飾的類。可是哪里來的final類? 原來,DAO層使用的是mybatis,可以只寫接口不用寫實現類。而我們項目中就是沒有寫實現類。但是spring也可以對接口進行代理,繼續分析。
Mapper開發規則
- 在mapper.xml中將namespace設置為mapper.java的全限定名
- 將mapper.java接口的方法名和mapper.xml中statement的id保持一致。
- 將mapper.java接口的方法輸入參數類型和mapper.xml中statement的parameterType保持一致
- 將mapper.java接口的方法輸出 結果類型和mapper.xml中statement的resultType保持一致。
注意遵循上邊四點規范!這樣拋棄Dao實現類的寫法: 具有更好的可擴展性,提高了靈活度。
先來說明下mybatis為何可以只寫接口而不寫實現類,通過mybatis源碼分析可知:
mybatis通過JDK的動態代理方式,在啟動加載配置文件時,根據配置mapper的xml去生成Dao的實現。session.getMapper()使用了代理,當調用一次此方法,都會產生一個代理class的instance,看看這個代理class的實現.
1 public class MapperProxy implements InvocationHandler { 2 ... 3 public static <T> T newMapperProxy(Class<T> mapperInterface, SqlSession sqlSession) { 4 ClassLoader classLoader = mapperInterface.getClassLoader(); 5 Class<?>[] interfaces = new Class[]{mapperInterface}; 6 MapperProxy proxy = new MapperProxy(sqlSession); 7 return (T) Proxy.newProxyInstance(classLoader, interfaces, proxy); 8 } 9 10 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 11 if (!OBJECT_METHODS.contains(method.getName())) { 12 final Class<?> declaringInterface = findDeclaringInterface(proxy, method); 13 final MapperMethod mapperMethod = new MapperMethod(declaringInterface, method, sqlSession); 14 final Object result = mapperMethod.execute(args); 15 if (result == null && method.getReturnType().isPrimitive()) { 16 throw new BindingException("Mapper method '" + method.getName() + "' (" + method.getDeclaringClass() + ") attempted to return null from a method with a primitive return type (" + method.getReturnType() + ")."); 17 } 18 return result; 19 } 20 return null; 21 }
這里是用到了JDK的代理Proxy。 newMapperProxy()可以取得實現interfaces 的class的代理類的實例。
當執行interfaces中的方法的時候,會自動執行invoke()方法,其中public Object invoke(Object proxy, Method method, Object[] args)中 method參數就代表你要執行的方法.
MapperMethod類會使用method方法的methodName 和declaringInterface去取 sqlMapxml 取得對應的sql,也就是拿declaringInterface的類全名加上 sql-id..
因此,dao類被多次代理,第二次aop進行代理的時候拿到的是第一次代理后的對象,這個對象是個final形式的,因此報錯。
解決方法:最后我在外層封裝了一個service接口和接口的實現類,將dao注入到該service中,最后對該service實現aop,問題就解決了。
總結
動態代理解決問題的檢查點:
- 需要AOP攔截的類是否是final的,final類不可使用CGLIB來代理。
- 是否在給BEAN配AOP的時候強制使用CGLIB,如果是則可指定proxyTargetClass屬性以讓spring強制代理目標類。
- 類是否被多次代理了,如果類被多次代理過,則第二次進行代理的時候拿到的是第一次代理后的對象,這個對象是個final形式的,所以會出現這個錯誤。
基於第三點要注意,類是否被多次代理不緊緊取決於類是否被配置了多次AOP,如果類實現了某個接口,則還要看類實現的接口是否被aop攔截過。如果類實現了接口且接口也被AOP攔截了,則很可能出現上面的錯誤(是否出錯取決於AOP代理執行的順序)。
spring配置aop需要注意:
1、proxy-target-class屬性值決定是基於接口的還是基於類的代理被創建,啟動對@Aspectj的支持 true為cglib(基於類),false為jdk代理(基於接口),不寫的話默認為false。為true的話,會導致攔截不了mybatis的mapper
<aop:aspectj-autoproxy proxy-target-class="false" />
2、在類沒有實現任何接口,並且沒有默認構造函數的情況下,通過構造函數注入時,目前的Spring是無法實現AOP切面攔截的。 參考通過CGLIB實現AOP的淺析(順便簡單對比了一下JDK的動態代理)