Cc同学记得以前开发项目主要是以SSM或是SSH为主。Spring由于其繁琐的配置,一度被人认为“配置地狱”,各种XML、Annotation配置,让人眼花缭乱,而且如果出错了也很难找出原因。Cc同学记得刚自学Spring的时候就怕配置出错,因为一旦出错就要各种搜索。而Spring Boot的问世,让开发者看到了曙光,SpringBoot让创建基于Spring的应用更加快捷简易。 大部分Spring Boot Application只要一些极简的配置,即可“一键运行”。
这篇文章主要通过源码剖析Mybatis在Spring Boot环境下是如何进行自动配置的以及Mapper具体的工作原理。
1.准备工作-搭建一个极简的测试项目
通过实际运行项目来一步一步解读源码。
新建Spring Boot项目,引入 mybatis-spring-boot-starter和数据库驱动依赖;
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
数据库新建测试表,项目内生成对应的实体及Mapper接口和对应的Mapper.xml。这里就不多赘述了。
2.找到入口–MybatisAutoConfiguration
根据Spring Boot的启动原理,我们只需要找到mybatis-spring-boot-autoconfigure下的META-INF/spring.factories文件,即可可以看到自动配置类指向了MybatisAutoConfiguration,话不多说直接跟进。
简单看一下MybatisAutoConfiguration里面的几个重要的点。省略了方法体。
//构建SqlSessionFactory
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {}
//构建SqlSessionTemplate
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {}
//注入MapperScan
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {}
//扫描Mapper接口
@org.springframework.context.annotation.Configuration
@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {}
3.Mapper的前世今生
//通过@Import(AutoConfiguredMapperScannerRegistrar.class)来加载AutoConfiguredMapperScannerRegistrar
@org.springframework.context.annotation.Configuration
@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
//省略代码部分。。。。。。
}
//实现了ImportBeanDefinitionRegistrar,所以直接看registerBeanDefinitions方法内部
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {
private BeanFactory beanFactory;
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//需要扫描的包
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
//省略代码部分。。。。。。
//动态注入bean,将MapperScannerConfigurer注入进IOC容器
registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
}
@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
}
打开MapperScannerConfigurer,可以看到它实现了BeanDefinitionRegistryPostProcessor接口,根据Spring的机制,直接查看它的postProcessBeanDefinitionRegistry方法。
//当该类被IOC容器加载时,会调用该方法
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
if (this.processPropertyPlaceHolders) {
processPropertyPlaceHolders();
}
//创建扫描器
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
//省略代码部分。。。。。。
//扫描所配置包下所有的带有@Mapper注解的接口
scanner.scan(
StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
}
当我继续步入方法内部,实际上进入了ClassPathMapperScanner的doScan方法
然后调用processBeanDefinitions()方法,直接进入该方法内部
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
AbstractBeanDefinition definition;
BeanDefinitionRegistry registry = getRegistry();
//循环刚刚扫描的mapper集合
for (BeanDefinitionHolder holder : beanDefinitions) {
definition = (AbstractBeanDefinition) holder.getBeanDefinition();
//这里是最重要的一步,我们刚刚扫描出来的mapper BeanDefinition实际上是mapper接口本身
//这里将Class的类型改为了MapperFactoryBean.class
definition.setBeanClass(this.mapperFactoryBeanClass);
}
}
到此一步Mapper的初步工作就完成了,将我们定义好的Mapper接口扫描并注入IOC容器,且将原本的Class类型修改为了MapperFactoryBean.class类型,为什么要将Mapper接口修改为MapperFactoryBean?接下来实际运行看一下为什么以及mapper的工作原理
4.Mapper的工作原理
将断点打到我们的Mapper方法上,直接运行测试项目
可以看到,我们Spring给我注入的是MapperProxy类型,上面我们说过Mybatis将Mapper接口类型改为MapperFactoryBean,而这是拿到的确实MapperProxy这是为啥呢?我们去看一下MapperFactoryBean的源码。
可以看到实现了FactoryBean接口,而FactoryBean注入的时候是通过getObject去获取对象。
最后实际调用链调用的是MapperProxyFactory中的newInstance方法,实际上是通过JDK动态代理生成了Mapper接口的代理类,这里是巧妙的地方,就是JDK动态代理接口是必须得,而实现类是可有可无的,Mybatis就是对用户定义的Mapper的接口生成代理类MapperProxy之后,实际的逻辑都是通过MapperProxy内部实现的,没有用到实现类。
继续刚才的步骤继续debug,步入代码内部
调用cachedInvoker()去缓存中获取MapperMethodInvoker,从中获取MapperMethod,这个也是MyBatis的一级缓存,在内存中存储使用过的SQL语句
从而执行MapperMethod的execute方法
MapperMethod是Mybatis非常核心的一个组件
首先看一下它的构造方法
private final SqlCommand command;
private final MethodSignature method;
public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new SqlCommand(config, mapperInterface, method);
this.method = new MethodSignature(config, mapperInterface, method);
}
SqlCommand是MapperMethod的一个静态内部类封装了SQL类型 insert update delete select
MethodSignature也是一个静态内部类封装了方法的参数信息 返回类型信息等
接下来看一下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;
}
继续debug我们的代码
可以看到command中的name是咱们的测试方法名 getTest,type是SELECT
继续下一步
我们讲一下这两行代码
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
可以看到args是我们传进来的参数数组
这里主要工作就是组织好参数对应的值及顺序
接下来就是执行SqlTemplate的方法
进入selectOne方法内部
这里的this.sqlSessionProxy实际上SqlSession的代理类,这个可以在SqlSessionTemplate的构造方法中可以找到答案
所以继续跟进代码,进入SqlSessionTemplate的内部类的SqlSessionInterceptor中也就没有疑问了吧
实际上调用了DefaultSqlSession的selectOne方法
进入DefaultSqlSession的selectOne方法内部
继续进入selectList方法
第一步首先获取MappedStatement对象,MappedStatement对象对应Mapper.xml配置文件中的一个select/update/insert/delete节点,描述的就是一条SQL语句的信息。
简单看一下它的内部属性,继续执行
Executor是Mybatis执行器的顶层接口,它定义了对数据库的操作,这里我们看到Executor用的是它其中的一个实现类CachingExecutor,它为 Executor 对象增加了二级缓存的相关功能,它封装了一个用于执行数据库操作的 Executor 对象,以及一个用于管理缓存的 TransactionalCacheManager 对象。
进入CachingExecutor的query方法
可以看到第一步是得到待执行的SQL语句信息,以及二级缓存需要的key值,进入query方法
首先判断是否有缓存,第一次执行肯定是没有缓存,继续跟进
这里实际上是进入了BaseExecutor的queryFromDatabase方法,这里有一部分的缓存处理
继而进入doQuery方法,实际调用的是SimpleExcutor的doQuery方法
这里获取了StatementHandler,然后调用它的query方法
StatementHandler是四大组件中最重要的一个对象,负责操作 Statement 对象与数据库进行交流,在工作时还会使用 ParameterHandler 和 ResultSetHandler 对参数进行映射,对结果进行实体类的绑定。
调用了PreparedStatement的execute方法,到达这一步大家应该彻底懂了吧,Mybatis对JDBC的操作的进一步封装,最后一步就是对结果的映射,这里就不细讲了,小伙伴有兴趣可以自己去探索。