MyBatis源码分析【二】核心运行源码

138 阅读15分钟

第2节 核心运行源码

对于MyBatis,是一个ORM类型的框架,解决了数据库访问和操作的问题,对现有的JDBC技术进行了封装,使用SqlSession对相关的技术进行了封装。

在 Mybatis 开发过程中,有着两种配置文件:主配置文件和Mapper文件(对应于DAO层接口)。那么这文件最终是如何进行解析的?

配置文件中的那些标签是否都对应于Java对象?

也从这个章节开始,逐步开始分析Mybatis的源码!

一、基础回顾

1.1 主配置文件

首先,在开发过程中的主配置文件,在里面我们会做以下几件事情:

  • 配置数据库连接环境
  • 注册Mapper文件
  • 配置信息
  • 设置别名
 <?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>
     <!--配置信息,比如日志-->
     <settings>
         <setting name="logImpl" value="STDOUT_LOGGING"/>
     </settings>
     <!--设置别名-->
     <typeAliases>
         <typeAlias type="com.haolong.User"></typeAlias>
     </typeAliases>
     <!--配置Mybatis的运行环境 default是从多个数据源中选一个,选的哪一个的id与这个的defalut的值是一样的-->
     <environments default="development">
         <!--一个数据源,id值唯一-->
         <environment id="development">
             <!--配置事务管理-->
             <transactionManager type="JDBC"/>
             <!--配置连接池-->
             <dataSource type="POOLED">
                 <!--连接数据库的四个要素-->
                 <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                 <property name="url" value="jdbc:mysql://localhost:3306 /jdbc?serverTimezone=GMT%2B8"/>
                 <property name="username" value="root"/>
                 <property name="password" value="123456"/>
             </dataSource>
         </environment>
     </environments>
     <mappers>
         <mapper resource="mapper/UserMapper.xml"/>
     </mappers>
 </configuration>

既然说环境可以配置多个,我们是否不同的方法,选择不同的数据库进行操作?

解答:可以,在标签之中,是有一个属性,叫做databaseId,可以通过这个进行指定

 <insert id="save" parameterType="User" databaseId="">
     insert into user(name) values(#{name})
 </insert>

既然可以使用多个数据源,那么在一个Service之中,多次进行Dao层操作(操作不同的数据源),那么这个的事务如何进行控制?

解答:只能通过分布式事务进行控制

同样,注意到主配置文件的根标签是configuration标签

1.2 两种开发方式

01 通过原生的接口

 InputStream resourceAsStream = Resources.getResourceAsStream("config.xml");
 ​
 SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
 ​
 SqlSession sqlSession = build.openSession();
 ​
 String url = "com.hoalong.dao.UserDao.save";
 ​
 User user = new User("haolong");
 ​
 sqlSession.update(url, user);
 ​
 sqlSession.commit();

02 通过代理的方式

 InputStream resourceAsStream = Resources.getResourceAsStream("config.xml");
 ​
 SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
 ​
 SqlSession sqlSession = build.openSession();
 ​
 UserDao mapper = sqlSession.getMapper(UserDao.class);
 ​
 mapper.save(new User("haolong"));
 ​
 sqlSession.commit();
 ​

在这种开发方式之下,我们并没有开发过对应UserDao的实现类,这里就是多态的体现,那么就能够间接性的得出通过getMapper方法,获取出来的是对应的代理对象。那么这个代理对象在哪里?

这实际上就是一个动态字节码技术,在JVM运行时创建,当JVM消失之后,就消失

那么这个动态代理对象是如何创建出来的?里面的方法又是如何实现的?

  • 既然说SqlSession之中有原生的实现,我们就可以在代理对象之中,直接使用原生的方式就能实现
  • 而对于这个对象的创建来说,实际上就是一个代理模式
 # 什么时候使用代理模式?
 ​
 - 为原始对象添加额外功能
 ​
 - 远程代理(网络通信,数据传输)
 ​
 - 无中生有,开发过程之中只定义了接口,我们看不见实实在在的实现类文件,但是在运行的时候能够感知的到

这里,我们首先来模拟一下,代理开发过程,为了操作的方便(避免在代理过程之中要进行特殊判断),这里Dao层之中,只保留一个方法

 public interface UserDao {
     public List<User> selectList();
 }

开发代理类

  • 传入SqlSession,是为了使用原生接口的方式
  • 传入Class,是为了获取接口的全限定名称。
 public class UserDaoProxy implements InvocationHandler {
     private SqlSession sqlSession;
     private Class aClass;
     public UserDaoProxy(SqlSession sqlSession,Class aClass) {
         this.sqlSession = sqlSession;
         this.aClass = aClass;
     }
     @Override
     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
         String namespace = aClass.getName();
         String id = method.getName();
         List<User> res = sqlSession.selectList(namespace + "."+ id);
         return res;
     }
 }
 ​

测试一下

 InputStream resourceAsStream = Resources.getResourceAsStream("config.xml");
 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(resourceAsStream);
 SqlSession sqlSession = sqlSessionFactory.openSession();
 ​
 UserDao userDao = (UserDao) Proxy.newProxyInstance(
         TestProxy.class.getClassLoader(),
         new Class[]{UserDao.class},
         new UserDaoProxy(sqlSession,UserDao.class)
     );
 List<User> users = userDao.selectList();
 users.forEach(System.out::println);

这样做也是可以的,这个时候我们就可以来测试一下MyBatis之中,是如何做的,通过getMapper方式,我们首先就能够找到下面这个调用链条

1、首先,是调用下面的这个方法,在这个方法之中,会创建MapperProxy,最终就交给了上面这个方法,而上面这个方法之中的代码,就如同我们所做的这个代理,所以也能够明白,这个MapperProxy 对象,就是核心做代理的

image-20221229223328188

2、 而在创建这个代理类MapperProxy的时候,同样传入了SqlSession和对应的接口,既然这个一个代理类,我们只需要核心分析一下,里面的invoke方法,就行了

这个时候,问题就有了,为什么将原来的Method对象变为了MapperMethod对象呢?带来了哪些好处?

从里面的构造方法,也能够明白,其实就是封装了两个核心对象,一个就是SqlCommand对象,另一个就是MethodSignature对象

image-20221229225400337

分析完成这个对象之后,我们来看一下核心的执行方法

而这个方法,实际上就是通过判断不同的方法类型,调用不同的SqlSession原生接口方式

03 两种方式的对比

  1. 使用Mapper代理这种方式相比而言更加好,主要在于可读性更加好,概念更加清晰。
  2. 并且Mapper代理方式,实际上就是使用代理设计模式对原生接口进行封装
 // 如果说使用这种方式,表示一个name含义的时候,我们并不清楚,这个name表示的是人名还是狗名
 String name = "peiki";
 ​
 // 通过这种方式,我们就很容易能够知道,这个name就是一个人名
 public class User {
     private String name = "peiki";
 }

二、数据存储类对象

在MyBatis之中,其实有两类对象,一类就是数据存储类对象,一类就是执行类对象,这里首先对数据存储类对象进行分析

在Java之中,对MyBatis的配置信息进行存储,在MyBatis的开发过程之中,主要有两种配置文件

  • 一种叫做:主配置文件,被封装成为了Configuration对象
  • 一种叫做:XXXMapper.xml,被封装成为了MappedStatement对象(并不是很准确)

2.1 Configuration

作用:主要用来封装Mybatis主配置文件中的信息

01 封装 environment 标签

对象:environment 对象

 protected Environment environment;

02 封装 settings 标签

因为这里面就是一些配置信息,也就说只用关注这些配置是否开启就行了,对应于boolean类型的值

 protected boolean safeRowBoundsEnabled;
 ​
 protected boolean safeResultHandlerEnabled;
 ​
 protected boolean mapUnderscoreToCamelCase;
 ​
 protected boolean aggressiveLazyLoading;
 ​
 protected boolean multipleResultSetsEnabled;
 ​
 protected boolean useGeneratedKeys;
 ​
 protected boolean useColumnLabel;
 ​
 protected boolean cacheEnabled;
 ​
 protected boolean callSettersOnNulls;
 ​
 protected boolean useActualParamName;
 ​
 protected boolean returnInstanceForEmptyRow;

03 封装 typeAlias 标签

对象:typeAliasRegistry

 protected final TypeAliasRegistry typeAliasRegistry;

04 封装 mappers 标签

在Configuration对象中,封装为一个Set集合,保证元素不会进行重复

 protected final Set<String> loadedResources;

05 封装 Mapper 文件信息

在之前的分析过程之中,对于Mapper文件,我们把它封装为了MapperStatement对象,但其实这并不是准确的,正确的应该是将我们所写的CRUD的标签封装为了MapperStatement对象

在主配置文件中,保存mapper文件中的信息

这里的Key都是 key = namespace + id

 protected final Map<String, MappedStatement> mappedStatements;
 ​
 protected final Map<String, Cache> caches;
 ​
 // 会将所有Mapper文件的ResultMap都会存储到Configuration对象里面
 protected final Map<String, ResultMap> resultMaps;
 ​
 protected final Map<String, ParameterMap> parameterMaps;
 ​
 ​
 protected final Map<String, KeyGenerator> keyGenerators;

06 创建核心对象

在这个类之中,同样涉及到一下核心对象的创建工作

2.2 MappedStatement

mapper文件中的标签被封装成了一个MappedStatement对象,所以说一个Mybatis应用中就会有N个MappedStatement对象

这里看看我们所熟悉的东西,这些其实都是标签中的属性

 public final class MappedStatement {
     
     private String resource;
     
     // 主配置文件解析之后的对象
     private Configuration configuration;
     
     // id值,唯一:namespace + id
     private String id;
     
     // 使用Statement的具体类型,默认就是PrepareStatement
     // statementType="CALLABLE | PREPARED | STATEMENT"
     private StatementType statementType;
     
     // 结果类型
     private ResultSetType resultSetType;
     
     private SqlSource sqlSource;
 }

不过这里我们并没有发现SQL语句在哪里?只是发现了SqlSource

 public interface SqlSource {
     BoundSql getBoundSql(Object var1);
 }

发现这是一个接口,是获取BoundSql的对象,而这个对象里面才真正封装了SQL语句,在这里面不仅仅是封装了一个基础的SQL语句,还包含了对应的参数信息

 public class BoundSql {
     private final String sql;
     private final List<ParameterMapping> parameterMappings;
     private final Object parameterObject;
     private final Map<String, Object> additionalParameters;
     private final MetaObject metaParameters;
 }

到这里,配置文件对应的对象就分析完成了,但是MyBatis又是如何将配置文件转为这个两大对象的呢?

三、解析XML

在MyBatis的开发过程之中,对于第一行,虽然说我们只写了主配置文件的路径,但是实际上,他也会将Mapper文件读取到这个输入流之中

 InputStream resourceAsStream = Resources.getResourceAsStream("config.xml");

在读取完成之后,MyBatis又是如何做的呢?他又是如何将XML文件解析为对象的呢?

对于XML的解析方式,主要有三种方式:DOM,SAX,XPath。

在Mybatis中,解析XMl中用到的是XPathParser,在Spring中用到的是SAX。

在XPathParser之中,又是将整个标签组划分为了对象

3.1 手写模拟

首先,新建一个xml文件,我们来模拟一下,比方说,我下面建立的这个文件,并且已经表明了整个文件的结构

明确了结构之后,可以看看下面的代码:

 @Test
 public void test() throws IOException {
     
     InputStream resourceAsStream = Resources.getResourceAsStream("user.xml");
     
     // 在构建过程之中,将输入流作为XPathparser的构造参数传入,进行读取分析
     XPathParser xPathParser = new XPathParser(resourceAsStream);
     
     // 解析users标签及其子标签
     List<XNode> list = xPathParser.evalNodes("/users/*");
     
     // 通过Node的方式,获取子标签
     for (XNode xNode : list) {
         log.info("xNode:{}",xNode);
         List<XNode> children = xNode.getChildren();
         User user = new User();
         user.setId(children.get(0).getIntBody());
         user.setName(children.get(1).getStringBody());
         log.info("user:{}",user);
     }
 }

image-20221214203948627

通过执行结果,和我们分析的一致,这里,我们手动模拟了一下XML的解析,接下来,就来看看Mybatis中是如何做的

3.2 源码分析

在我们手写过程中,我们会首先获取主配置文件的输入流,然后传入到XPathParser中,而在Mybatis的开发过程,我们是将这个输入流传入到SqlSessionFactoryBuilder的build方法里面,所以可以猜出,build方法中会出现XPathParser的解析过程,我们来通过阅读源码来验证一下我们的猜想:

01 原来的代码

 InputStream resourceAsStream = Resources.getResourceAsStream("config.xml");
 ​
 SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
 ​
 // 解析了配置文件,并创建了默认的SqlSessionFactory方法
 SqlSessionFactory factory = factoryBuilder.build(resourceAsStream);
 ​
 // 
 SqlSession sqlSession = factory.openSession();

02 build 方法

接下来,分析一下,build方法,忽略重载,直接看最后的方法

 public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
     SqlSessionFactory var5;
     try {
         XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
         var5 = this.build(parser.parse());
     } catch (Exception var14) {
         throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
     } finally {
         ErrorContext.instance().reset();
 ​
         try {
             inputStream.close();
         } catch (IOException var13) {
         }
     }
     return var5;
 }

在这个方法之中,首先是进行XMLConfigBuilder对象的创建,通过查看源码,我们能够分析出,这里面封装了XPathParser,而对于这个build方法来说

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

他的参数是Configuration对象,所以能够得出对于parser.parse()的返回值是Configuration对象,并在这个方法之中,完成了对应的解析工作

03 parse()

所以,接下来,就来分析一下parser.parse()

 public Configuration parse() {
     if (this.parsed) {
         throw new BuilderException("Each XMLConfigBuilder can only be used once.");
     } else {
         this.parsed = true;
         // 主配置文件中的子标签 都被封装成了 XNode 传给了 parseConfiguration
         this.parseConfiguration(this.parser.evalNode("/configuration"));
         return this.configuration;
     }
 }

在这个方法之中,我们看到了和我们首先过程之中一样的代码,就是this.parser.evalNode("/configuration"),说明这个时候,已经将主配置文件中的内容封装为了XNode节点。将这个XNode传入下面的方法之中,进行对应的标签解析

04 主配置文件解析

不过在这个方法之中,也请注意,Mapper文件的解析也是在这个方法之中mapperElement(root.evalNode("mappers")),从这里面就能够发现,这里解析的就是Mapper文件在主配置文件注册的标签

05 mapper文件解析

这里面的if else 判断,其实就对应于mappers标签的不同写法,但是在每个分支之中,最后的一步,其实是可以分为两组,其实也对应于两种方式

 <mappers>
     <package name=""/>
     <mapper resource=""/>
 </mappers>

1)第一组

 configuration.addMappers(mapperPackage);

最终也将会转化为Mapper文件的解析

2)第二组

 XMLMapperBuilder mapperParser = new XMLMapperBuilder(
     inputStream, 
     configuration, 
     resource, 
     configuration.getSqlFragments()
 );
 mapperParser.parse();

我们来看这解析的方法:

 public void parse() {
     // 如果说当前xml资源还没有被加载过
     if (!configuration.isResourceLoaded(resource)) {
         // 解析mapper元素
         configurationElement(parser.evalNode("/mapper"));
         configuration.addLoadedResource(resource);
         // 解析和绑定命名空间
         bindMapperForNamespace();
     }
     // 解析 resultMap
     parsePendingResultMaps();
     // 解析 cache-ref
     parsePendingCacheRefs();
     // 解析声明的Statement
     parsePendingStatements();
 }

这里只关心:解析mapper元素

 private void configurationElement(XNode context) {
     try {
         // 提取命名空间
         String namespace = context.getStringAttribute("namespace");
         if (namespace == null || namespace.equals("")) {
             throw new BuilderException("Mapper's namespace cannot be empty");
         }
         builderAssistant.setCurrentNamespace(namespace);
         // 解析 cache-ref 标签
         cacheRefElement(context.evalNode("cache-ref"));
         // 解析 cache 标签
         cacheElement(context.evalNode("cache"));
         // 废弃
         parameterMapElement(context.evalNodes("/mapper/parameterMap"));
         // 解析 resultMap 标签
         resultMapElements(context.evalNodes("/mapper/resultMap"));
         // 解析 sql 标签
         sqlElement(context.evalNodes("/mapper/sql"));
         // 构建 Statement
         buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
     } catch (Exception e) {
         throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
     }
 }

三、操作类对象

对于这些操作类型的对象,是在Configuration对象之中进行创建的,在分析Configuration对象的时候,也是看到了对应的创建方法.

对于MyBatis的两种开发方式,通过getMapper的开发方式,最终能够转换为了原生接口的开发方式,但是原生接口底层又是如何实现的呢?

这就涉及到了底层的操作类对象

3.1 Executor

是MyBatis中处理功能的核心

对于CRUD而言,如果是增删改,对应于update方法,如果是查询,对应于query

不过Executor这是一个接口,之所以定义为接口,是因为这里牵扯到设计原则相关的,在我们定义操作相关的功能的时候,都定义为接口。

而对于接口实现类,有如下几个:

  • BatchExecutor

    • 作用:JDBC批处理操作(一次连接之上,进行多次SQL)
  • ReuseExecutor

    • 作用:当SQL语句一样的时候,复用Statement对象。注意:Statement对象和SQL语句相关的
  • SimpleExecutor

    • 默认执行器,在源码中用的体现,主要是体现在MyBatis的Configuration对象之中
     protected ExecutorType defaultExecutorType = ExecutorType.SIMPLE;
    

3.2 StatementHandler

作用:Mybatis中封装JDBC Statement对象的核心,是执行CRUD的核心

image.png

既然StatementHandler才是封装JDBC的核心,为什么还要提供Executor?

解答:StatementHandler仅仅完成你的是Executor核心功能中一个部分,也只做这一部分功能,Executor再此基础之上,对这个类型进行了包装,添加额外功能

再此之前,我们分析数据存储类对象的时候,提到过,对于SQL的封装,封装为了BoundSql对象

这个类型,仍然是接口,同样来看看他的接口实现类:

  • SimpleStatementHandler
  • PreparedStatementHandler
  • CallableStatementHandler

这里我们查看SimpleStatementHandler中的query方法,发现他其实就是JDBC执行的过程,最后封装结果集返回,这里也是能够体会出就是对JDBC的封装,才是数据库访问操作的核心

image-20221214143819938

3.3 ParameterHandler

我们所写的SQL都是带有参数的,如何将MyBatis中的参数,替换为JDBC相关的参数,也就是将@Param--> #{} ---> ? 的过程

  <insert id="save" parameterType="User">
         insert into user(name) values(#{name})
  </insert>

作用:Mybatis参数 -----> JDBC参数

3.4 ResultSetHandler

作用:对JDBC中的 ResultSet 结果集的进行封装

 public interface ResultSetHandler {
     <E> List<E> handleResultSets(Statement var1) throws SQLException;
 ​
     <E> Cursor<E> handleCursorResultSets(Statement var1) throws SQLException;
 ​
     void handleOutputParameters(CallableStatement var1) throws SQLException;
 }

这个接口中方法中传入的为什么是Statement?

解答:在JDBC之中,有了Statement才能够获取ResultSet

3.5 TypeHandler

作用:Java对象属性类型和MySQL类型的相互映射

主要体现就是两点:在执行SQL的时候,Java类型转为数据库类型;查询出结果,数据库类型转为Java类型

 public interface TypeHandler<T> {
     void setParameter(PreparedStatement var1, int var2, T var3, JdbcType var4) throws SQLException;
 ​
     T getResult(ResultSet var1, String var2) throws SQLException;
 ​
     T getResult(ResultSet var1, int var2) throws SQLException;
 ​
     T getResult(CallableStatement var1, int var2) throws SQLException;
 }

四、与 SqlSession 建立联系

首先,来回顾一下,原生Mybatis中的写法:

 InputStream resourceAsStream = Resources.getResourceAsStream("config.xml");
 ​
 SqlSessionFactory build = new SqlSessionFactoryBuilder().build(resourceAsStream);
 ​
 SqlSession sqlSession = build.openSession();
 ​
 String url = "com.hoalong.dao.UserDao.save" ;
 ​
 User user = new User("haolong");
 ​
 sqlSession.update(url, user);
 ​
 sqlSession.commit();

通过代理对象的方式,执行对应的操作,最终都是转为原生的这种写法,所以,这个时候,我们只需要关心原生写法即可

这里,通过Debug过程,来走一遍这个流程:

  • 首先:DefaultSqlSession,这里发现这里调用的是update方法

    • 这个的statement的就是namespace + id
 public int insert(String statement, Object parameter) {
     return this.update(statement, parameter);
 }
 public int update(String statement, Object parameter) {
     int var4;
     try {
         this.dirty = true;
         // 1. 获取了MapperStatement对象,也就是之前所说的Mapper文件中的一个标签对应于一个MappedStaatement
         MappedStatement ms = this.configuration.getMappedStatement(statement);
         // 2. 调用 Executor 对象的 update方法
         var4 = this.executor.update(ms, this.wrapCollection(parameter));
     } catch (Exception var8) {
         throw ExceptionFactory.wrapException("Error updating database.  Cause: " + var8, var8);
     } finally {
         ErrorContext.instance().reset();
     }
 ​
     return var4;
 }

而在Executor中:

 public int update(MappedStatement ms, Object parameter) throws SQLException {
     ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
     if (this.closed) {
         throw new ExecutorException("Executor was closed.");
     } else {
         this.clearLocalCache();
         return this.doUpdate(ms, parameter);
     }
 }

我们发现,最终是调用StatementHandler中的方法来实现的

  public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
         Statement stmt = null;
         int var6;
         try {
             Configuration configuration = ms.getConfiguration();
             StatementHandler handler = configuration.newStatementHandler
                 (this, ms, parameter, RowBounds.DEFAULT, (ResultHandler)null, (BoundSql)null);
             stmt = this.prepareStatement(handler, ms.getStatementLog());
             var6 = handler.update(stmt);
         } finally {
             this.closeStatement(stmt);
         }
         return var6;
     }

值得注意的是,这里在执行 update 方法的时候,传入了一个Statement对象,我们首先来看看这个对象是如何进行获取的

 private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
     Connection connection = this.getConnection(statementLog);
     Statement stmt = handler.prepare(connection, this.transaction.getTimeout());
     handler.parameterize(stmt);
     return stmt;
 }

这里又是调用了StatementHandler中的prepare方法,所以在这个时候,我们明白:在执行update方法之前,我们需要先执行StatementHandler中的prepare方法,为update方法准备Statement对象,不过请注意:不仅仅是update操作,select操作也同样会先准备Statement对象。

分析完成之后,我们继续看这个update方法:最后都是原生JDBC的操作

 @Override
 public int update(Statement statement) throws SQLException {
     PreparedStatement ps = (PreparedStatement) statement;
     ps.execute();
     int rows = ps.getUpdateCount();
     Object parameterObject = boundSql.getParameterObject();
     KeyGenerator keyGenerator = mappedStatement.getKeyGenerator();
     keyGenerator.processAfter(executor, mappedStatement, ps, parameterObject);
     return rows;
 }