1. 程式人生 > 程式設計 >業務類無法被AOP代理問題

業務類無法被AOP代理問題

背景

在專案裡面引入了shiro框架,然而在使用者登入的時候始終會出現資料庫訪問異常,異常資訊如下:

org.springframework.jdbc.BadSqlGrammarException: 
### Error querying database.  Cause: java.sql.SQLSyntaxErrorException: Table 'ems.sys_user_manager' doesn't exist
### The error may exist in com/greenet/platform/common/mapper/UserMapper.java (best guess)
### The error may involve defaultParameterMap ### The error occurred while setting parameters ### SQL: SELECT account,password,grouptype,account_tmtype,email,notes FROM sys_user_manager WHERE account = ? ### Cause: java.sql.SQLSyntaxErrorException: Table 'ems.sys_user_manager' doesn't exist ; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: Table 'ems.sys_user_manager'
doesn't exist at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:234) ~[spring-jdbc-5.1.5.RELEASE.jar:5.1.5.RELEASE] at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72) ~[spring-jdbc-5.1.5.RELEASE.jar:5.1.5.RELEASE] at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:73) ~[mybatis-spring-1.3.2.jar:1.3.2] at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446) ~[mybatis-spring-1.3.2.jar:1.3.2] at com.sun.proxy.$Proxy63.selectOne(Unknown Source) ~[na:na] at org.mybatis.spring.SqlSessionTemplate.selectOne(SqlSessionTemplate.java:166) ~[mybatis-spring-1.3.2.jar:1.3.2] at com.baomidou.mybatisplus.core.override.PageMapperMethod.execute(PageMapperMethod.java:101) ~[mybatis-plus-core-3.0.7.1.jar:3.0.7.1] at com.baomidou.mybatisplus.core.override.PageMapperProxy.invoke(PageMapperProxy.java:64) ~[mybatis-plus-core-3.0.7.1.jar:3.0.7.1] at com.sun.proxy.$Proxy64.selectOne(Unknown Source) ~[na:na] at com.greenet.platform.common.service.impl.UserServiceImpl.findUserByUsername(UserServiceImpl.java:48) ~[classes/:na] at com.greenet.platform.config.UserRealm.doGetAuthenticationInfo(UserRealm.java:47) ~[classes/:na]
複製程式碼

從異常日誌來看是找錯了資料庫,因為專案中使用了多資料來源技術,通過在service層加上@DS(Database.DATABASE_CA_SYSTEM)註解來標示該service需要使用哪個資料來源。這個異常說明註解並沒有實現它該有的作用,導致資料來源沒有切換成功。

多資料來源使用的是baomidou開發的框架,相關依賴如下,它的核心是通過AOP技術來切換每個service需要使用的資料來源,具體實現這裡先不討論。

<dependency>    
    <groupId>com.baomidou</groupId>    
    <artifactId>dynamic-datasource-spring-boot-starter</artifactId> 		    
</dependency>
複製程式碼

上面的異常說明userService這個bean並沒有被代理,那麼問題出來哪裡呢?

問題定位

springboot的啟動日誌中發現一個值得注意的點:

Warn: Could not find @TableId in Class: com.greenet.platform.common.entity.User. 2019-11-14 15:50:42.487 INFO 17752 --- [ main] trationDelegateBeanPostProcessorChecker : Bean 'userMapper' of type [org.mybatis.spring.mapper.MapperFactoryBean] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2019-11-14 15:50:42.489  INFO 17752 --- [           main] trationDelegateBeanPostProcessorChecker : Bean 'userMapper' of type [com.sun.proxy.Proxy64] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2019-11-14 15:50:42.490  INFO 17752 --- [           main] trationDelegateBeanPostProcessorChecker : Bean 'userService' of type [com.greenet.platform.common.service.impl.UserServiceImpl] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2019-11-14 15:50:42.555 INFO 17752 --- [ main] trationDelegate$BeanPostProcessorChecker : Bean 'cacheManager' of type [org.apache.shiro.cache.ehcache.EhCacheManager] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying) 2019-11-14 15:50:42.556 INFO 17752 --- [ main] o.a.shiro.cache.ehcache.EhCacheManager : Using existing EHCache named [passwordRetryCache

重點在這句話:

trationDelegate$BeanPostProcessorChecker : Bean 'userService' of type [com.greenet.platform.common.service.impl.UserServiceImpl] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)

這句話意思是userService由於不符合某種條件,導致不會被自動代理,那麼這個條件是什麼呢?

檢視BeanPostProcessorChecker的原始碼,列印日誌的地方如下:

@Overridepublic Object postProcessAfterInitialization(Object bean,String beanName) {   
    if (!(bean instanceof BeanPostProcessor) && !isInfrastructureBean(beanName) &&         this.beanFactory.getBeanPostProcessorCount() < this.beanPostProcessorTargetCount) {      
        if (logger.isInfoEnabled()) {    
            logger.info("Bean '" + beanName + "' of type [" + bean.getClass().getName() +               "] is not eligible for getting processed by all BeanPostProcessors " +               "(for example: not eligible for auto-proxying)");      }   }   
    return bean;
}
複製程式碼

日誌打印出來的原因是this.beanFactory.getBeanPostProcessorCount() < this.beanPostProcessorTargetCount返回了true,也就是說此時註冊到beanFactory的後置處理器數量是少於總的後置處理器數量的,也就是說這個時候有其他後置處理器還沒準備好,userService就已經被例項化了;

既然這個地方說了userService不會被代理,那麼這個bean又是在什麼時候被spring例項化的呢。想搞清楚這個問題很簡單,可以在AbstractBeanFactory.doGetBean方法裡面打一個條件斷點,然後看呼叫棧。

得到如下呼叫棧:

"main@1" prio=5 tid=0x1 nid=NA runnable
  java.lang.Thread.State: RUNNABLE
	  at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:242)
	  at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	  at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:277)
	  at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1247)
	  at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1167)
	  at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:857)
	  at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:760)
	  at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:509)
	  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1305)
	  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1144)
	  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555)
	  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
	  at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
	  at org.springframework.beans.factory.support.AbstractBeanFactory$$Lambda$121.1862994526.getObject(Unknown Source:-1)
	  at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
	  - locked <0x12b7> (a java.util.concurrent.ConcurrentHashMap)
	  at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
	  at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	  at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:277)
	  at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1247)
	  at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1167)
	  at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:857)
	  at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:760)
	  at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:509)
	  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1305)
	  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1144)
	  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getSingletonFactoryBeanForTypeCheck(AbstractAutowireCapableBeanFactory.java:991)
	  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getTypeForFactoryBean(AbstractAutowireCapableBeanFactory.java:865)
	  at org.springframework.beans.factory.support.AbstractBeanFactory.isTypeMatch(AbstractBeanFactory.java:574)
	  at org.springframework.beans.factory.support.DefaultListableBeanFactory.doGetBeanNamesForType(DefaultListableBeanFactory.java:514)
	  at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForType(DefaultListableBeanFactory.java:477)
	  at org.springframework.beans.factory.BeanFactoryUtils.beanNamesForTypeIncludingAncestors(BeanFactoryUtils.java:227)
	  at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1411)
	  at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1210)
	  at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1167)
	  at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:857)
	  at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:760)
	  at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:509)
	  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1305)
	  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1144)
	  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555)
	  at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515)
	  at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
	  at org.springframework.beans.factory.support.AbstractBeanFactory$$Lambda$121.1862994526.getObject(Unknown Source:-1)
	  at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
	  at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
	  at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:204)
	  at org.springframework.context.support.PostProcessorRegistrationDelegate.registerBeanPostProcessors(PostProcessorRegistrationDelegate.java:228)
	  at org.springframework.context.support.AbstractApplicationContext.registerBeanPostProcessors(AbstractApplicationContext.java:721)
	  at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:534)
	  - locked <0x12ea> (a java.lang.Object)
	  at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:142)
	  at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775)
	  at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
	  at org.springframework.boot.SpringApplication.run(SpringApplication.java:316)
	  at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260)
	  at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248)
	  at com.greenet.platform.Application.main(Application.java:24)

複製程式碼

從呼叫棧可以看到兩個重點資訊:

  • 這個棧裡面出現了三次AbstractBeanFactory.doGetBean,說明userService是在例項化其他bean的時候由於依賴注入而例項化的;
  • 棧的入口是 AbstractApplicationContext.registerBeanPostProcessors

這兩點說明userService不是在正常的AbstractApplicationContext.finishBeanFactoryInitialization階段通過例項化,而是在註冊後置處理器的時候就例項化了。

為什麼會提前例項化呢?

MethodValidationPostProcessor

從上面打的斷點回頭追溯到registerBeanPostProcessors的地方,可以看到此時註冊的後置處理器MethodValidationPostProcessor,除錯截圖如下:

是對應的程式碼如下:

	for (String ppName : orderedPostProcessorNames) {
			BeanPostProcessor pp = beanFactory.getBean(ppName,BeanPostProcessor.class);
			orderedPostProcessors.add(pp);
			if (pp instanceof MergedBeanDefinitionPostProcessor) {
				internalPostProcessors.add(pp);
			}
		}
複製程式碼

也就是說userServiceMethodValidationPostProcessor這個後置處理器註冊前的例項化的過程中,因為某種原因被提前例項化了。

那麼到底是為啥例項化MethodValidationPostProcessor的時候需要例項化userService

通過一波除錯發現,問題出來我的shiro配置裡面

  @Bean("securityManager")
    public SecurityManager securityManager(UserRealm userRealm,SessionManager sessionManager,EhCacheManager cacheManager) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(userRealm);
        securityManager.setSessionManager(sessionManager);
        securityManager.setRememberMeManager(null);
        securityManager.setCacheManager(cacheManager);
        return securityManager;
    }


    /**
     * shiro許可權驗證
     *
     * @param securityManager 安全管理器
     * @return 安全管理factorybean
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
        shiroFilter.setSecurityManager(securityManager);
        shiroFilter.setLoginUrl("/login");

        Map<String,String> filterMap = new LinkedHashMap<>();

        filterMap.put("/static/**","anon");
        filterMap.put("/","anon");
        filterMap.put("/login/getVerifyCodeImage","anon");

        filterMap.put("/**","authc");
        shiroFilter.setFilterChainDefinitionMap(filterMap);

        return shiroFilter;
    }
複製程式碼

罪魁禍首是這個ShiroFilterFactoryBean,這個東西的定義如下:

public class ShiroFilterFactoryBean implements FactoryBean,BeanPostProcessor {
複製程式碼

它實現了FactoryBean,BeanPostProcessor這兩個特殊的介面,所有的FactoryBean會在後置處理器MethodValidationPostProcessor例項化過程中被例項化,為啥呢,這跟這個後置處理器的定義的地方有關係,這個後置處理器在ValidationAutoConfiguration類中定義,具體方法程式碼如下:

	@Bean
	@ConditionalOnMissingBean
	public static MethodValidationPostProcessor methodValidationPostProcessor(
			Environment environment,@Lazy Validator validator) {
		MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
		boolean proxyTargetClass = environment
				.getProperty("spring.aop.proxy-target-class",Boolean.class,true);
		processor.setProxyTargetClass(proxyTargetClass);
		processor.setValidator(validator);
		return processor;
	}
複製程式碼

想要例項化這個類,需要知道這個方法的引數,問題就出在這個Environment裡面,為了找到型別為Environment的引數,spring會遍歷整個beanDefinitionNames,然後去找到符合Environment這個型別的bean,找的過程中發現有些beanfactoryBean,那不好意思,需要把這個factoryBean給例項化出來,再看上面ShiroFilterFactoryBean定義的方法可以看到,它依賴了securityManager,而securityManager又依賴了userRealm,而userRealm又依賴了userService:

@Component
public class UserRealm extends AuthorizingRealm {

    private static final String ACCOUNT_TYPE_WEB = "2";
    private static final String ACCOUNT_TYPE_ALL = "255";

    @Autowired
    UserService userService;
複製程式碼

就這樣一步步把userService給例項化了,這個時候還沒到後置處理器處理bean的時候,那負責做AOP代理的後置處理器自然無法再去處理userService了。

解決方案

那麼如何去解決這個問題呢,思路是不要在factoryBean裡面去注入業務類。一種比較簡單的辦法是去注入ApplicationContext,然後通過getBean方法拿到業務的service

  private UserService getUserService() {
        return (UserService) applicationContext.getBean("userService");
    }
複製程式碼