这篇文章将从源码层面,阐述以下问题:
- MyBatis如何集成到SpringBoot中的
- 为什么Mapper的方法不需要实现就能执行sql
MyBatis的执行流程
先大致讲讲MyBatis的执行流程。
- 应用启动后,MyBatis读取配置文件,包括MyBatis的配置文件(比如数据源、连接池)、Mapper配置文件,它会把这些配置转换成单例模式的org.apache.ibatis.session.Configuration对象。其配置内容包括如下:
- properties全局参数
- settings设置
- typeAliases别名
- typeHandler类型处理器
- ObjectFactory对象
- plugin插件
- environment环境
- DatabaseIdProvider数据库标识
- Mapper映射器
- 根据Configuration对象SqlSessionFactory
- SqlSessionFactory会创建SqlSession对象
- SqlSession通过代理生成Mapper接口代理对象
- Mapper接口代理对象调用方法,执行Sql语句
MyBatis的整个执行流程就是这5步,下面将来解析这5步的逻辑。
构建SqlSessionFactory
直接上源码
在mybatis-spring-boot-autoconfigure包中的MyBatisAutoConfiguration类中
// 我将DataSource参数理解连接池对象,比如在我的项目中引入了Hikra,
// 那么这个dataSource就是Hikara相关的代理类
// 通过ide的debugger可以看到它的具体类是HikariDataSource$$EnhancerBySpringCGLIB$$4d16d247@8509,是通过CGLIB代理的
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
// 这个Vfs属性,个人理解就是SpringBoot提供的,能够用来帮助读取yaml配置文件中的属性
factory.setVfs(SpringBootVFS.class);
// 这个属性我没用过,估计是设置MyBatis的外部加载配置
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
// 这个方法就是设置已读取的MyBatis配置到factory对象
this.applyConfiguration(factory);
// 用得少,我觉得不用关注
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
// 设置MyBatis的插件
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
// 设置数据库的类别,比如MySql、oracle
if (this.databaseIdProvider != null) {
factory.setDatabaseIdProvider(this.databaseIdProvider);
}
// 用得少,我觉得不用关注
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
// 用得少,我觉得不用关注
if (this.properties.getTypeAliasesSuperType() != null) {
factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
}
// 用得少,我觉得不用关注
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
// 用得少,我觉得不用关注
if (!ObjectUtils.isEmpty(this.typeHandlers)) {
factory.setTypeHandlers(this.typeHandlers);
}
// 设置所有的mapper路径
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
// 用得少,我觉得不用关注
Set<String> factoryPropertyNames = (Set)Stream.of((new BeanWrapperImpl(SqlSessionFactoryBean.class)).getPropertyDescriptors()).map(FeatureDescriptor::getName).collect(Collectors.toSet());
Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
factory.setScriptingLanguageDrivers(this.languageDrivers);
if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
defaultLanguageDriver = this.languageDrivers[0].getClass();
}
}
if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
}
// 构建SqlSessionFactory
return factory.getObject();
}
在sqlSessionFactory方法中,需要关注三点:
- 设置MyBatis插件
- 设置mapper路径
- 构建SqlSessionFactory,也就是factory.getObject() 前两点非常简单,就是读取数据然后再设置,重点是第三点
下面看看getObject()方法
public SqlSessionFactory getObject() throws Exception {
if (this.sqlSessionFactory == null) {
afterPropertiesSet();
}
return this.sqlSessionFactory;
}
应用第一次启动的时候,sqlSessionFactory肯定是null,那么会进入afterPropertiesSet()方法
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();
}
然后就是buildSqlSessionFactory()方法 在这个方法里面,才是真正的加载这些内容来生成Configuration对象,然后创建SqlSessionFactory
- properties全局参数
- settings设置
- typeAliases别名
- typeHandler类型处理器
- ObjectFactory对象
- plugin插件
- environment环境
- DatabaseIdProvider数据库标识
- Mapper映射器 下面是代码片段:
protected SqlSessionFactory buildSqlSessionFactory() throws Exception {
final Configuration targetConfiguration;
XMLConfigBuilder xmlConfigBuilder = null;
if (this.configuration != null) {
// 省略代码
} else if (this.configLocation != null) {
xmlConfigBuilder = new XMLConfigBuilder(this.configLocation.getInputStream(), null, this.configurationProperties);
targetConfiguration = xmlConfigBuilder.getConfiguration();
} else {
// 省略代码
if (!isEmpty(this.plugins)) {
Stream.of(this.plugins).forEach(plugin -> {
targetConfiguration.addInterceptor(plugin);
LOGGER.debug(() -> "Registered plugin: '" + plugin + "'");
});
}
// 省略代码
if (this.mapperLocations != null) {
if (this.mapperLocations.length == 0) {
LOGGER.warn(() -> "Property 'mapperLocations' was specified but matching resources are not found.");
} else {
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
throw new NestedIOException("Failed to parse mapping resource: '" + mapperLocation + "'", e);
} finally {
ErrorContext.instance().reset();
}
LOGGER.debug(() -> "Parsed mapper file: '" + mapperLocation + "'");
}
}
} else {
LOGGER.debug(() -> "Property 'mapperLocations' was not specified.");
}
return this.sqlSessionFactoryBuilder.build(targetConfiguration);
}
只展示出了两部分,一部分是配置插件(大部分情况就是保存自定义的插件信息),另一部分是设置mapper(就是mapper.xml中的select、update、resultMap等等配置内容)
注意xmlMapperBuilder.parse();这个方法的调用,里面有一步configurationElement(parser.evalNode("/mapper"));这一步会解析mapper对应的xml配置,将每一个 SELECT、UPDATE、DELETE操作转换成对应的MappedStatement,而MappedStatement这个对象会在执行Mapper方法的时候用到。
配置插件的方法层层往里跟进,其最后的结果就是下面这样
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
InterceptorChain对象实例会作为Configuration对象属性,然后plugin就通过addInterceptor添加
最后就是调用this.sqlSessionFactoryBuilder.build(targetConfiguration)生成SqlSessionFactory
构建SqlSession
在SpringBoot中,SqlSession通过SqlSessionTepmlate管理(方便实现事务)。所以前面初始化完SqlSessionFactoryBuild的下一步就是初始化SqlSessionTemplate
看代码
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
if (executorType != null) {
return new SqlSessionTemplate(sqlSessionFactory, executorType);
} else {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
这里根据ExecutorType执行不同的构造方法。我一般都没有指定ExecutorType,所以初始化的时候使用系统提供的默认值。 跟着构造方法一直跟进,最后的构造方法内容如下
public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType,
PersistenceExceptionTranslator exceptionTranslator) {
notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required");
notNull(executorType, "Property 'executorType' is required");
this.sqlSessionFactory = sqlSessionFactory;
this.executorType = executorType;
this.exceptionTranslator = exceptionTranslator;
this.sqlSessionProxy = (SqlSession) newProxyInstance(SqlSessionFactory.class.getClassLoader(),
new Class[] { SqlSession.class }, new SqlSessionInterceptor());
}
- sqlSessionFactory: 就是上文初始化的SqlSessionFactory
- exceptionTranslator: 官方解释是说将MyBatis抛出的异常转换成运行时异常,该属性可以为null
- sqlSessionProxy: SqlSession的代理对象。有两个作用:1、spring的事务处理 2、捕获MyBatis的异常,转换成运行时异常
Mapper 接口的代理和注入
在使用MyBatis的时候,我们会添加@MapperScan或者@Mapper注解。这两个注解的作用就是将Mapper注入到Spring的Bean容器中。因为所有的Mapper都是接口,所以实际注入容器之前,mybatis-spring会把这些的真实类设置为MapperFactoryBean。后面在@Service层的类中自动注入mapper就会调用MapperFactoryBean的getObject(),获得Mapper的代理对象类。
比如注入UserMappper,会调用下面这个方法
@Override
public T getObject() throws Exception {
return getSqlSession().getMapper(this.mapperInterface);
}
Mapper的执行流程
下面通过代码来探寻Mapper的执行流程,这里在控制器中注入Mapper,然后调用Mapper的接口
// TestController.java
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private AppServiceMapper appServiceMapper;
@PostMapping
public void test() {
appServiceMapper.selectById(1L);
}
}
// AppServiceMapper.xmlselectById
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="AppServiceMapper">
<select id="selectById" resultType="io.choerodon.devops.infra.dto.AppServiceDTO">
SELECT * FROM devops_app_service das where das.id=#{id}
</select>
</mapper>
上面的截图可以看到,appServiceMapper是由MapperProxy类进行代理的。
从截图的断点开始,通过step into开始debug
现在进入MapperProxy的invoke方法,最终执行到第85行
cachedInvoker方法会根据调用的Mapper方法找到对应的PlainMethodInvoker对象,其包含Mapper对应的xml配置内容,比如这里的method是selectById,那么返回的PlainMethodInvoker里面包含内容如下:
然后调用PlainMethodInvoker的invoke方法,其实也就是调用MapperMethod的execute方法。
下面看看MapperMethod的execute方法内容
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
整体结构就是switch/case,可以发现里面有四个case:INSERT、UPDATE、DELETE、SELECT,而刚才调用的方法是SELECT,所以会进入SELECT,最后执行到这里
接下来就执行sqlSession的selectOne。这里的sqlSession本质是前面生成的sqlSessionTemplate,所以执行的是sqlSessionTemplate的selectOne方法。
上面的sqlSessionProxy对象是由JDK代理生成的
这里可以看到声明逻辑,所以下一步是执行SqlSessionInterceptor的invoke方法
最后执行到这里
先获取真正的SqlSession,即MyBatis的DefaultSqlSession,然后调用DefaultSqlSession的selectOne方法
往里跟进,到达这一层
接下来进入executor.query方法
到达这里,开始执行MyBatis的所有拦截器逻辑
所有拦截器执行完毕后,开始执行真正的query方法
然后查出所有结果并返回