Mybatis源码阅读

137 阅读5分钟

前言

由于工作中第一次使用Mybatis,阅读完官方文档后,想了解一下Mybaits的主体代码设计和运行流程,通过在本地搭建Mybatis环境并Debug源码,最终以框架图的输出来总结本次源码阅读。

官方文档

源码阅读目标

  • Mybatis的运行流程是怎么样的?
  • 阅读完主体代码后画一个简易的框架图(如下)

image.png

一、  环境工作

pom文件依赖

<?xml version="1.0" encoding="UTF-8"?>
<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>

    <groupId>org.example</groupId>
    <artifactId>mybatis_study</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!-- 最新release版本是3.5.15 -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.15</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.23</version>
        </dependency>
    </dependencies>
</project>

mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="com.mysql.jdbc.Driver"/>
                <property name="url" value="jdbc:mysql://localhost:3336/test_db?characterEncoding=utf-8"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>

        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/BlogMapper.xml"/>
    </mappers>
</configuration>

BlogMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.example.mapper.BlogMapper">
    <select id="selectBlogById" resultType="org.example.po.Blog">
        select * from Blog where id = #{id}
    </select>

    <select id="selectBlogByTitle" resultType="org.example.po.Blog">
        select * from Blog where title = #{title}
    </select>
</mapper>

BlogMapper

package org.example.mapper;

import org.example.po.Blog;

/**
 * @author caizelin
 * @date 2024/1/13 18:09
 */
public interface BlogMapper {

    Blog selectBlogById(Integer id);

    Blog selectBlogByTitle(String title);
}

BlogPO

package org.example.po;

/**
 * @author caizelin
 * @date 2024/1/13 18:08
 */
public class Blog {
    private Integer id;
    private String title;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

Main

package org.example;

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.example.mapper.BlogMapper;
import org.example.po.Blog;

import java.io.IOException;
import java.io.InputStream;

/**
 * main()方法,分4步来debug源码
 * @author caizelin
 * @date 2024/1/13 17:59
 */
public class Main {
    public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        // step 1 : 构建SqlSession工厂
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

        // step 2: 创建SqlSession
        try (SqlSession session = sqlSessionFactory.openSession()) {
            // step 3: 通过sqlSession先获取到Mapper接口对象
            BlogMapper mapper = session.getMapper(BlogMapper.class);
            
            // step 4: 调用mapper的方法
            Blog blog = mapper.selectBlogById(1);
        }
    }
}

二、  源码分析

1.  SqlSessionFactory

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

这段代码执行完成后,会优先解析mybatis-config.xml。然后转为配置对象Configuration,关注mapperRegistry和mappedStatements字段,这两个字段由具体的Mapper.xml解析出来的。

image.png image.png

  • environment字段,dataSource数据放在在这里 image.png

  • mapperRegistry负责生成所有的Mapper的代理工厂的Map image.png

public class MapperRegistry {
  private final Configuration config;
  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new ConcurrentHashMap<>();
 }
- config: 由xml解析出来的全局的配置类Configuration
- knowMappers: 收集所有的Mapper接口的接口名-代理对象工厂类的Map
 比如:BlogMapper Interface
     key: BlogMapper接口全类名称 如org.example.mapper.BlogMapper
      value: BlogMapper接口的代理对象工厂类,MapperProxyFactory<BlogMapper>
  • mappedStatements负责解析所有的xml并生成:方法名称- xml配置解析对象(包含sql等信息) image.png

第一次代码推断

image.png 推断代码段一:

BlogMapper mapper = session.getMapper(BlogMapper.class);

当从session.getMapper的时候,从configuration拿到mapperRegistry的knownMappers, 然后根据接口获取到对应的Mapper的代理类工厂,从工厂中创建出对应的代理类

推断代码段二:

Blog blog = mapper.selectBlogById(1);

代理类执行代码, 先从当前这个mapper中,根据方法名称,从mappedStatements中根据方法名,拿到xml配置解析出来的对象MappedStatement,然后取出MappedStatement.sqlSource.sql和入参来做SQL的拼接,得到select * from Blog where id = 1; 然后调用JDBC API 查询DB。

也就是说,这两行代码的核心是:

根据Mapper接口得到代理类,代理类中,完成SQL拼接,然后执行SQL,响应封装并返回。

2.  SqlSession

SqlSession session = sqlSessionFactory.openSession() 

image.png

SqlSession本身是个接口, 从注释信息来看,通过这个接口来执行命令,获取Mapper代理对象,并且管理transactions(一个session连接) image.png

SqlSession的默认实现是DefaultSqlSession image.png

也就是说,DefaultSqlSession内组合来Executor,通过Executor来调用JDBC来完成CRUD image.png

第二次代码推断

走到了这里,这里就是根据statement得到MappedStatement,然后调用executor去查DB。根据第一次代码推断,代理类去执行方法然后返回数据,也就是说,代理类Blog blog = mapper.selectBlogById(1) 是怎么走到SqlSession.selectList(),所以,代理类的核心工作逻辑大概就是:根据selectBlogById判断要走到SqlSession的哪个方法,然后调用SqlSession的方法执行代码

3.  获取代理类逻辑

Debug源代码 image.png image.png image.png image.png image.png image.png

4.  代理类执行方法

image.png image.png image.png

代理类执行方法的时候,在invoker的方法中,调用cachedInvoker.invoke(),而cachedInvoker的时候,会传入当前方法method,当前Mapper接口,以及sqlSession中的configuration(全局配置) 生成一个MapperMethod 对象。这个对象里面有两个字段需要关注,command和method方法签名

调用cachedInvoker.invoke() image.png

cachedInvoker.invoke()即执行MapperMethod.execute(sqlSession, args)方法,在这里最终会进入到SELET中, image.png

上图的写错了,是进入到SELET中,然后调用了sqlSession.selectOne()方法去执行数据查询 image.png

小结:这个代理类的作用仅仅是分析出目前所执行的方法要去调用sqlSession的哪个方法,然后传入method名称和参数,调用sqlSession.selectOne()去执行代码。

5.  SqlSession最终执行方法

image.png image.png

Eexecutor会从DB拿数据 image.png

执行doQuery方法,在这里执行prepareStatement image.png

执行repareStatement image.png

获取数据库连接 image.png image.png image.png image.png

返回Statement,这里的SQL已经拼接好了 image.png

执行handler.query方法 image.png image.png image.png image.png

三、总结

流程大概走完了,我在这分为了3层,配置层,代理层,核心层

配置层:

  • 通过xml解析,得到Configuration配置类(全局配置),这个类中,有两个核心字段需要关注,分别是mapperRegistry(为每个mapper.xml注册代理类工厂,放在knowMappers子属性中)和mappedStatements(扫描每个mapper.xml 得到 方法名-MappedStatement(xml中该方法sql的配置映射对象))
  • 构建SqlSessionFactory, 使用默认实现DefaultSqlSessionFactory,该对象组合了Configuration作为其属性

核心层:

  • 使用SqlSessionFactory构建SqlSession,SqlSession接口可以执行命令,获取mapper代理类,并管理连接
  • SqlSession调用Executor进行SQL的执行
  • Executro调用 JDBC API 和DB交互,封装并返回数据

代理层:

  • 基于核心层的SqlSession获取mapper代理类,调用代理类执行方法
  • 代理类本身是MapperProxy类型,invoke的时候,会使用方法名,全局配置Configuration, mapper的接口类型构造MapperMethod对象,该对象在构建过程中,会解析入参并归类当前的方法属于哪个SqlCommandType(SQL的命令类型,INSERT,UPDATE,DELETE,SELECT等,并封装到属性SqlComand中。
  • 代理类构造完MapperMethod后,会执行MapperMethod.executor()方法,该方法会根据当前MapperMethod对象的SqlCommandType,去调用核心层的sqlSession的对应方法去完成命令的执行,然后核心层继续往下执行得到响应并返回给代理类数据

image.png