深入Mybatis源码实现


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映射。


Author: 顺坚
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source 顺坚 !
评论
 Previous
硬盘的工作原理 硬盘的工作原理
硬盘是用于存储数据的硬件,内存也是存储数据的,但二者不同的是内存数据是掉电不保存的而硬盘数据是可以永久保存的,在读写速度上硬盘比内存慢几个数量级。硬盘也是和程序交互比较频繁的硬件了,经常在看一些数据存储相关框架的原理时,会对一些硬盘方面的专
2019-07-13
Next 
聊聊对ThreadLocal的理解 聊聊对ThreadLocal的理解
JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。也是面试中出现频率比较高的知识点。ThreadLo
2019-06-22
  TOC