1.3.找到学习 Mybatis 源码的切入点

优质
小牛编辑
120浏览
2023-12-01

我们要学习Mybatis的源码,第一步肯定是要找到学习的切入点,我们先从一个简单的Demo开始,重新感受一下Mybatis的使用方式,并从中可以切入到mybatis源码的学习过程中去。

Hello World

首先我们新建一个Hello World工程,用来体会Mybatis的使用方式。

数据准备

连接Mysql服务器,新建一个名为mybatis的数据库,并创建一个包含了两个字段(id,name)的user表。

  • 创建数据库 CREATE DATABASE mybatis;
  • 新建一个用户表:
    CREATE TABLE `user` (
    `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
    `name` varchar(255) NOT NULL DEFAULT '' COMMENT '用户名称',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户';
    

完整的脚本:

  CREATE DATABASE mybatis;
  USE mybatis;
  CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `name` varchar(255) NOT NULL DEFAULT '' COMMENT '用户名称',
  PRIMARY KEY (`id`)
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户';

mybatis工程的src/test/java/org/apache/learning目录下,新建一个名为helloworld的子包,该包将会用来存放我们的示例代码。

helloworld下新建一个user子包,并新建以下文件。

  • User.java:

    import lombok.ToString;
    
    @ToString
    public class User {
        private Integer id;
        private String name;
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
    }
    
  • UserMapper.java:

    import org.apache.ibatis.annotations.Param;
    
    public interface UserMapper {
    
        /**
         * 根据ID获取用户信息
         *
         * @param id 用户ID
         * @return 用户信息
         */
        User getById(Integer id);
    
        /**
         * 插入一个新的用户数据
         *
         * @param id   用户ID
         * @param name 用户名称
         * @return 本次操作影响的行数
         */
        Integer insert(@Param("id") Integer id, @Param("name") String name);
    }
    
  • UserMapper.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.learning.helloworld.user.UserMapper">
    
        <insert id="insert">
            insert into user values(#{id},#{name});
        </insert>
    
        <select id="getById" parameterType="int" resultType="org.apache.learning.helloworld.user.User">
            SELECT * FROM user WHERE id=#{id};
        </select>
    </mapper>
    

之后在helloworld包下,新建以下文件:

  • hello-world.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>
        <settings>
            <setting name="cacheEnabled" value="true"/>
            <setting name="lazyLoadingEnabled" value="false"/>
            <!--日志实现类,使用SLF4J-->
            <setting name="logImpl" value="SLF4J"/>
        </settings>
    
        <environments default="development">
            <environment id="development">
                <!-- 声明使用那种事务管理机制 JDBC/MANAGED -->
                <transactionManager type="JDBC"/>
                <!-- 配置数据库连接信息 -->
                <dataSource type="POOLED">
                    <!-- 需要注意这里: MYSQL 6(含)可以使用下列配置,MYSQL 6以下还是需要使用旧的com.mysql.jdbc.Driver-->
                    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                    <!-- 需要特殊处理 & 符号,转为&amp; -->
                    <property name="url" value="jdbc:mysql://127.0.0.1:3306/mybatis?useUnicode=true&amp;characterEncoding=UTF-8&amp;serverTimezone=UTC"/>
                    <property name="username" value="root"/>
                    <!-- 需要特殊处理 & 符号,转为&amp; -->
                    <property name="password" value="dUP6lKU+c3&amp;s"/>
                </dataSource>
            </environment>
        </environments>
    
        <mappers>
            <mapper resource="org/apache/learning/helloworld/user/userMapper.xml"/>
        </mappers>
    
    </configuration>
    
  • HelloWorldTest.java

    import lombok.extern.slf4j.Slf4j;
    import org.apache.ibatis.session.SqlSession;
    import org.apache.ibatis.session.SqlSessionFactory;
    import org.apache.ibatis.session.SqlSessionFactoryBuilder;
    import org.apache.learning.helloworld.user.User;
    import org.apache.learning.helloworld.user.UserMapper;
    import org.junit.jupiter.api.Test;
    import java.io.InputStream;
    @Slf4j
    public class HelloWorldTest {
        /**
         * Mybatis全局配置文件
         */
        private static final String GLOBAL_CONFIG_FILE = "org/apache/learning/helloworld/hello-world.xml";
    
        @Test
        public void helloWord() {
            // 获取Mybatis会话工厂的建造器
            SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
            // 创建Mybatis的会话工厂
            SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(loadStream(GLOBAL_CONFIG_FILE));
    
            try (
                    // 通过SqlSessionFactory获取一个SqlSession
                    SqlSession sqlSession = sqlSessionFactory.openSession();
            ) {
                // 从mybatis中获取UserMapper的代理对象
                UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
                final int id = 1;
                final String name = "jpanda";
    
                log.debug("在数据库新增了用户数据:id={},name={}", id, name);
                Integer updateCount = userMapper.insert(id, name);
                assert updateCount == 1;
    
                // 查询该用户
                User user = userMapper.getById(id);
                log.debug("获取数据用户id为{}的用户数据为:{}", id, user);
                // 回滚
                sqlSession.rollback();
            }
        }
    
        /**
         * 获取指定文件的输入流
         *
         * @param path 文件地址
         * @return 输入流
         */
        private InputStream loadStream(String path) {
            return HelloWorldTest.class.getClassLoader().getResourceAsStream(path);
        }
    }
    

执行HelloWorldTesthelloWord方法,不出意外的话,控制台将会打印出下列内容:

DEBUG [main] - 在数据库新增了用户数据:id=1,name=jpanda
DEBUG [main] - ==>  Preparing: insert into user values(?,?);
DEBUG [main] - ==> Parameters: 1(Integer), jpanda(String)
DEBUG [main] - <==    Updates: 1
DEBUG [main] - ==>  Preparing: SELECT * FROM user WHERE id=?;
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
DEBUG [main] - 获取数据用户id为1的用户数据为:User(id=1, name=jpanda)

在上面的代码中,我们实现了一个简单的demo来演示Mybatis的使用方法。

先通过Mybatis的主要配置文件初始化一个SqlSessionFactory的实例,之后通过SqlSessionFactory来获取一个SqlSession,然后在这个SqlSession中获取我们想要执行数据库操作的接口的实例,进而执行数据库操作。

在这个Demo中,一共涉及到五个文件:

  • User.java是一个简单的实体类,他对应着数据库mybatis中的表User,像这种对应着数据库表的实体类,我们通常称之为PO(Persistent Object),即持久化对象。
  • UserMapper.java是一个接口,它定义了操作user表的方法,像这种定义了数据库操作方法的接口,我们通常称之为DAO(Data Access Object),即数据库访问对象。
  • UserMapper.xml对应着UserMapper.java接口,它给出了在UserMapper.java中定义的方法对应的SQL操作。
  • hello-world.xml是Mybatis的全局配置文件,它定义了mybatis的基本配置。
  • HelloWorldTest.java是一个单元测试类,也是我们本次演示代码的入口所在。

HelloWorldTesthelloWord方法中,我把所有代码分为两个阶段:

  • 第一个阶段是:mybatis运行环境的准备阶段

    在这个阶段,我们创建了一个SqlSessionFactoryBuilder对象的实例,并利用该实例来处理Mybatis全局配置文件hello-world.xml,最终生成了一个SqlSessionFactory对象.

    // 获取Mybatis会话工厂的建造器
    SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
    // 创建Mybatis的会话工厂
    SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(loadStream(GLOBAL_CONFIG_FILE));
    
  • 第二个阶段是:mybatis功能使用阶段

    在这个阶段,我们通过上一阶段获取到的SqlSessionFactory创建了一个SqlSession对象的实例,然后通过该实例获取到UserMapper对象来执行具体的数据库操作。

    try (
                // 通过SqlSessionFactory获取一个SqlSession
                SqlSession sqlSession = sqlSessionFactory.openSession();
        ) {
    
            // 从mybatis中获取UserMapper的代理对象
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
            final int id = 1;
            final String name = "jpanda";
    
            log.debug("在数据库新增了用户数据:id={},name={}", id, name);
            Integer updateCount = userMapper.insert(id, name);
            assert updateCount == 1;
    
            // 查询该用户
            User user = userMapper.getById(id);
            log.debug("获取数据用户id为{}的用户数据为:{}", id, user);
            // 回滚
            sqlSession.rollback();
        }
    

这两个阶段涉及到的java对象,主要就是SqlSession,SqlSessionFactory以及SqlSessionFactoryBuilder这三个类,现在我们可能还不了解这三个类,但是没关系,下面我们依次来学习这三个类。

SqlSession

其中,SqlSession是Mybatis提供给用户进行数据库操作的顶层API接口,它定义了操作数据库的方法并提供了操纵事务的能力,在Mybatis框架中,一定程度上我们可以认为SqlSession对象用于取代JDBCConnection对象。

区别于Connection对象,SqlSession对象除了封装了JDBC操作之外,还提供了一些其他好玩的东西,比如他有一个getMapper(Class)方法,这个方法就很有意思,你传给他一个DAO操作接口的类型,他还给你一个对应的实体对象:

// 从mybatis中获取UserMapper的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

之后你调用获取到的对象的方法的时候,就自带了数据库操作的效果:

  log.debug("在数据库新增了用户数据:id={},name={}", id, name);
  Integer updateCount = userMapper.insert(id, name);
  // 查询该用户
  User user = userMapper.getById(id);
  log.debug("获取数据用户id为{}的用户数据为:{}", id, user);

操作日志:

DEBUG [main] - 在数据库新增了用户数据:id=1,name=jpanda
DEBUG [main] - ==>  Preparing: insert into user values(?,?);
DEBUG [main] - ==> Parameters: 1(Integer), jpanda(String)
DEBUG [main] - <==    Updates: 1
DEBUG [main] - ==>  Preparing: SELECT * FROM user WHERE id=?;
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <==      Total: 1
DEBUG [main] - 获取数据用户id为1的用户数据为:User(id=1, name=jpanda)

这种以操作java对象的形式来操作数据库的能力是mybatis的核心功能,那么这个看起来灰常六的功能是如何实现的呢?

该功能的实现其实很复杂,在后续的学习过程中,我们会逐渐搞懂该问题,不过,就目前简单来说,该功能的实现依托了一种常用的设计模式——代理模式。

代理模式是一种结构型设计模式,他定义一个代理对象取代被代理对象暴露给被代理对象的调用方,该代理对象在调用方和被代理对象之间起作用,以便于控制对真实对象的访问以及执行其他操作。

在这里mybatis为UserMapper创建了一个代理对象,该代理对象拦截用户对UserMapper的方法调用,并执行被拦截方法对应的JDBC操作,最后将处理后的JDBC数据转换为java对象返回给方法的调用方。

SqlSessionFactory

SqlSessionFactory对象是mybatis的核心对象,他的主要作用就是创建SqlSession对象。

其实从该类的名称上我们不难发现,他也是一种设计模式的体现——工厂方法模式。

工厂方法模式是一种常见的创建型设计模式,工厂方法模式定义一个创建对象的工厂接口,将具体的创建工作延迟到其子类中完成,工厂方法模式的应用场景主要有两种:

1.在编写软件的过程中,当发现有大量的对象需要创建,并且这些对象具有共同的接口时, 此时就可以考虑使用工厂模式.

2.在编写软件的过程中,发现有多个同类型的操作, 他们在流程上具有一致性,只有细节上有些许差别,此时可以考虑使用工厂模式来实现.

SqlSessionFactory作为SqlSession对象的创建工厂,他同时满足了工厂方法的两种应用场景,他不仅提供了创建SqlSession对象的方法,还有针对性的提供了该方法的多种重载形式,用来定制化的创建SqlSession对象实例。

同时在SqlSessionFactory中还定义了一个getConfiguration()方法用于获取Configuration对象。

Configuration对象也是mybatis的核心对象,它几乎存放了mybatis中所有的配置信息,在接下来学习mybatis的过程中,我们会经常和该类打交道,而且很快我们就会碰到该类,因此这里我们先跳过不谈,继续看负责创建SqlSessionFactory对象的SqlSessionFactoryBuilder类,

SqlSessionFactoryBuilder

怎么说呢,在我们常用的23种设计模式中,有一种设计模式叫做建造者模式,他通常用于构建复杂对象,很巧的是,在这里SqlSessionFactoryBuilder就是建造者模式的一种简单实现。

建造者模式(生成器模式)也是一种创建型设计模式,他可以把一个复杂对象的创建过程抽象出来,以实现通过不同的创建过程的实现可以获取到不同表现形式的对象。

SqlSessionFactoryBuilder就是一个很典型的建造者模式的应用,他定义了一个build的方法用于创建SqlSessionFactory对象,同时为build提供了多种不同的重载方法,这些方法根据入参的不同,构造SqlSessionFactory对象的过程或者结果也有些许的不同,他这种表现形式刚好吻合了建造者模式定义中的通过不同的创建过程的实现来获取不同表现形式的对象

其实对于SqlSessionFactory对象的创建过程并不复杂,复杂的是创建SqlSessionFactory依赖的用于存放mybatis配置信息的Configuration对象,SqlSessionFactoryBuilder中的大部分build方法也都是先获取Configuration对象的实现类,最后通过Configuration对象来完成SqlSessionFactory的实例化工作。

Configuration对象的创建是一个很复杂的操作,我们很快就会接触到,在此之前,我们先看一下SqlSession,SqlSessionFactory以及SqlSessionFactoryBuilder的类图:

在类图中又引入了两个新的类:DefaultSqlSessionFactoryDefaultSqlSession,这两个类分别是SqlSessionFactorySqlSession接口的默认实现,了解一下就行,因为待会我们会提到他们。

现在,我们再回过头来看HelloWorldTesthelloWord方法,可能又会有一些新的感受:

原始代码,下面会有详细的解析:

// 获取Mybatis会话工厂的建造器
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();
// 创建Mybatis的会话工厂
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(loadStream(GLOBAL_CONFIG_FILE));

try (
        // 通过SqlSessionFactory获取一个SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
) {
    // 从mybatis中获取UserMapper的代理对象
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

    final int id = 1;
    final String name = "jpanda";

    log.debug("在数据库新增了用户数据:id={},name={}", id, name);
    Integer updateCount = userMapper.insert(id, name);
    assert updateCount == 1;

    // 查询该用户
    User user = userMapper.getById(id);
    log.debug("获取数据用户id为{}的用户数据为:{}", id, user);
    // 回滚
    sqlSession.rollback();
}

这里是租后一行老是判断不准啊。这就很难受了 首先,我们手动创建了一个SqlSessionFactoryBuilder对象:

// 获取Mybatis会话工厂的建造器
SqlSessionFactoryBuilder sqlSessionFactoryBuilder = new SqlSessionFactoryBuilder();

创建该对象的目的很明确,就是用它来处理mybatis配置文件的输入流以便于获取SqlSessionFactory对象实例,因此代码的表现形式就是:

// 创建Mybatis的会话工厂
SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBuilder.build(loadStream(GLOBAL_CONFIG_FILE));

在这里sqlSessionFactoryBuilder默认创建的是DefaultSqlSessionFactory实例.

在获取到了SqlSessionFactory对象实例之后,我们就要通过该实例来获取一个SqlSession对象的实例。

// 通过SqlSessionFactory获取一个SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();

在这里DefaultSqlSessionFactory创建的是DefaultSqlSession实例。

在拿到SqlSession实例之后,我们就可以通过SqlSessiongetMapper(Class)方法来为UserMapper接口创建一个代理实例。

// 从mybatis中获取UserMapper的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

接下来就可以通过UserMapper的代理对象来完成JDBC操作了。

final int id = 1;
final String name = "jpanda";

log.debug("在数据库新增了用户数据:id={},name={}", id, name);
Integer updateCount = userMapper.insert(id, name);
assert updateCount == 1;

// 查询该用户
User user = userMapper.getById(id);
log.debug("获取数据用户id为{}的用户数据为:{}", id, user);
// 回滚
sqlSession.rollback();

到这里,示例代码也看完了,再回头看看这张类图,在脑子里梳理一下他们的关系,思路是不是更清晰了呢?