问题背景:
项目使用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">