一、Spring+Mybatis的典型配置
1.1、pom文件引入依赖
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>${mybatis.version}</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>${mybatis-spring.version}</version>
</dependency>
<!-- mybatis end -->
1.2、配置文件
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 全局映射器启用缓存 -->
<setting name="cacheEnabled" value="true" />
<!-- 查询时,关闭关联对象即时加载以提高性能 -->
<setting name="lazyLoadingEnabled" value="true" />
<!-- 设置关联对象加载的形态,此处为按需加载字段(加载字段由SQL指定),不会加载关联表的所有字段,以提高性能 -->
<setting name="aggressiveLazyLoading" value="true" />
<!-- 对于未知的SQL查询,允许返回不同的结果集以达到通用的效果 -->
<setting name="multipleResultSetsEnabled" value="true" />
<!-- 允许使用列标签代替列名 -->
<setting name="useColumnLabel" value="true" />
<!-- 允许使用自定义的主键值(比如由程序生成的UUID 32位编码作为键值),数据表的PK生成策略将被覆盖 -->
<setting name="useGeneratedKeys" value="true" />
<!-- 给予被嵌套的resultMap以字段-属性的映射支持 -->
<setting name="autoMappingBehavior" value="FULL" />
<!-- 对于批量更新操作缓存SQL以提高性能 -->
<setting name="defaultExecutorType" value="REUSE" />
<!-- 数据库超过25000秒仍未响应则超时 -->
<setting name="defaultStatementTimeout" value="25000" />
</settings>
<!-- 指定别名扫描包路径,mapper文件中可直接用别名替换全路径限定名 -->
<typeAliases>
<!-- typeAlias必须要放在package前 -->
<!--<typeAlias type="com.**.**.wechat.**.User" alias="user"/>-->
<package name="com.**.**.wechat.po"/>
</typeAliases>
<plugins>
<!-- 分页插件,PageHelper实现分页需要在调用dao之前调用PageHelper#startPage()方法 -->
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="dialect" value="mysql" />
<!-- 该参数默认为false,设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用,和startPage中的pageNum效果一样 -->
<property name="offsetAsPageNum" value="true" />
<!-- 该参数默认为false,设置为true时,使用RowBounds分页会进行count查询 -->
<property name="rowBoundsWithCount" value="true" />
</plugin>
</plugins>
</configuration>
applicationContext-mybatis.xml
<?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:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<bean id="statFilter" class="com.alibaba.druid.filter.logging.Slf4jLogFilter">
<property name="statementExecutableSqlLogEnable" value="false"/>
<property name="dataSourceLogEnabled" value="false"/>
</bean>
<bean id="logFilter" class="com.alibaba.druid.filter.stat.StatFilter">
<property name="slowSqlMillis" value="50"/>
<property name="logSlowSql" value="false"/>
<property name="mergeSql" value="true"/>
</bean>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
init-method="init" destroy-method="close">
<property name="driverClassName" value="${datasource.driverClassName}"/>
<property name="url" value="${datasource.url}"/>
<property name="username" value="${datasource.username}"/>
<property name="password" value="${datasource.password}"/>
<property name="maxActive" value="${datasource.maxPoolSize}"/>
<property name="initialSize" value="${datasource.initialPoolSize}"/>
<property name="maxWait" value="${datasource.maxWait}"/>
<property name="minIdle" value="${datasource.minIdle}"/>
<property name="timeBetweenEvictionRunsMillis" value="60000"/>
<property name="minEvictableIdleTimeMillis" value="300000"/>
<property name="validationQuery" value="SELECT 'x'"/>
<property name="testWhileIdle" value="true"/>
<property name="testOnBorrow" value="false"/>
<property name="testOnReturn" value="false"/>
<property name="poolPreparedStatements" value="true"/>
<property name="proxyFilters">
<list>
<ref bean="statFilter"/>
<ref bean="logFilter"/>
</list>
</property>
</bean>
<bean id="dbcpDataSource" class="org.apache.commons.dbcp.BasicDataSource"
destroy-method="close">
<property name="driverClassName" value="${datasource.driverClassName}"/>
<property name="url" value="${datasource.url}"/>
<property name="username" value="${datasource.username}"/>
<property name="password" value="${datasource.password}"/>
<property name="maxActive" value="${datasource.maxPoolSize}"/>
<property name="maxIdle" value="${datasource.maxIdle}"/>
<property name="minIdle" value="${datasource.minIdle}"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!-- mybatis配置文件路径 -->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!-- mapper.xml文件扫描路径 -->
<property name="mapperLocations" value="classpath:mybatis/*.xml"/>
<!-- 指定扫描单个类型别名 -->
<property name="typeAliases" value="com.**.**.wechat.pojo.User" />
<!-- 指定需要扫描的类型别名包路径,这样在mapper.xml其它需要使用实体类(Entity)的时候就可以直接使用别名而不是全限定类名 -->
<property name="typeAliasesPackage" value="com.**.**.wechat.pojo"/>
<!-- 指定需要扫描类型的父类,如果不传默认为Object.class -->
<property name="typeAliasesSuperType" value="com.**.**.wechat.pojo.BaseDomain"/>
</bean>
<!-- mapper.java文件扫描路径 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.**.**.wechat.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<!-- 事务管理 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 支持事务注解 -->
<tx:annotation-driven/>
</beans>
二、加载过程
根据上面的配置可以看出,加载过程主要可以分为两步,一是通过MapperScannerConfigurer进行mapper包路径的扫描并生成BeanDefinition,二是SqlSessionFactoryBean读取xml配置构建SqlSessionFactory。
2.1、扫描mapper包路径
2.1.1、MapperScannerConfigurer
MapperScannerConfigurer实现了BeanDefinitionRegistryPostProcessor接口,它的作用是通过扫描器ClassPathMapperScanner自动扫描basePackage路径下的mapper接口,依次生成BeanDefinition并注册,basePackage可以是以多种分隔符(,; \t\n)串联起来的多个包路径。MapperScannerConfigurer中postProcessBeanDefinitionRegistry方法的实现如下:
MapperScannerConfigurer实现的postProcessBeanDefinitionRegistry接口
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
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);
// 注册Filter
scanner.registerFilters();
// 扫描包路径并注册BeanDefinition
scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
2.1.2、ClassPathMapperScanner
ClassPathMapperScanner继承了ClassPathBeanDefinitionScanner,并扩展了它的doScan()接口,主要方法有如下几个:
ClassPathMapperScanner的主要方法
// 注册过滤器,用于判断哪些类需要被注册,哪些需要被排除
public void registerFilters() {
boolean acceptAllInterfaces = true;
if (this.annotationClass != null) {
this.addIncludeFilter(new AnnotationTypeFilter(this.annotationClass));
acceptAllInterfaces = false;
}
if (this.markerInterface != null) {
this.addIncludeFilter(new AssignableTypeFilter(this.markerInterface) {
protected boolean matchClassName(String className) {
return false;
}
});
acceptAllInterfaces = false;
}
// 默认所有的类都需要扫描和注册
if (acceptAllInterfaces) {
this.addIncludeFilter((metadataReader, metadataReaderFactory) -> {
return true;
});
}
// 排除package-info.java文件
this.addExcludeFilter((metadataReader, metadataReaderFactory) -> {
String className = metadataReader.getClassMetadata().getClassName();
return className.endsWith("package-info");
});
}
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
// 调用父类ClassPathBeanDefinitionScanner的doScan方法获得BeanDefinitionHolder集合
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
} else {
this.processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
Iterator var3 = beanDefinitions.iterator();
while(var3.hasNext()) {
BeanDefinitionHolder holder = (BeanDefinitionHolder)var3.next();
GenericBeanDefinition definition = (GenericBeanDefinition)holder.getBeanDefinition();
String beanClassName = definition.getBeanClassName();
definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName);
// 设置MapperFactoryBean,用于获取实例
definition.setBeanClass(this.mapperFactoryBean.getClass());
definition.getPropertyValues().add("addToConfig", this.addToConfig);
boolean explicitFactoryUsed = false;sqlSessionFactoryBeanName
// 如果sqlSessionFactoryBeanName的名字不为空,则在Spring容器中查询
if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {
definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));
explicitFactoryUsed = true;
// 如果sqlSessionFactory不为null,则设置definition的sqlSessionFactory
} else if (this.sqlSessionFactory != null) {
definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);
explicitFactoryUsed = true;
}
if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {
definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));
explicitFactoryUsed = true;
} else if (this.sqlSessionTemplate != null) {
definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);
explicitFactoryUsed = true;
}
if (!explicitFactoryUsed) {
definition.setAutowireMode(2);
}
}
}
2.2、构建SqlSessionFactory
2.2.1、SqlSessionFactoryBean
SqlSessionFactoryBean实现了FactoryBean接口,泛型参数为SqlSessionFactory,说明它的最终目的是构建并生成SqlSessionFactory,下面看下为了得到SqlSessionFactory,内部都做了哪些事情。
SqlSessionFactoryBean构建SqlSessionFactory的过程
protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
XMLConfigBuilder xmlConfigBuilder = null;
Configuration targetConfiguration;
// configuration可通过配置传入,如果没有且configLocation不为空,则会借助config构建器XMLConfigBuilder新生成一个
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);
}
// 这里是我们传入的mybatis-config.xml路径
} else if (this.configLocation != null) {
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), (String)null, this.configurationProperties);
targetConfiguration = xmlConfigBuilder.getConfiguration();
} else {
targetConfiguration = new Configuration();
Optional.ofNullable(this.configurationProperties).ifPresent(targetConfiguration::setVariables);
}
// 下面的objectFactory、objectWrapperFactory和vfs都是既可以支持配置传入,也可以采用默认实现
Optional.ofNullable(this.objectFactory).ifPresent(targetConfiguration::setObjectFactory);
Optional.ofNullable(this.objectWrapperFactory).ifPresent(targetConfiguration::setObjectWrapperFactory);
Optional.ofNullable(this.vfs).ifPresent(targetConfiguration::setVfsImpl);
// 如果传入的类型别名包路径不为空,则通过TypeAliasRegistry.registerAliases(String, Class<?>)方法保存类型与别名的映射到Map中
// 该Map在构造方法中已经添加了常见的基本类型(int)和封装类型(Integer)的映射
// 别名默认是Class的SimpleName,用户也可以通过在Class上增加注解@Alias指定
String[] typeHandlersPackageArray;
if (StringUtils.hasLength(this.typeAliasesPackage)) {
typeHandlersPackageArray = StringUtils.tokenizeToStringArray(this.typeAliasesPackage, ",; \t\n"); // 默认使用",; \t\n"进行split
Stream.of(typeHandlersPackageArray).forEach((packageToScan) -> { // 如果未指定超类,默认使用Object.class
targetConfiguration.getTypeAliasRegistry().registerAliases(packageToScan, this.typeAliasesSuperType == null ? Object.class : this.typeAliasesSuperType);
});
}
// 指定单个类型别名
if (!ObjectUtils.isEmpty(this.typeAliases)) {
Stream.of(this.typeAliases).forEach((typeAlias) -> {
targetConfiguration.getTypeAliasRegistry().registerAlias(typeAlias);
});
}
// 如果配置了插件,这里添加到Configuration的interceptorChain中;同时也可以在mybatis-config.xml文件中添加插件进行拦截
if (!ObjectUtils.isEmpty(this.plugins)) {
Stream.of(this.plugins).forEach((plugin) -> {
targetConfiguration.addInterceptor(plugin);
});
}
// 如果类型处理器包路径不为空,则会扫描包下实现了TypeHandler的类并注册到TypeHandlerRegistry的Map中;TypeHandler的作用是负责java数据类型和jdbc数据类型之间的映射和转换
if (StringUtils.hasLength(this.typeHandlersPackage)) {
typeHandlersPackageArray = StringUtils.tokenizeToStringArray(this.typeHandlersPackage, ",; \t\n");
Stream.of(typeHandlersPackageArray).forEach((packageToScan) -> {
targetConfiguration.getTypeHandlerRegistry().register(packageToScan);
});
}
// 手动指定会覆盖默认映射
if (!ObjectUtils.isEmpty(this.typeHandlers)) {
Stream.of(this.typeHandlers).forEach((typeHandler) -> {
targetConfiguration.getTypeHandlerRegistry().register(typeHandler);
});
}
if (this.databaseIdProvider != null) {
try {
targetConfiguration.setDatabaseId(this.databaseIdProvider.getDatabaseId(this.dataSource));
} catch (SQLException var23) {
throw new NestedIOException("Failed getting a databaseId", var23);
}
}
Optional.ofNullable(this.cache).ifPresent(targetConfiguration::addCache);
if (xmlConfigBuilder != null) {
try {
// 解析mybatis-config.xml文件入口
xmlConfigBuilder.parse();
} catch (Exception var21) {
throw new NestedIOException("Failed to parse config resource: " + this.configLocation, var21);
} finally {
ErrorContext.instance().reset();
}
}
targetConfiguration.setEnvironment(new Environment(this.environment, (TransactionFactory)(this.transactionFactory == null ? new SpringManagedTransactionFactory() : this.transactionFactory), this.dataSource));
// 扫描mapperLocations包下的mapper.xml文件并依次解析
if (!ObjectUtils.isEmpty(this.mapperLocations)) {
Resource[] var24 = this.mapperLocations;
int var4 = var24.length;
for(int var5 = 0; var5 < var4; ++var5) {
Resource mapperLocation = var24[var5];
if (mapperLocation != null) {
try {
// 解析mapper.xml文件入口
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(), targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception var19) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", var19);
} finally {
ErrorContext.instance().reset();
}
}
}
}
// 通过Configuration创建sqlSessionFactory的默认实现DefaultSqlSessionFactory并返回
// 除了通过xml之外,SqlSessionFactoryBuilder还提供了通过java代码和任意输入流(InputStream)的方式构建
return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}
2.2.2、XMLConfigBuilder解析config配置
下面一起看下config.xml的解析过程
XMLConfigBuilder解析过程
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 解析从根节点configuration开始
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
// 读取properties配置
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
// 读取类型别名配置,该参数也可以在SqlSessionFactoryBean配置中进行设置,注意这两个地方的读取顺序,先SqlSessionFactoryBean中后config中,后面会覆盖前面的
typeAliasesElement(root.evalNode("typeAliases"));
// 同样可以设置拦截插件
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 读取并设置setting配置,包括是否开启全局换存、是否延迟加载、主键生成策略、超时时间等
settingsElement(settings);
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 同SqlSessionFactoryBean中的typeHandlers配置
typeHandlerElement(root.evalNode("typeHandlers"));
// 在config中也可以设置mapper文件扫描路径,一般推荐配置在SqlSessionFactoryBean中,两者实际上最终都是通过XMLMapperBuilder进行解析和构建
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
2.2.3、XMLMapperBuilder解析mapper.xml文件
XMLMapperBuilder解析过程
public void parse() {
// 一个mapper文件只加载一次,resource为全路径文件名
if (!configuration.isResourceLoaded(resource)) {
// 解析mapper文件入口
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
// 为mapper绑定namespace,namespace是<mapper>标签中的namespace值
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
private void configurationElement(XNode context) {
try {
// 每个mapper文件都需要指定命名空间
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
// 对其他命名空间缓存配置的引用
cacheRefElement(context.evalNode("cache-ref"));
// 对当前命名空间的缓存配置
cacheElement(context.evalNode("cache"));
// 引用外部parameterMap的已经被废弃的方法
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
// 解析resultMap,用来描述如何从数据库结果集中来加载对象
resultMapElements(context.evalNodes("/mapper/resultMap"));
// 定义可以被重用的sql代码段
sqlElement(context.evalNodes("/mapper/sql"));
// 解析可执行语句结点
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
上面的L-37是解析sql的主要过程,方法内部是通过XMLStatementBuilder#parseStatementNode()构造每个执行节点对应的MappedStatement,并将其添加到Configuration的Map<String, MappedStatement> mappedStatements属性中,内部比较重要的步骤是sqlSource的获取,这部分逻辑可以参考《mybatis源码分析—sql动态解析》篇。