問題背景:
項目使用spring,springmvc框架,后邊需操作關系數據庫,選擇了mybatis + durid,集成mybatis后,項目一直啟動失敗。錯誤的原因是dataSource初始化的時候讀取不到配置文件中的值,初始化失敗。
日志如下:
ERROR - aba.druid.pool.DruidDataSource - {dataSource-1} init error java.sql.SQLException: unkow jdbc driver : ${jdbc.url} at com.alibaba.druid.util.JdbcUtils.getDriverClassName(JdbcUtils.java:436) at com.alibaba.druid.pool.DruidDataSource.init(DruidDataSource.java:643) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:606)
問題原因:
初步查看日志原因還是比較明確的,數據源初始化的時候,配置文件還沒有讀取完畢。不過問題來了spring管理的其他的bean初始化的時候也用到了配置文件中的值,然而啟動的時候都沒用問題,為什么到這會出現這個問題呢?
繼續查看spring配置:
<!-- 配置sqlSessionFactory --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="typeAliasesPackage" value="xxxxxxxxx" /> <property name="typeAliasesSuperType" value="xxxxxxx" /> <property name="mapperLocations" value="xxxxxxxxxxx" /> <property name="configLocation" value="xxxxxxxxxxxxx" /> </bean> <!-- 掃描basePackage下的接口 --> <bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" /> <property name="basePackage" value="xxxxxx" /> </bean>
發現是mybatis的 MapperScannerConfigurer 引用了dataSource,查看MapperScannerConfigurer 的源碼,發現 MapperScannerConfigurer 自己實現了spring的BeanDefinitionRegistryPostProcessor,此時猜想是不是因為它自定義了bean初始化的借口,然后在初始化的時候它游離在spring的初始化進程之外,導致在它初始化的時候PropertyPlaceholderConfigurer還沒有初始化完畢,因此讀取不到配置文件中得值。
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware { ...................... }
繼續查看 MapperScannerConfigurer的代碼,發現了有意思東西:
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware { ........................ private boolean processPropertyPlaceHolders; ........................ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { if (this.processPropertyPlaceHolders) { processPropertyPlaceHolders(); } ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); scanner.setAddToConfig(this.addToConfig); scanner.setAnnotationClass(this.annotationClass); scanner.setMarkerInterface(this.markerInterface); scanner.setSqlSessionFactory(this.sqlSessionFactory); scanner.setSqlSessionTemplate(this.sqlSessionTemplate); scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName); scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName); scanner.setResourceLoader(this.applicationContext); scanner.setBeanNameGenerator(this.nameGenerator); scanner.registerFilters(); scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)); } /* * BeanDefinitionRegistries are called early in application startup, before * BeanFactoryPostProcessors. This means that PropertyResourceConfigurers will not have been * loaded and any property substitution of this class' properties will fail. To avoid this, find * any PropertyResourceConfigurers defined in the context and run them on this class' bean * definition. Then update the values. */ private void processPropertyPlaceHolders() { Map<String, PropertyResourceConfigurer> prcs = applicationContext.getBeansOfType(PropertyResourceConfigurer.class); if (!prcs.isEmpty() && applicationContext instanceof GenericApplicationContext) { BeanDefinition mapperScannerBean = ((GenericApplicationContext) applicationContext) .getBeanFactory().getBeanDefinition(beanName); // PropertyResourceConfigurer does not expose any methods to explicitly perform // property placeholder substitution. Instead, create a BeanFactory that just // contains this mapper scanner and post process the factory. DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); factory.registerBeanDefinition(beanName, mapperScannerBean); for (PropertyResourceConfigurer prc : prcs.values()) { prc.postProcessBeanFactory(factory); } PropertyValues values = mapperScannerBean.getPropertyValues(); this.basePackage = updatePropertyValue("basePackage", values); this.sqlSessionFactoryBeanName = updatePropertyValue("sqlSessionFactoryBeanName", values); this.sqlSessionTemplateBeanName = updatePropertyValue("sqlSessionTemplateBeanName", values); } } ................................. }
看到注釋證實了上面的猜測,確實是因為 MapperScannerConfigurer 初始化過早的原因:
在BeanFactoryPostProcessor之前,BeanDefinitionRegistries在應用程序啟動的早期被調用。 這意味着PropertyResourceConfigurers將不會被加載,並且這個類的屬性的任何屬性替換都將失敗。 為了避免這種情況發現上下文中定義的任何PropertyResourceConfigurers,並在這個類的beanz定義上運行它們。 然后更新這些值。
解決方法:
查看源碼的時候發現這三個屬性比較有意思,似乎是與問題相關的:
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware { ...................... private SqlSessionFactory sqlSessionFactory; ....................... private String sqlSessionFactoryBeanName; ...................... private boolean processPropertyPlaceHolders; ....................
@Deprecated
public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) {
this.sqlSessionFactory = sqlSessionFactory;
}
....................
}
發現 MapperScannerConfigurer 注入 sqlSessionFactory 的時候有兩種選擇一種是在spring配置的時候直接注入sqlSessionFactory對象,另一種是sqlSessionFactoryBeanName通過名字來注入bean。
這兩種的區別在於直接注入對象要求在初始化的時候對象必須已經初始化完畢,而通過名字注入則在初始化的時候並不要求對象初始化完畢。並且通過對象方式的注入已經被標記為過時了。
到這里發現似乎在配置 MapperScannerConfigurer 的時候,加上 processPropertyPlaceHolders 屬性似乎就能解決問題。當時心里很高興,立即配置后上試了下,結果很悲劇,問題依然存在。
當時心里挺奇怪的,然后就繼續看代碼,心里有了一個猜測是不是因為我們 SqlSessionFactory bean的名字也是 sqlSessionFactory 跟MapperScanner中屬性的名字一致,導致初始化的時候依然注入了對象。
抱着試試看的心態,修改sqlSessionFactory 為mySqlSessionFactory 結果問題竟然解決了。
總結:
1.配置MapperScannerConfigurer的時候, 使用sqlSessionFactoryBeanName方式注入SqlSessionFactory
2.配置SqlSessionFactory 的時候,beanId一定不能為sqlSessionFactory ,(這里是因為項目配置原因,default-autowire="byName" ):
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" default-autowire="byName">