MyBatis 是一款优秀的持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生信息,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。我使用Mybatis也有好几年了,对于Mybatis配置和使用也是熟门熟路。尤其在Spring Boot出现后,Mybatis的配置和使用也更加简单。但对于Mybatis的原理一直没去了解,Spring Boot在启动时,Mybatis做了哪些事情。在调用Mapper层接口后,底层到底是怎么调用SQL然后操作数据库的。本文就来聊一聊Mybatis的工作原理
概述
通过Spring Boot使用第三方框架时,配置和使用都变得异常简单。这得益于它的默认大于配置的思想,只要少量的配置就能快速的集成第三方框架,Mybatis同样如此,本文是以Mybatis-1.3.2为例。Maven依赖如下
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
使用 mybatis-spring-boot-starter 依赖后,只需要在 yml 中配置xml的扫描路径就可以使用mybatis了。那么这个包的源码中纠结做了哪些事情。打开这个包的源代码,会发现这包中没有代码,只有一个pom.xml
打开这个pom.xml,就能看到Mybatis的全部依赖都在这个pom中了,源码如下
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot</artifactId>
<version>1.3.2</version>
</parent>
<artifactId>mybatis-spring-boot-starter</artifactId>
<name>mybatis-spring-boot-starter</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</dependency>
</dependencies>
</project>
其中重要的依赖就是 mybatis-spring-boot-autoconfigure 包,这个包中就有Mybatis自动注入的源码实现。下面就来看看这个包的源码吧
配置
在 mybatis-spring-boot-autoconfigure 包中有两个重要的类,分别是 MybatisAutoConfiguration 和 MybatisProperties 。其中 MybatisProperties 的作用很简单就是加载 application.yml 中有关 Mybatis的属性配置,源码如下:
@ConfigurationProperties(
prefix = "mybatis"
)
public class MybatisProperties {
public static final String MYBATIS_PREFIX = "mybatis";
private String configLocation;
private String[] mapperLocations;
private String typeAliasesPackage;
private String typeHandlersPackage;
private boolean checkConfigLocation = false;
private ExecutorType executorType;
private Properties configurationProperties;
@NestedConfigurationProperty
private Configuration configuration;
//
}
这里只贴出了属性,省略了方法。其中configLocation就是配置xml文件的扫描路径,从这个类的私有属性也可以看出在 application.yml 可以配置哪些属性。接下来看一下另一个核心类的实现,MybatisAutoConfiguration。这个类中做了很多事情,主要是在Spring Boot启动时对Mybatis的初始化。下面是这个类的部分源码
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnBean({DataSource.class})
@EnableConfigurationProperties({MybatisProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class})
public class MybatisAutoConfiguration {
//.....
}
这里省略了类中的全部代码,先来看一下这个类在注入前做了什么事情。其中 Configuration 注解不用多说了吧。然后就是ConditionalOnClass注解。这个注解很少见,下面总结了一些类似注解的作用,这些注解在平常使用几乎很少,但在Spring Boot的源码中却很常见。
注解 | 作用 |
---|---|
@ConditionalOnBean | 仅当前上下文中存在配置的对象Bean时,才会加载当前类 |
@ConditionalOnMissingBean | 与前一个注解作用相反,仅当不存在配置的对象时,才会加载当前类 |
@ConditionalOnClass | 仅当在 classpath 中存在指定类,才创建某个Bean |
@ConditionalOnExpression | 当表达式为true的时候,才会实例化一个Bean |
@AutoConfigureAfter | 在加载配置的类之后再加载当前类 |
@Import | 功能类似XML配置的,用来导入配置类,可以导入带有@Configuration注解的配置类或实现了ImportSelector/ImportBeanDefinitionRegistrar,或者导入普通的POJO(Spring会将其注册成Spring Bean,导入POJO需要使用Spring 4.2以上) |
到这里可以看出在MybatisAutoConfiguration加载之前,会先加载 DataSource,DataSourceAutoConfiguration等类。
Mapper加载
前面说了那么多都是铺垫,到了加载MybatisAutoConfiguration类才真正开始。在MybatisAutoConfiguration类中,最为重要的方法就是加载Mapper的方法,源码如下
@Configuration
@Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
@ConditionalOnMissingBean({MapperFactoryBean.class})
public static class MapperScannerRegistrarNotFoundConfiguration {
public MapperScannerRegistrarNotFoundConfiguration() {
}
@PostConstruct
public void afterPropertiesSet() {
MybatisAutoConfiguration.logger.debug("No {} found.", MapperFactoryBean.class.getName());
}
}
在这个方法上有个@Import注解,表示在这个方法加载之前会先导入AutoConfiguredMapperScannerRegistrar实例。而AutoConfiguredMapperScannerRegistrar是MybatisAutoConfiguration的一个静态内部类,并且实现了ImportBeanDefinitionRegistrar接口。ImportBeanDefinitionRegistrar的作用是手动注入bean到容器中,再通过@Import注解导入实现了ImportBeanDefinitionRegistrar接口的类,并会执行registerBeanDefinitions方法。下面是Mapper注册类的源码实现
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar, ResourceLoaderAware {
private BeanFactory beanFactory;
private ResourceLoader resourceLoader;
public AutoConfiguredMapperScannerRegistrar() {
}
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
MybatisAutoConfiguration.logger.debug("Searching for mappers annotated with @Mapper");
// 扫描所有注解了@Mapper的接口。
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
try {
if (this.resourceLoader != null) {
scanner.setResourceLoader(this.resourceLoader);
}
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (MybatisAutoConfiguration.logger.isDebugEnabled()) {
Iterator var5 = packages.iterator();
while(var5.hasNext()) {
String pkg = (String)var5.next();
MybatisAutoConfiguration.logger.debug("Using auto-configuration base package '{}'", pkg);
}
}
scanner.setAnnotationClass(Mapper.class);
scanner.registerFilters();
scanner.doScan(StringUtils.toStringArray(packages));
} catch (IllegalStateException var7) {
MybatisAutoConfiguration.logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.", var7);
}
}
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
这个方法中核心代码在于第30行的scanner.doScan(StringUtils.toStringArray(packages));。这行代码就是扫描真正Mapper类,并为每个Mappper类生成了代理类,继续看doScan方法。
public Set<BeanDefinitionHolder> doScan(String... basePackages) {
Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);
if (beanDefinitions.isEmpty()) {
this.logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");
} else {
this.processBeanDefinitions(beanDefinitions);
}
return beanDefinitions;
}
这个方法的第一行的super.doScan(basePackages);就是获取Mapper接口了,了解Spring初始化Bean过程的人可能都知道,Spring首先会将需要初始化的所有class先通过BeanDefinitionRegistry进行注册,并且将该Bean的一些属性信息(如scope、className、beanName等)保存至BeanDefinitionHolder中;Mybatis这里首先会调用Spring中的ClassPathBeanDefinitionScanner.doScan方法,将所有Mapper接口的class注册至BeanDefinitionHolder实例中,然后返回一个Set集合,其它包含了所有搜索到的Mapper class BeanDefinitionHolder对象。接下来就是一个非空判断,然后就会进入processBeanDefinitions方法。
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
Iterator var3 = beanDefinitions.iterator();
while(var3.hasNext()) {
BeanDefinitionHolder holder = (BeanDefinitionHolder)var3.next();
GenericBeanDefinition definition = (GenericBeanDefinition)holder.getBeanDefinition();
if (this.logger.isDebugEnabled()) {
this.logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + definition.getBeanClassName() + "' mapperInterface");
}
definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());
definition.setBeanClass(this.mapperFactoryBean.getClass());
definition.getPropertyValues().add("addToConfig", this.addToConfig);
// ...
}
}
此方法比较冗长,源码–。其中核心的代码是definition.setBeanClass(this.mapperFactoryBean.getClass());当看MapperFactoryBean的classname就知道它是一个专门用于创建 Mapper 实例Bean的工厂。这个工厂作用可就大了。例如当在以Service层中,需要注入 UserMapper接口实例时,由于mybatis给所有的Mapper 实例注册都是一个MapperFactory的工厂,所以产生UserMapper实际上是 MapperFactoryBean来进行创建的。接下来看看 MapperFactoryBean的处理过程。
MapperFactoryBean
一个FactoryBean,负责创建对应映射接口的实现类对象,这个实现类负责完成映射接口的方法和XML定义的SQL语句的映射关系。Mybatis通过SqlSession接口执行SQL语句,所以MapperFactoryBean会在初始化时通过持有的SqlSessionFactory对象创建一个SqlSessionTemplate(它实现了SqlSession)对象。这个SqlSessionTemplate是mybatis-spring的核心,它比常规的SqlSession赋予了更多的功能。在MapperFactoryBean类中是通过 getObject 方法获取Mapper实例,源码如下
public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
private Class<T> mapperInterface;
public MapperFactoryBean(Class<T> mapperInterface) {
this.mapperInterface = mapperInterface;
}
protected void checkDaoConfig() {
super.checkDaoConfig();
Assert.notNull(this.mapperInterface, "Property 'mapperInterface' is required");
Configuration configuration = this.getSqlSession().getConfiguration();
if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) {
try {
configuration.addMapper(this.mapperInterface);
} catch (Exception var6) {
this.logger.error("Error while adding the mapper '" + this.mapperInterface + "' to configuration.", var6);
throw new IllegalArgumentException(var6);
} finally {
ErrorContext.instance().reset();
}
}
}
public T getObject() throws Exception {
return this.getSqlSession().getMapper(this.mapperInterface);
}
}
其中 mapperInterface 代表的就是Mapper接口的类。在getObject方法中 this.getSqlSession()方法返回SqlSession接口,而在此类中是SqlSessionTemplate类,继续看SqlSessionTemplate类中的getMapper方法
public <T> T getMapper(Class<T> type) {
return this.getConfiguration().getMapper(type, this);
}
继续刨根问底,看这个方法中的getMapper方法
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
return this.mapperRegistry.getMapper(type, sqlSession);
}
其中 mapperRegistry保存着当前所有的 mapperInterface class。那么它在什么时候将 mapperinterface class 保存进入的呢?其实就是在上面的 MapperFactoryBean类中checkDaoConfig方法通过 configuration.addMapper(this.mapperInterface) 添加进入的。再继续看getMapper方法的实现。
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory)this.knownMappers.get(type);
if (mapperProxyFactory == null) {
throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
} else {
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception var5) {
throw new BindingException("Error getting mapper instance. Cause: " + var5, var5);
}
}
}
转了这么多getMapper方法,终于到了最核心的部分。可以看到这个方法最终返回的是一个代理对象。核心方法是mapperProxyFactory.newInstance(sqlSession)。继续看newInstance方法。
public T newInstance(SqlSession sqlSession) {
MapperProxy<T> mapperProxy = new MapperProxy(sqlSession, this.mapperInterface, this.methodCache);
return this.newInstance(mapperProxy);
}
至此,基本Mybatis已经完成了Mapper实例的整个创建过程,也就是你在具体使用 UserMapper.getUser 时,它实际上调用的是 MapperProxy,因为此时 所返回的 MapperProxy是实现了 UserMapper接口的。只不过 MapperProxy拦截了所有对userMapper中方法的调用。
MapperProxy类
继续深挖MapperProxy类,他实现了InvocationHandler接口,因此调用它时会执行invoke方法,invoke方法源码如下
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
if (this.isDefaultMethod(method)) {
return this.invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
MapperMethod mapperMethod = this.cachedMapperMethod(method);
return mapperMethod.execute(this.sqlSession, args);
}
第一个判断是对Object方法的判断,如果是调用Object中的方法,则直接调用被代理对象的方法。核心的代码是mapperMethod.execute(this.sqlSession, args);此方法是真正执行sql的地方,其中sqlSession是数据库的会话,args是调用dao接口时的参数。继续看execute方法
public Object execute(SqlSession sqlSession, Object[] args) {
Object param;
Object result;
switch(this.command.getType()) {
case INSERT:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
break;
case UPDATE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
break;
case DELETE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
break;
case SELECT:
if (this.method.returnsVoid() && this.method.hasResultHandler()) {
this.executeWithResultHandler(sqlSession, args);
result = null;
} else if (this.method.returnsMany()) {
result = this.executeForMany(sqlSession, args);
} else if (this.method.returnsMap()) {
result = this.executeForMap(sqlSession, args);
} else if (this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else {
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + this.command.getName());
}
if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
} else {
return result;
}
}
到这就已经到了对数据库的操作了。对于Mybatis的总结,我们编写的dao层接口就对应了数据库的操作。Mybatis会为dao层的所有Mapper类生成代理对象,因此实际在调用Mapper接口的方法时,调用的是代理对象的方法。这个代理对象接管了Mapper的方法到xml中的SQL映射。