Cannot find class: ${jdbc.driver}——配置了sqlSessionFactoryBeanName也报错之问题分析
MyBatis中一个sqlSessionFactory代表一个数据源,那么配置多个数据源只需要注入多个sqlSessionFactory即可。
首先需要说明的是,用mybatis-spring-1.1.0貌似无法配置多个数据源(这里说的不对,应该是无法在配置数据源中使用${..}占位符),这里大概折腾了我一整天的时间。后来才想到可能是版本问题,于是换了最新的1.2.2版,问题就迎刃而解了。
下面记录一下分析这个问题的过程:
首先我在Spring的配置文件中配置了org.springframework.beans.factory.config.PropertyPlaceholderConfigurer,这个Bean中的作为一个BeanFactoryPostProcessor会在载入所有BeanDefinition后运行,然后利用指定的properties文件来替换BeanDefinition中定义的${...}占位符,Spring的配置文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
<bean id="pmsDatasource" class="org.apache.ibatis.datasource.pooled.PooledDataSource">
<property name="driver" value="${pms.driver}" />
<property name="url" value="${pms.url}" />
<property name="username" value="${pms.username}" />
<property name="password" value="${pms.password}" />
</bean>
<bean id="pmsSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="pmsDatasource" />
</bean>
<bean id="pmsMapperConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.westsoft.pms.dao" />
<property name="sqlSessionFactoryBeanName" value="pmsSqlSessionFactory"></property>
</bean>
<bean id="kftDatasource" class="org.apache.ibatis.datasource.pooled.PooledDataSource">
<property name="driver" value="${kft.driver}" />
<property name="url" value="${kft.url}" />
<property name="username" value="${kft.username}" />
<property name="password" value="${kft.password}" />
</bean>
<bean id="kftSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="kftDatasource" />
</bean>
<bean id="kftMapperConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.westsoft.kft.dao" />
<property name="sqlSessionFactoryBeanName" value="kftSqlSessionFactory"></property>
</bean>
<bean id="propertyPlaceholderConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>classpath:/META-INF/config/kft-ds.properties</value>
<value>classpath:/META-INF/config/pms-ds.properties</value>
</list>
</property>
</bean>
</beans>
但是启动容器后,却报错:Cannot find class: ${kft.driver}
因为记得几个月前也弄过这个,但是没成功,后来我就改成不读properties文件,而是直接写在xml中了,算是一种逃避吧
不过这次,我不打算放过它。问了度娘,上面铺天盖地的都是说不要注入MapperScannerConfigurer的sqlSessionFactory属性,而应该使用sqlSessionFactoryBeanName。可问题是我这里已经使用sqlSessionFactoryBeanName,显然不是这个问题。可是度娘上只有这么一个答案,真是略坑啊~难道没人用1.0.0的版本?好吧,那么我做第一人,若以后有人碰到诸如此类的问题,就不用像我搞这么久了~
虽然度娘上没给出解决答案,但是分析的还是有道理的:说是因为MapperScannerConfigurer初始化的时候同时也初始化了SqlSessionFactoryBean,并且由于MapperScannerConfigurer要先于PropertyPlaceholderConfigurer初始化,那么此时对于datasource中配置的占位符无法被替换,所以也就导致出现了上面的错误。
根据上面的思路,跟踪MapperScannerConfigurer的源码看看:
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
processPropertyPlaceHolders();
Scanner scanner = new Scanner(beanDefinitionRegistry);
scanner.setResourceLoader(this.applicationContext);
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
发现MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口,这类Bean会在BeanDefinition被全部载入后,BeanPostProcessor前初始化。并且在初始化时,会执行postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry)方法。
其中后面几行我们看的明白,是自动扫描Mapper的。而第一行执行了processPropertyPlaceHolders()方法,看了代码注释,这个方法就是设计出来防止MapperScannerConfigurer先于PropertyPlaceholderConfigurer初始化而无法转换${..}的。但是为什么没起作用呢?我们来看看源码:
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);
}
}
首先这个方法不管从名字或者意义上来看,我们都不难才想到他是用来得到所有定义的PropetyResourceConfigurer类。
其次,注意到上面的条件了吗?这里需要applicationContext是GenericApplicationContext才起作用,而我们这里的applicationContext的实现类是XmlWebApplicationContext,所以这一步完全不起作用(不对,应该说还起了副作用,因为上面的报错就是和这个方法有关)可以找到这个方法最终其实是交给DefaultListableBeanFactory的getBeansOfType(...)来实现的:
public <T> Map<String, T> getBeansOfType(Class<T> type, boolean includeNonSingletons, boolean allowEagerInit)
throws BeansException {
String[] beanNames = getBeanNamesForType(type, includeNonSingletons, allowEagerInit);
Map<String, T> result = new LinkedHashMap<String, T>(beanNames.length);
for (String beanName : beanNames) {
try {
result.put(beanName, getBean(beanName, type));
}
catch (BeanCreationException ex) {
Throwable rootCause = ex.getMostSpecificCause();
if (rootCause instanceof BeanCurrentlyInCreationException) {
BeanCreationException bce = (BeanCreationException) rootCause;
if (isCurrentlyInCreation(bce.getBeanName())) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Ignoring match to currently created bean '" + beanName + "': " +
ex.getMessage());
}
onSuppressedException(ex);
// Ignore: indicates a circular reference when autowiring constructors.
// We want to find matches other than the currently created bean itself.
continue;
}
}
throw ex;
}
}
return result;
}
这里先去寻找相应type的beanName,继续跟踪getBeanNameForType(...):
public String[] getBeanNamesForType(Class<?> type, boolean includeNonSingletons, boolean allowEagerInit) {
if (type == null || !allowEagerInit) {
return this.doGetBeanNamesForType(type, includeNonSingletons, allowEagerInit);
}
Map<Class<?>, String[]> cache = includeNonSingletons ?
this.nonSingletonBeanNamesByType : this.singletonBeanNamesByType;
String[] resolvedBeanNames = cache.get(type);
if (resolvedBeanNames != null) {
return resolvedBeanNames;
}
resolvedBeanNames = this.doGetBeanNamesForType(type, includeNonSingletons, allowEagerInit);
cache.put(type, resolvedBeanNames);
return resolvedBeanNames;
}
后两个boolean参数在这里都是true,且在cache中没有该bean,继续跟踪doGetBeanNamesForType(...)
private String[] doGetBeanNamesForType(Class<?> type, boolean includeNonSingletons, boolean allowEagerInit) {
List<String> result = new ArrayList<String>();
// Check all bean definitions.
String[] beanDefinitionNames = getBeanDefinitionNames();
for (String beanName : beanDefinitionNames) {
// Only consider bean as eligible if the bean name
// is not defined as alias for some other bean.
if (!isAlias(beanName)) {
try {
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
// Only check bean definition if it is complete.
if (!mbd.isAbstract() && (allowEagerInit ||
((mbd.hasBeanClass() || !mbd.isLazyInit() || this.allowEagerClassLoading)) &&
!requiresEagerInitForType(mbd.getFactoryBeanName()))) {
// In case of FactoryBean, match object created by FactoryBean.
boolean isFactoryBean = isFactoryBean(beanName, mbd);
boolean matchFound = (allowEagerInit || !isFactoryBean || containsSingleton(beanName)) &&
(includeNonSingletons || isSingleton(beanName)) && isTypeMatch(beanName, type);
if (!matchFound && isFactoryBean) {
// In case of FactoryBean, try to match FactoryBean instance itself next.
beanName = FACTORY_BEAN_PREFIX + beanName;
matchFound = (includeNonSingletons || mbd.isSingleton()) && isTypeMatch(beanName, type);
}
if (matchFound) {
result.add(beanName);
}
}
}
catch (CannotLoadBeanClassException ex) {
if (allowEagerInit) {
throw ex;
}
// Probably contains a placeholder: let's ignore it for type matching purposes.
if (this.logger.isDebugEnabled()) {
this.logger.debug("Ignoring bean class loading failure for bean '" + beanName + "'", ex);
}
onSuppressedException(ex);
}
catch (BeanDefinitionStoreException ex) {
if (allowEagerInit) {
throw ex;
}
// Probably contains a placeholder: let's ignore it for type matching purposes.
if (this.logger.isDebugEnabled()) {
this.logger.debug("Ignoring unresolvable metadata in bean definition '" + beanName + "'", ex);
}
onSuppressedException(ex);
}
}
}
// Check singletons too, to catch manually registered singletons.
String[] singletonNames = getSingletonNames();
for (String beanName : singletonNames) {
// Only check if manually registered.
if (!containsBeanDefinition(beanName)) {
// In case of FactoryBean, match object created by FactoryBean.
if (isFactoryBean(beanName)) {
if ((includeNonSingletons || isSingleton(beanName)) && isTypeMatch(beanName, type)) {
result.add(beanName);
// Match found for this bean: do not match FactoryBean itself anymore.
continue;
}
// In case of FactoryBean, try to match FactoryBean itself next.
beanName = FACTORY_BEAN_PREFIX + beanName;
}
// Match raw bean instance (might be raw FactoryBean).
if (isTypeMatch(beanName, type)) {
result.add(beanName);
}
}
}
return StringUtils.toStringArray(result);
}
可以看到要得到 指定类型的类,其实最后是通过遍历所有的beanDefinition来的。其中判断type是否一致是用isTypeMatch(...)
public boolean isTypeMatch(String name, Class<?> targetType) throws NoSuchBeanDefinitionException {
String beanName = transformedBeanName(name);
Class<?> typeToMatch = (targetType != null ? targetType : Object.class);
// Check manually registered singletons.
Object beanInstance = getSingleton(beanName, false);
if (beanInstance != null) {
<pre name="code" class="java"> <span style="font-family: Arial, Helvetica, sans-serif;">//省略部分代码.....</span> else {<span style="font-family: Arial, Helvetica, sans-serif;">//省略部分代码.....</span>// Check bean class whether we‘re dealing with a FactoryBean.if (FactoryBean.class.isAssignableFrom(beanClass)) {if (!BeanFactoryUtils.isFactoryDereference(name)) {// If it‘s a FactoryBean, we want to look at what it creates, not the factory class.Class<?> type = getTypeForFactoryBean(beanName, mbd);return (type != null && typeToMatch.isAssignableFrom(type));}else {return typeToMatch.isAssignableFrom(beanClass);}}else {return !BeanFactoryUtils.isFactoryDereference(name) &&typeToMatch.isAssignableFrom(beanClass);}}}
在我模拟的时候发现在mapper类进来的时候会造成sqlSessionFactory的初始化,这是为什么呢?
由于mapper被MyBatis代理了,mapper定义的时候是用下面这样的形式:
<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean"> <property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserMapper" /> <property name="sqlSessionFactory" ref="sqlSessionFactory" /> </bean>
所以当要获取userMapper的类型时,由于判断其是FactoryBean,且不是Dereference(不是以‘&‘开头去取FactoryBean的类型),就去执行getTypeForFactoryBean(beanName, mbd)这个方法:
protected Class<?> getTypeForFactoryBean(String beanName, RootBeanDefinition mbd) {
if (!mbd.isSingleton()) {
return null;
}
try {
FactoryBean<?> factoryBean = doGetBean(FACTORY_BEAN_PREFIX + beanName, FactoryBean.class, null, true);
return getTypeForFactoryBean(factoryBean);
}
catch (BeanCreationException ex) {
// Can only happen when getting a FactoryBean.
if (logger.isDebugEnabled()) {
logger.debug("Ignoring bean creation exception on FactoryBean type check: " + ex);
}
onSuppressedException(ex);
return null;
}
}
继续跟进doGetBean(...)方法查看:
protected <T> T doGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
throws BeansException {
<pre name="code" class="java"> <span style="font-family: Arial, Helvetica, sans-serif;">//省略部分代码.....</span> }else { //省略部分代码.....// Create bean instance.if (mbd.isSingleton()) {// 总算找到了,这里是会去创建sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {public Object getObject() throws BeansException
{try {return createBean(beanName, mbd, args);}catch (BeansException ex) {// Explicitly remove instance from singleton cache: It might have been put there// eagerly by the creation process, to allow for circular reference resolution.// Also remove any beans
that received a temporary reference to the bean.destroySingleton(beanName);throw ex;}}});bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);}//省略部分代码.....}<span style="font-family: Arial, Helvetica, sans-serif;">//省略部分代码.....</span>return (T) bean;}
所以关键原因是因为要得到FactoryBean的类型(其实是得到创建的Object的类型),那么先要去初始化它再通过getObjectType方法来得到其类型。于是就造成了sqlSessionFactory提前初始化了!另外,分析过程应该是倒推的,而不是正推~
而看了下mybatis-spring-1.2.2的源码,发现MapperScannerConfigurer初始化中的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));
}
并且可以看到,它提供了更多配置项用于自动发现Mapper。