先看一个配置案例:
<!-- config.xml-->
<bean id="targetScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage"
value="test.**.dao,test1.**.dao" />
<property name="sqlSessionFactoryBeanName" value="targetSqlSessionFactory" />
</bean>
<bean id="targetSqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="mydatasource" />
<property name="mapperLocations" value="classpath*:mybatis/*.xml" />
<property name="configuration">
<bean class="org.apache.ibatis.session.Configuration">
<property name="mapUnderscoreToCamelCase" value="true" />
<property name="localCacheScope" value="STATEMENT" />
</bean>
</property>
</bean>
<bean id="mydatasource" factory-bean="myDataSourceFactory"
factory-method="getDataSource" />
在spring框架整合的场景下,mybatis只需要这三个核心配置就可以正确加载,datasource不多说,核心是另外两个,下面对每个配置做下探索
1、SqlSessionFactoryBean - sql映射文件(xml)解析
先整体看下这个类是做什么的再去分析配置项
sqlSessionFactoryBean的本身能力
public class SqlSessionFactoryBean
implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent>
这个类实现了FactoryBean、InitializingBean、ApplicationListner三个接口
-
FactoryBean:说明这个类是个bean工厂,在bean加载的过程中会调用它的getObject方法来获取工厂生产的bean
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
它的getObject方法返回的是SqlSessionFactory对象,当该对象不存在的时候,调用afterPropertiesSet方法。sqlSessionFactory对象是负责管理数据连接和最终执行的,感兴趣可以自行研究,与本篇内容关系不大,这里不多赘述
-
InitializingBean:说明这个类在doCreateBean(create、populate、initialize)的initialize过程中会调用afterPropertiesSet方法做bean的自定义化处理
public void afterPropertiesSet() throws Exception {
notNull(dataSource, "Property 'dataSource' is required");
notNull(sqlSessionFactoryBuilder, "Property 'sqlSessionFactoryBuilder' is required");
state((configuration == null && configLocation == null) || !(configuration != null && configLocation != null),
"Property 'configuration' and 'configLocation' can not specified with together");
this.sqlSessionFactory = buildSqlSessionFactory();
}
看到里面做的动作,核心就是sqlSessionFactory的加载。因此它会在bean初始化的时候去加载自己的工厂产物:sqlSessionFactory,同时为了防止构造未完成,在spring主动加载sqlSessionFactory的时候,调用SqlSessionFactoryBean#getObject方法同样也会再进行一次加载
-
ApplicationListner:说明这个bean具备spring的事件监听能力
public void onApplicationEvent(ApplicationEvent event) {
if (failFast && event instanceof ContextRefreshedEvent) {
// fail-fast -> check all statements are completed
this.sqlSessionFactory.getConfiguration().getMappedStatementNames();
}
}
这个类会监听所有事件,当事件是spring上下文加载的时候通过refresh方法发送的事件 -- ContextRefreshedEvent时,sqlSessionFactory同步会执行getMappedStatementNames()这个方法,这里暂时不继续看
buildSqlSessionFactory方法
该方法实际上就是在解析在XML中的targetSqlSessionFactory这个bean配置的各个属性。
在方法中可以看到所有xml配置项的加载步骤。
首先是configuration和configLocation,二者只解析一个
if (this.configuration != null) {
targetConfiguration = this.configuration;
if (targetConfiguration.getVariables() == null) {
targetConfiguration.setVariables(this.configurationProperties);
} else if (this.configurationProperties != null) {
targetConfiguration.getVariables().putAll(this.configurationProperties);
}
} else if (this.configLocation != null) {
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
targetConfiguration = xmlConfigBuilder.getConfiguration();
使用configLocation说明需要读取专门的xml配置文件做mybatis的配置项,例如:
<bean id="sqlSessionFactoryBean_1" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="configLocation" value="classpath:mybatis/mybatis-config.xml"/>
……
</bean>
mybatis自定义插件解析
if (!isEmpty(this.plugins)) {
Stream.of(this.plugins).forEach(plugin -> {
targetConfiguration.addInterceptor(plugin);
LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
});
}
使用了插件的配置项例如:
<bean id="sqlSessionFactoryBean_1" class="org.mybatis.spring.SqlSessionFactoryBean">
……
<property name="plugins">
<list>
<bean id="filterLargeObjectSqlPlugin" class="test.FilterLargeObjectSqlPlugin"/>
</list>
</property>
</bean>
下面一个核心的配置项是mapperLocations,这个配置项决定了mybatis去哪里找sql映射文件
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
xmlMapperBuilder.parse();
在XmlMapperBuilder#parse方法可以找到mapper中的各个标签的解析,感兴趣可以自行了解,这里大致梳理下解析的最终产物:映射文件对应的class类
在parse方法中,解析完xml后,调用bindMapperForNamespace方法,通过反射构造映射文件对应的class类
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
// ignore, bound type is not required
}
if (boundType != null && !configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
这里可以看出来,在映射文件中配置的namespace起到的作用是通过namespace提供的Type建立xml与Dao之间的映射
跟踪MapperRegistry#addMapper方法,可以看到底层实际上是通过代理的方式,通过预编译sql创造一个映射文件的代理类,每当执行Dao层接口的时候,由代理类执行真正的预编译后的sql及建立数据连接的方法
knownMappers.put(type, new MapperProxyFactory<>(type));
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
预编译的流程在parse方法中,感兴趣可以继续深入了解
这里把type和对应的代理类工厂存到了一个内存map中,有存必然有取,取的地方在MapperRegistry#getMapper中,一路向上找,发现找到的是MapperFactoryBean这个类
关于这个类等会再看
mapperLocations的配置逻辑
private Resource[] mapperLocations;
看到SqlSessionFactoryBean中的mapperLocations变量,类型是Resource[],那么在xml中应该如何去配置它?我们看以下四种配置:
<!-- 配置方案1 -->
<bean id="resourceTest" class="com.huawei.cbc.udrmetricservice.resourcetest.ResourceTest">
<property name="resources" value="classpath*:mybatis/*.xml" />
</bean>
<!-- 配置方案2 -->
<bean id="resourceTest2" class="com.huawei.cbc.udrmetricservice.resourcetest.ResourceTest">
<property name="resources">
<list>
<value>classpath*:mybatis/BatchCompositeEventDao.xml</value>
<value>classpath*:mybatis/BatchEventDao.xml</value>
</list>
</property>
</bean>
<!-- 配置方案3 -->
<bean id="resourceTest3" class="com.huawei.cbc.udrmetricservice.resourcetest.ResourceTest">
<property name="resources" value="classpath*:mybatis/BatchCompositeEventDao.xml,classpath*:mybatis/BatchEventDao.xml" />
</bean>
<!-- 配置方案4 -->
<bean id="resourceTest4" class="com.huawei.cbc.udrmetricservice.resourcetest.ResourceTest">
<property name="resources" value="classpath*:mybatis.BatchCompositeEventDao.xml" />
</bean>
哪一种方案可以拿到正确的映射文件路径?
@Setter
@Getter
public class ResourceTest {
private Resource[] resources;
}
public void test() {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
ResourceTest resourceTest = (ResourceTest) context.getBean("resourceTest");
System.out.println(resourceTest.getResources().length);
ResourceTest resourceTest2 = (ResourceTest) context.getBean("resourceTest2");
System.out.println(resourceTest2.getResources().length);
ResourceTest resourceTest3 = (ResourceTest) context.getBean("resourceTest3");
System.out.println(resourceTest3.getResources().length);
ResourceTest resourceTest4 = (ResourceTest) context.getBean("resourceTest3");
System.out.println(resourceTest4.getResources().length);
}
28
2
0
0
可见第三种配置方案是拿不到对应的映射文件的,当代码执行到mybatis接口dao,就会产生找不到映射报错
为什么会这样?其实这里是mybatis借助了spring提供的资源路径模糊匹配的能力,具体可以了解doCreateBean中的populateBean方法,这里只是简单顺一下这个思路:
在populateBean方法注入属性需要做属性转换,会走到TypeConverterDelegate#convertIfNecessary方法中,这个方法先找自定义的属性转换器,如果不存在,找默认的属性转换器
PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);
……
if (editor == null) {
editor = findDefaultEditor(requiredType);
}
convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor);
自定义转换器就是自己实现PropertyEditor接口的转换器,它也是在refresh流程中被加载到上下文中的
而spring提供了一个默认转换器ResourceArrayPropertyEditor,专门用来转换Resource类型的属性注入,可以通过配置String路径实现Resource属性注入。
public void setAsText(String text) {
String pattern = resolvePath(text).trim();
try {
setValue(this.resourcePatternResolver.getResources(pattern));
}
catch (IOException ex) {
throw new IllegalArgumentException(
"Could not resolve resource location pattern [" + pattern + "]: " + ex.getMessage());
}
}
借助这个转换器做转换时,实际上是通过PathMatchingResourcePatternResolver这个工具来做转换的,代码在getReousrces方法中,转换逻辑并不复杂,实际上就是在解析绝对路径和模糊路径,找到路径下所有的的文件
// 首先判断以classpath*:开头
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
……
// 模糊查找
if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
return findPathMatchingResources(locationPattern);
}
else {
// 按绝对路径查找
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}
……
回看配置项1,实际上是一个模糊路径,配置项2实际上是两个绝对路径,配置项3其实并不是路径,尽管看起来像是两个绝对路径,但这个解析工具的执行流程并不是把一条String做分割符分割得到最终路径的
2、MapperScannerConfigurer - dao文件扫描及接口-sql映射关系建立
看MapperScannerConfigurer之前不得不先看另一个Mybatis提供的配置项:MapperFactoryBean。因为这个MapperFactoryBean的getObject方法可以直接获取到接口type与xml映射文件的代理类之间的映射关系
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
先看MapperFactoryBean的配置案例:
<bean id="userMapper" class="org.mybatis.Spring.mapper.MapperFactoryBean">
<property name="mapperInterface" value="test.mybatis.dao.UserMapper"></property>
<property name="sqlSessionFactory" ref="sqlSessionFactory"></property>
</bean>
可以看到这里配置了一个mapperInterface属性,和一个sqlSessionFactory属性
private Class<T> mapperInterface;
因为mapperInterface属性是一个纯Class类型的对象,说明MapperFactoryBean并不适合配置大批量的DAO层文件
而MapperScannerConfigurer是怎么跟MapperFactoryBean产生关联的呢?
MapperScannerConfigurer的本身能力
public class MapperScannerConfigurer
implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware
MapperScannerConfigurer同样实现了InitializingBean和BeanDefinitionRegistryPostProcessor两个接口
-
InitializingBean:通过初始化操作验证属性注入
public void afterPropertiesSet() throws Exception {
notNull(this.basePackage, "Property 'basePackage' is required");
}
在afterPropertiesSet方法中就只做了一个判断,即basePackage属性不能为空
-
BeanDefinitionRegistryPostProcessor:在上下文refresh的流程中,beanDefinition加载完后,bean开始创建之前,修改beanDfinition的一个入口
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
……
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
可以看出来,在创建bean之前调用这个方法,实际上是为了让spring能扫到DAO文件,并且把DAO文件注册给spring托管
String CONFIG_LOCATION_DELIMITERS = ",; \t\n";
通过这个属性能看出来,basePackage属性支持的分割符包括“,”、“;”、空格符、换行符
public int scan(String... basePackages) {
int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
doScan(basePackages);
// Register annotation config processors, if necessary.
if (this.includeAnnotationConfig) {
AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}
processBeanDefinitionsClassPathMapperScanner#doScan方法中对于包路径的解析和扫描,以及注册到spring的逻辑感兴趣可以自行了解,这里只看其中的一个核心步骤:
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
……
definition.setBeanClass(this.mapperFactoryBeanClass);
将扫到的bd设置了一下BeanClass属性,设置为MapperFactoryBean类型,这里有啥用处呢?可以看出来MapperFactoryBean是个工厂类,那么它的getObject方法就必定会在bean初始化的时候被调用。
其实是在spring加载bean的环节中,doGetBean流程会判断bean已经加载完成了之后,调用getObjectForBeanInstance方法获取真正的实例
beanInstance = getObjectForBeanInstance(sharedInstance, name, beanName, null);
如果创建出来的bean是一个工厂bean,就会调用工厂bean的getObject方法
if (mbd != null) {
mbd.isFactoryBean = true;
}
……
// 这里向下分析,就可以找到getObject方法了
object = getObjectFromFactoryBean(factory, beanName, !synthetic);
而isFactoryBean属性的来源,就是刚刚看到的beanClass属性
protected boolean isFactoryBean(String beanName, RootBeanDefinition mbd) {
Boolean result = mbd.isFactoryBean;
if (result == null) {
// 这里的beanType取的就是beanClass属性
Class<?> beanType = predictBeanType(beanName, mbd, FactoryBean.class);
// 判断beanType属于FactoryBean的实现类
result = (beanType != null && FactoryBean.class.isAssignableFrom(beanType));
mbd.isFactoryBean = result;
}
return result;
}
到这里逻辑其实就闭环了,通过MapperScannerConfigurer提供的扫包能力,将扫到的Dao层接口打上MapperFacotryBean的标记,这样就可以在spring加载bean时使接口与mapper的代理类建立绑定关系。
3、总结
-
SqlSessionFactoryBean配置项实现了mybatis获取数据源连接和mapper映射文件扫描
-
mapperLocations属性配置了mapper映射文件的位置,当出现找不到mapper文件的时候,首先考虑这里的配置是否正确
-
mapperLocations属性不支持用逗号分割的形式配置,只支持一条配置一个路径,但是路径可以是模糊匹配的
-
当集成类似tablepartition这种增加了新的mapper映射文件的组件时,需要注意,组件的mapper映射文件路径是否和项目中配置一样,若不一样,需要新增对应配置
-
扫描完成的mapper映射文件会生成代理类存储在内存中
-
Could not find resource com/zhou/dao/UserMapper.xml
UserMapper is not known to the MapperRegistry.
-
MapperScannerConfigurer配置项实现了mybatis对dao接口文件的扫描,同时建立dao文件和mapper映射文件代理类之间的映射关系
-
basePackage属性配置了dao文件路径,支持用逗号、分号、空格、空行的形式做拆分
-
当出现了无法加载DAO文件对应的bean这类问题,首先考虑是不是这个配置项出了问题
-
当继承像tablepartition这样增加新的dao文件的组件时,需要注意,组件的dao文件目录需要在basePackage中进行配置才能在spring中进行托管
-
当出现了类似dao层的接口与mapper文件没有映射关系的时候,首先考虑dao层接口的全路径与mapper中的namespace是否一致,以及接口方法与id是否一致
-
Invalid bound statement (not found):