源码篇 - 带你精读 MyBatis 源码

1,550 阅读8分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

对于开发人员来说,阅读一些框架的源码,不光有助于提升我们自身的技术水平,还可以在遇到一些问题时快速定位问题的原因,也有助于我们去参加一些开源实践,轮子虽然不是我们自己造的,但是我们要懂如何造轮子。下面我们将一起走进 MyBatis 源码的世界。

1 了解项目结构

image.png

  • annotations 注解

    提供在Mapper 接口上使用注解进行CRUD操作,虽然方便但是不能很好的解耦,更推荐在XML文件写SQL

  • binding

    在调用 SqlSession 相应方法执行数据库操作时,需要指定映射文件中定义的 SQL 节点,如果出现拼写错误,我们只能在运行时才能发现相应的异常。Mapper 接口与映射配置文件关联起来,系统可以通过调用自定义 Mapper 接口中的方法执行相应的 SQL 语句完成数据库操作,从而避免上述问题。

    注意: 开发人员无须编写自定义 Mapper 接口的实现,MyBatis 会自动为其创建动态代理对象。

    一般在系统抛出 BindingException 我们就需要去检查namespace,SQL的id是否与系统里的命名一一对应。

  • builder 配置解析

    MyBatis 初始化时,会加载 mybatis-config.xml 配置文件、映射配置文件以及 Mapper 接口中的注解信息,解析后的配置信息会形成相应的对象并保存到 Configuration 对象中。

  • cache 缓存

    MyBatis 中提供了一级缓存和二级缓存,而这两级缓存都是依赖于基础支持层中的缓 存模块实现的。

    注意: MyBatis 中自带的这两级缓存与 MyBatis 以及整个应用是运行在同一个 JVM 中的,共享同一块堆内存。如果这两级缓存中的数据量较大, 则可能影响系统中其他功能的运行,所以当需要缓存大量数据时,优先考虑使用 Redis、Memcache 等缓存产品。

  • cursor 执行结果的游标

  • datasource 数据源

    数据源是开发常用的组件之一,开源的数据源都提供了比较丰富的功能,例如,连接池功能、检测连接状态等,选择性能优秀的数据源组件对于提升 ORM 框架乃至整个应用的性能都是非常重要的。

    MyBatis提供了相应的数据源实现,也提供了与第三方数据源集成的接口,这些功能都位于数据源模块之中

  • exceptions 异常

    定义了 MyBatis 专有的 PersistenceException 和 TooManyResultsException 异常。

  • executor SQL执行器

    主要负责维护一级缓存和二级缓存,并提供事务管理的相关操作,它会将数据库相关操作委托给 StatementHandler完成。

  • io 资源加载

    对类加载器进行封装,确定类加载器的使用顺序,并提供加载类文件以及其他资源的功能,

  • jdbc JDBC 单元测试工具类。

  • lang Java版本

  • logging 日志

    无论开发、测试还是生产环境日志在整个系统中地位极其重要。

    该模块可以很好的帮我们集成第三方日志框架。

  • mapping SQL解析

    例如:

    • <resultMap>( ResultSet 的映射规则) 会被解析成 ResultMap 对象。
    • <result> (属性映射)会被解析成 ResultMapping 对象。之后,利用该 Configuration 对象创建 SqlSessionFactory对象。待 MyBatis 初始化之后,开发人员可以通过初始化得到 SqlSessionFactory 创建 SqlSession 对象并完成数据库操作。
  • parsing 解析

    初始化对mybatis-config.xml 以及映射配置文件进行解析

    处理SQL语句中的占位符

  • plugin 插件

    MyBatis 提供了插件接口,我们可以通过添加用户自定义插件的方式对 MyBatis 进行扩展。用户自定义插件也可以改变 Mybatis 的默认行为,例如,我们可以拦截 SQL 语句并对其进行重写。由于用户自定义插件会影响 MyBatis 的核心行为,在使用自定义插件之前,开发人员需要了解 MyBatis 内部的原理,这样才能编写出安全、高效的插件。

  • reflection

    对 Java 原生的反射进行了良好的封装,提了更加简洁易用的 API,方便上层使调用,并且对反射操作进行了一系列优化,例如缓存了类的元数据,提高了反射操作的性能。

  • scripting 动态SQL,会根据用户传入的实参,解析映射文件中定义的动态 SQL 节点,并形成数据库可执行的 SQL 语句。之后会处理 SQL 语句中的占位符,绑定用户传入的实参。

  • session

    核心是 SqlSession 接口,该接口中定义了 MyBatis 暴露给应用程序调用的 API,也就是上层应用与 MyBatis 交互的桥梁。接口层在接收到调用请求时,会调用核心处理层的相应模块来完成具体的数据库操作。

  • transaction 事务

    MyBatis对数据库事物进行抽象,其自身提供了响应的事物接口和简单实现

    通常情况下,MyBatis会与Spring进行集成,并由Spring管理框架。

  • type 类型

    1. MyBatis 为简化配置文件提供了别名,这里就需要该模块去做类型转换
    2. Java类型与JDBC类型的相互转化
  • utils 工具类

2 搭建测试类

image.png

2.1 引入 mysql 连接依赖

 <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.46</version>
 </dependency>

2.2 mybatis-config.xml文件

在测试包下,建自己的测试包配置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>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC">
      </transactionManager>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://newAliyun:3306/test"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="org/apache/ibatis/zhjtest/MbGoodsMapper.xml"/>
  </mappers>
</configuration>

2.3 建立实体

对应的在数据库建一张表

package org.apache.ibatis.zhjtest;
​
import java.math.BigDecimal;
import java.util.Date;
​
public class MbGoods {
​
  private String id;
​
  private String name;
​
  private BigDecimal price;
​
  private String description;
​
  private String type;
​
  private Integer status;
​
  private String createBy;
​
  private Date createTime;
​
  private String updateBy;
​
  private Date updateTime;
​
  public String getId() {
    return id;
  }
​
  public void setId(String id) {
    this.id = id;
  }
​
  public String getName() {
    return name;
  }
​
  public void setName(String name) {
    this.name = name;
  }
​
  public BigDecimal getPrice() {
    return price;
  }
​
  public void setPrice(BigDecimal price) {
    this.price = price;
  }
​
  public String getDescription() {
    return description;
  }
​
  public void setDescription(String description) {
    this.description = description;
  }
​
  public String getType() {
    return type;
  }
​
  public void setType(String type) {
    this.type = type;
  }
​
  public Integer getStatus() {
    return status;
  }
​
  public void setStatus(Integer status) {
    this.status = status;
  }
​
  public String getCreateBy() {
    return createBy;
  }
​
  public void setCreateBy(String createBy) {
    this.createBy = createBy;
  }
​
  public Date getCreateTime() {
    return createTime;
  }
​
  public void setCreateTime(Date createTime) {
    this.createTime = createTime;
  }
​
  public String getUpdateBy() {
    return updateBy;
  }
​
  public void setUpdateBy(String updateBy) {
    this.updateBy = updateBy;
  }
​
  public Date getUpdateTime() {
    return updateTime;
  }
​
  public void setUpdateTime(Date updateTime) {
    this.updateTime = updateTime;
  }
​
  @Override
  public String toString() {
    return "MbGoods{" +
      "id='" + id + ''' +
      ", name='" + name + ''' +
      ", price=" + price +
      ", description='" + description + ''' +
      ", type='" + type + ''' +
      ", status=" + status +
      ", createBy='" + createBy + ''' +
      ", createTime=" + createTime +
      ", updateBy='" + updateBy + ''' +
      ", updateTime=" + updateTime +
      '}';
  }
}
​

2.4 写Mapper接口

package org.apache.ibatis.zhjtest;
​
import org.apache.ibatis.annotations.Param;
​
public interface MbGoodsMapper {
​
  MbGoods selectById(@Param("id") String id);
}

2.5 写Mapper接口对应的XML

<?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="org.apache.ibatis.zhjtest.MbGoodsMapper">
​
  <select id="selectById" resultType="org.apache.ibatis.zhjtest.MbGoods">
    select * from mb_goods where id = #{id}
  </select></mapper>

2.6 编写测试类

package org.apache.ibatis.zhjtest;
​
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Test;
​
import java.io.IOException;
import java.io.Reader;
​
public class MyTest {
​
  private static SqlSessionFactory sqlSessionFactory;
​
  @Test
  public void test01() throws IOException {
    //1、创建SqlSessionFactory
    String resource = "org/apache/ibatis/zhjtest/mybatis-config.xml";
    final Reader reader = Resources.getResourceAsReader(resource);
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
    reader.close();
    //2、获取sqlSession
    SqlSession sqlSession = sqlSessionFactory.openSession();
    //3、获取mapper
    MbGoodsMapper mapper = sqlSession.getMapper(MbGoodsMapper.class);
    //4、执行数据库操作,并处理结果集
    MbGoods goods = mapper.selectById("1");
    System.out.println(goods);
  }
}

3 执行流程分析

  1. 第一步就是加载配置文件

    通过IO包下资源加载类加载配置文件

    final Reader reader Resources.getResourceAsReader(resource);
    
  2. 第二步通过SqlSessionFactoryBuilder建造者模式构建SqlSessionFactory

    sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader)
    
    • 先需要将XML内容读取到XMLConfigBuilder转换成文档对象Document赋值给当前对象的文档属性

      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
      ​
      private Document createDocument(InputSource inputSource) {
          // important: this must only be called AFTER common constructor
          try {
            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
            factory.setValidating(validation);
      ​
            factory.setNamespaceAware(false);
            factory.setIgnoringComments(true);
            factory.setIgnoringElementContentWhitespace(false);
            factory.setCoalescing(false);
            factory.setExpandEntityReferences(true);
      ​
            DocumentBuilder builder = factory.newDocumentBuilder();
            builder.setEntityResolver(entityResolver);
            builder.setErrorHandler(new ErrorHandler() {
              @Override
              public void error(SAXParseException exception) throws SAXException {
                throw exception;
              }
      ​
              @Override
              public void fatalError(SAXParseException exception) throws SAXException {
                throw exception;
              }
      ​
              @Override
              public void warning(SAXParseException exception) throws SAXException {
                // NOP
              }
            });
            return builder.parse(inputSource);
          } catch (Exception e) {
            throw new BuilderException("Error creating document instance.  Cause: " + e, e);
          }
        }
      
    • 再解析文档XPathParser解析成一个一个节点

    • 把内容都构建在Configuration对象

    • 构建会话工厂

      public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
      }
      
    • 得到SqlSessionFactory

  3. 第三步通过工厂构建SqlSession

    由上一步我们获得的是DefaultSqlSessionFactory所以本步通过默认实现来构建SqlSession

    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        try {
          // 环境
          final Environment environment = configuration.getEnvironment();
          /**
          获取一个事物工厂
          会根据配置文件是否指定事物工厂生成,如果没有则使用自己的事物管理
          经常我们会将事物交给Spring来管理
          private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) {
                if (environment == null || environment.getTransactionFactory() == null) {
                return new ManagedTransactionFactory();
            }
            return environment.getTransactionFactory();
          } 
          */
          final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    ​
          /**
          public ManagedTransaction(DataSource ds, TransactionIsolationLevel level, boolean         closeConnection) {
                this.dataSource = ds;
                this.level = level;
                this.closeConnection = closeConnection;
            }
          */
          tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
          /**
          // 根据配置文件指定类型,构建一个执行器
          public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
                executorType = executorType == null ? defaultExecutorType : executorType;
                executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
                Executor executor;
                if (ExecutorType.BATCH == executorType) {
                    executor = new BatchExecutor(this, transaction);
                } else if (ExecutorType.REUSE == executorType) {
                    executor = new ReuseExecutor(this, transaction);
                } else {
                    executor = new SimpleExecutor(this, transaction);
                }
                // 判断是否开启缓存
                if (cacheEnabled) {
                    executor = new CachingExecutor(executor);
                }
                /** 插件拦截器
                如果有相关接口,通过代理模式生成代理对象返回
                public static Object wrap(Object target, Interceptor interceptor) {
                    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
                    Class<?> type = target.getClass();
                    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
                    if (interfaces.length > 0) {
                        return Proxy.newProxyInstance(
                            type.getClassLoader(),
                            interfaces,
                            new Plugin(target, interceptor, signatureMap));
                        }
                    return target;
                }
                */
                executor = (Executor) interceptorChain.pluginAll(executor);
                return executor;
            }
          */
          final Executor executor = configuration.newExecutor(tx, execType);
          return new DefaultSqlSession(configuration, executor, autoCommit);
        } catch (Exception e) {
          closeTransaction(tx); // may have fetched a connection so lets call close()
          throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    

    根据返回确定使用哪种实现方式image.png

  4. 第四步获取对应mapper

    读取XML文件时会将命名空间添加到Map中,通过动态代理为接口创建对象

    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
    ​
     // xml处理绑定命名空间
     private void bindMapperForNamespace() {
        String namespace = builderAssistant.getCurrentNamespace();
        if (namespace != null) {
          Class<?> boundType = null;
          try {
            boundType = Resources.classForName(namespace);
          } catch (ClassNotFoundException e) {
            // ignore, bound type is not required
          }
          if (boundType != null && !configuration.hasMapper(boundType)) {
            // Spring may not know the real resource name so we set a flag
            // to prevent loading again this resource from the mapper interface
            // look at MapperAnnotationBuilder#loadXmlResource
            configuration.addLoadedResource("namespace:" + namespace);
            configuration.addMapper(boundType);
          }
        }
      }
    ​
    public <T> void addMapper(Class<T> type) {
        mapperRegistry.addMapper(type);
    }
    ​
     public <T> void addMapper(Class<T> type) {
        if (type.isInterface()) {
          // 判断是否存在相同的
          if (hasMapper(type)) {
            throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
          }
          boolean loadCompleted = false;
          try {
            // 代理工厂
            knownMappers.put(type, new MapperProxyFactory<>(type));
            // It's important that the type is added before the parser is run
            // otherwise the binding may automatically be attempted by the
            // mapper parser. If the type is already known, it won't try.
            // 映射器处理
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            // XML解析
            parser.parse();
            loadCompleted = true;
          } finally {
            if (!loadCompleted) {
              knownMappers.remove(type);
            }
          }
        }
      }
    
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
        // 根据接口名判断是否右对应的代理实现,此处抛异常很可能是接口名与命名空间不符
        final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
        if (mapperProxyFactory == null) {
          throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
        }
        try {
          return mapperProxyFactory.newInstance(sqlSession);
        } catch (Exception e) {
          throw new BindingException("Error getting mapper instance. Cause: " + e, e);
        }
     }
    
  5. 第五步执行数据库操作返回结果集,并记录在缓存数据