Mybatis解析(一):MyBatis 是如何执行的 ?

MyBatis 是一款在持久层使用的 SQL映射框架,是对 JDBC 做了轻量级的封装。通过 XML/注解 的方式,完成数据库记录到 Java 实体的映射,可以灵活地控制 SQL 语句的构造,将 SQL 语句的编写和程序的运行分离开,使用更加便捷。 本文从 JDBC API 入手,从 MyBatis 源码的角度分析 MyBatis 配置、Mapper 绑定过程、SqlSession 操作数据库原理。

1 JDBC API 是什么?

JDBC (Java Database Connectivity)是 Java 语言中提供的访问关系型数据的接口。在 Java 编写的应用中,使用 JDBC API 可以执行 SQL 语句、检索 SQL 执行结果以及将数据更改写回到底层数据源。JDBC API 为 Java 程序提供了访问一个或多个数据源的能力。大多数情况下,数据源是关系型数据库,它的数据是通过 SQL 语句来访问的。 使用 JDBC 操作数据源的一般步骤:

  1. 与数据源建立连接;
  2. 执行 SQL 语句;
  3. 检索 SQL 执行结果;
  4. 关闭连接。

下面是一个 JDBC 的使用示例:

public void testJdbc() {
    try {
        // 加载驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        // 获取Connection对象
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");
        Statement statement = connection.createStatement();
        ResultSet resultSet = statement.executeQuery("select * from user");
        // 结果集的元信息
        ResultSetMetaData metaData = resultSet.getMetaData();
        // 结果集数据列的数量
        int columCount = metaData.getColumnCount();
        // 遍历查询结果 ResultSet
        while (resultSet.next()) {
            for (int i = 1; i <= columCount; i++) {
                String columName = metaData.getColumnName(i);
                Object columVal = resultSet.getObject(columName);
                System.out.println(columName + ":" + columVal);
            }
            System.out.println("--------------------------------------");
        }
        // 关闭连接
        statement.close();
        connection.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
复制代码

在上面的示例中,涉及到了JDBC 开发中的几个常用组件 Connection、Statement、ResultSet,他们之间的关系如下图所示:

1.1 Connection

一个 Connection 对象表示通过 JDBC 驱动与数据源建立的连接,这里的数据源可以是关系型数据库管理系统(DBMS)、文件系统或者其他通过 JDBC 驱动访问的数据,从JDBC 驱动的角度来看,Connection 对象表示客户端会话。 我们可以通过两种方式获取 JDBC 中的 Connection 对象:

  1. 通过 JDBC API 中提供的 DriverManager 类获取;
  2. 通过 DataSource 接口的实现类获取。

上文示例中已经介绍了 DriverManager 的用法,下面的示例介绍 DataSource 获取 Connection 的方式:

// 创建 DataSource 工厂
DataSourceFactory dsf = new UnpooledDataSourceFactory();
Properties properties = new Properties();
// 获取配置文件
InputStream configStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("database.properties");
properties.load(configStream);
dsf.setProperties(properties);
// 创建 DataSource 实例
DataSource dataSource = dsf.getDataSource();
// 获取Connection对象
Connection connection = dataSource.getConnection();
复制代码

database.properties

driver=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/test
username=root
password=root
复制代码

1.2 Statement

Statement 接口及它的子接口 PreparedStatement 和 CallableStatement 是JDBC API 中比较重要的部分。

  • Statement 接口中定义了执行 SQL 语句的方法,这些方法不支持参数输入;
  • PreparedStatement 接口中增加了设置 SQL 参数的方法;
  • CallableStatement 接口继承自 PreparedStatement,在此基础上增加了调用存储过程以及检索存储过程调用结果的方法。

上文介绍了 Statement 的用法,下面介绍 PreparedStatement 的用法:

@Test
public void testPreparedStatement() {
    try {
        DataSourceFactory dsf = new UnpooledDataSourceFactory();
        Properties properties = new Properties();
        InputStream configStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("database.properties");
        properties.load(configStream);
        dsf.setProperties(properties);
        DataSource dataSource = dsf.getDataSource();
        // 获取Connection对象
        Connection connection = dataSource.getConnection();
        PreparedStatement stmt = connection.prepareStatement("select * from user where id = ? ");
        // 指定占位符对应的参数,即 id = 1
        stmt.setInt(1, 0);
        stmt.execute();
        ResultSet resultSet = stmt.getResultSet();
        // 遍历ResultSet
        ResultSetMetaData metaData = resultSet.getMetaData();
        int columCount = metaData.getColumnCount();
        while (resultSet.next()) {
            for (int i = 1; i <= columCount; i++) {
                String columName = metaData.getColumnName(i);
                String columVal = resultSet.getString(columName);
                System.out.println(columName + ":" + columVal);
            }
            System.out.println("----------------------------------------");
        }
        stmt.close();
        connection.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
复制代码

1.3 ResultSet

表示数据库结果集的数据表,通常通过执行查询数据库的语句生成,ResultSet 对象具有指向其当前数据行的光标。最初,光标被置于第一行之前;next() 方法将光标移动到下一行;因为该方法在 ResultSet 对象没有下一行时返回 false,所以可以在 while 循环中使用它来迭代结果集,具体使用方式可参考上面的示例代码。

1.4 DatabaseMetaData

DatabaseMetaData 接口是由 JDBC 驱动程序实现的,用于提供底层数据源相关的信息。该接口主要用于为应用程序或工具确定如何与底层数据源交互,应用程序也可以使用 DatabaseMetaData 接口提供的方法获取数据源信息。

@Test
public void testDbMetaData() {
    try {
        DataSourceFactory dsf = new UnpooledDataSourceFactory();
        Properties properties = new Properties();
        InputStream configStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("database.properties");
        properties.load(configStream);
        dsf.setProperties(properties);
        DataSource dataSource = dsf.getDataSource();
        Connection conn = dataSource.getConnection();
        DatabaseMetaData dmd = conn.getMetaData();
        System.out.println("数据库URL:" + dmd.getURL());
        System.out.println("数据库用户名:" + dmd.getUserName());
        System.out.println("数据库产品名:" + dmd.getDatabaseProductName());
        System.out.println("数据库产品版本:" + dmd.getDatabaseProductVersion());
        System.out.println("驱动主版本:" + dmd.getDriverMajorVersion());
        System.out.println("驱动副版本:" + dmd.getDriverMinorVersion());
        System.out.println("数据库供应商用于schema的首选术语:" + dmd.getSchemaTerm());
        System.out.println("数据库供应商用于catalog的首选术语:" + dmd.getCatalogTerm());
        System.out.println("数据库供应商用于procedure的首选术语:" + dmd.getProcedureTerm());
        System.out.println("null值是否高排序:" + dmd.nullsAreSortedHigh());
        System.out.println("null值是否低排序:" + dmd.nullsAreSortedLow());
        System.out.println("数据库是否将表存储在本地文件中:" + dmd.usesLocalFiles());
        System.out.println("数据库是否为每个表使用一个文件:" + dmd.usesLocalFilePerTable());
        System.out.println("数据库SQL关键字:" + dmd.getSQLKeywords());
        IOUtils.closeQuietly(conn);
    } catch (Exception e) {
        e.printStackTrace();
    }
}
复制代码

2 MySQL Driver 的加载流程

2.1 JDBC Driver 的注册与数据库连接

所有的 JDBC 驱动都必须实现 Driver 接口,而且实现类必须包含一个静态初始化代码块。类的静态初始化代码块会在类初始化时调用,驱动实现类需要在静态初始化代码块中向 DriverManager 注册自己的一个实例:

static {
    try {
        java.sql.DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}
复制代码

当调用 Class.forName("com.mysql.cj.jdbc.Driver")时,会触发该类的类加载,类加载的过程中就会执行该类的静态代码块,从而将com.mysql.cj.jdbc.Driver的一个实例注册到DriverManager中。 通过 DriverManager.getConnection() 获取连接时,会遍历注册的 Driver,找到正确的 Driver

2.2 JDBC Driver 的 SPI 机制

在 JDBC 4.0 版本之前,使用 DriverManager 获取 Connection 对象之前都需要通过代码显式地加载驱动实现类,例如:

Class.forName("com.mysql.cj.jdbc.Driver")
复制代码

JDBC 4.0 之后的版本对此做了改进,我们不再需要显式地加载驱动实现类。通过 Java 中的 SPI 机制,实现 Driver 的自动注册。SPI(Service Provider Interface) 是 JDK 内置的一种服务提供发现机制。SPI是一种动态替换发现的机制,比如有一个接口,想在运行时动态地给它添加实现,只需要添加一个实现,SPI 机制在程序运行时就会发现该实现类,整体流程如: 当服务的提供者提供了一种接口的实现之后,需要在 classpath 下的 META-INF/services 目录中创建一个以服务接口命名的文件,这个文件中的内容就是这个接口具体的实现类。当其他的程序需要这个服务的时候,就可以查找这个 JAR 包中 META-INF/services 目录的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名加载服务实现类,然后就可以使用该服务了。 DirverManager 中加载 classpath 下,所有实现了java.sql.Driver接口的实现类; com.mysql.cj.jdbc.Driver的 jar 包,在META-INF/services/java.sql.Driver文件中指定com.mysql.cj.jdbc.Driver为该服务的实现类:

3 Mybatis 是如何执行的?

3.1 Mybatis 之初体验

添加 MyBatis 主配置文件:resource/myabtis/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>
  <settings>
    <setting name="useGeneratedKeys" value="true"/>
    <setting name="mapUnderscoreToCamelCase" value="true"/>
  </settings>

  <environments default="dev" >
    <environment id="dev">
      <transactionManager type="JDBC">
        <property name="" value="" />
      </transactionManager>
      <dataSource type="UNPOOLED">
        <property name="driver" value="com.mysql.cj.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/test" />
        <property name="username" value="root" />
        <property name="password" value="root" />
      </dataSource>
    </environment>
  </environments>

  <mappers>
    <mapper resource="com/example/learn/mybatis/mapper/UserMapper.xml"/>
  </mappers>
</configuration>
复制代码

接口配置文件:com/example/learn/mybatis/mapper/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="com.example.learn.mybatis.component.UserMapper">
    <sql id="userAllField">
        id,create_time, name, password, phone, nick_name
    </sql>

    <select id="listAllUser"  resultType="com.example.learn.mybatis.component.User" >
        select
        <include refid="userAllField"/>
        from user
    </select>

    <select id="getUserByEntity"  resultType="com.example.learn.mybatis.component.User">
        select
        <include refid="userAllField"/>
        from user
        <where>
            <if test="id != null">
                AND id = #{id}
            </if>
            <if test="name != null">
                AND name = #{name}
            </if>
            <if test="phone != null">
                AND phone = #{phone}
            </if>
        </where>
    </select>

    <select id="getUserByPhone" resultType="com.example.learn.mybatis.component.User">
        select
        <include refid="userAllField"/>
        from user
        where phone = ${phone}
    </select>
</mapper>
复制代码

定义接口 UserMapper

public interface UserMapper {

    List<User> listAllUser();

    @Select("select * from user where id=#{userId,jdbcType=INTEGER}")
    User getUserById(@Param("userId") Integer userId);

    List<User> getUserByEntity(User user);

    User getUserByPhone(@Param("phone") String phone);

}
复制代码

查询示例:

@Test
public  void testMybatis() throws IOException {
    // 获取配置文件输入流
    InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml");
    // 通过SqlSessionFactoryBuilder的build()方法创建SqlSessionFactory实例
    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    // 调用openSession()方法创建SqlSession实例
    SqlSession sqlSession = sqlSessionFactory.openSession();
    // 获取UserMapper代理对象
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

    // 执行Mapper方法,获取执行结果
    List<User> userList = userMapper.listAllUser();
    System.out.println(JSON.toJSONString(userList));

    // 兼容 Ibatis 用法,通过 Mapper Id 执行SQL操作
    List<User> userList2 = sqlSession.selectList(
        "com.example.learn.mybatis.component.UserMapper.listAllUser");
    System.out.println(JSON.toJSONString(userList2));
}
复制代码

SqlSessionMyBatis 中提供的与数据库交互的接口,SqlSession 实例通过工厂模式创建。为了创建 SqlSession 对象,首先需要创建 SqlSessionFactory 对象,而 SqlSessionFactory 对象的创建依赖于 SqlSessionFactoryBuilder 类的 build() 方法,以主配置文件的输入流作为参数调用 bulid() 方法,该方法返回一个 SqlSessionFactory 对象。有了 SqlSessionFactory 对象之后,调用 SqlSessionFactory 对象的 openSession() 方法即可获取一个与数据库建立连接的 SqlSession 实例。最后调用 SqlSessiongetMapper() 方法创建一个动态代理对象,然后调用 UserMapper 代理实例的方法即可完成与数据库的交互。

3.2 Mybatis 核心组件

3.2.2 核心组件操纵数据库流程

使用 MyBatis 的核心组件操作数据库的过程:

  1. 在上面3.1节 Mybatis 基本使用中,我们使用到了 SqlSession 组件,它是用户层面的 API;
  2. 实际上SqlSessionExecutor 组件的外观,真正执行 SQL 操作的是 Executor 组件,Executor 可以理解为 SQL 执行器,它会使用 StatementHandler 组件对 JDBC 的 Statement 对象进行操作;
  3. 当 Statement 类型为 CallableStatementPreparedStatement 时,会通过 ParameterHandler 组件为参数占位符赋值;
  4. ParameterHandler 组件中会根据 Java 类型找到对应的 TypeHlandler 对象,TypeHandler 中会通过 Statement 对象提供的 setXXX()方法 (例如,setString()方法) 为 Statement 对象中的参数占位符设置值;
  5. StatementHandler 组件使用 JDBC 中的 Statement 对象与数据库完成交互后,当 SQL 语句类型为 SELECT 时,MyBatis 通过 ResultSetHandler 组件从 Statement 对象中获取 ResultSet 对象,然后将 ResultSet 对象转换为 Java 对象。

3.2.2 核心组件详解

  • Configuration: 用于描述 MyBatis 的主配置信息,其他组件需要获取配置信息时,直接通过 Configuration 对象获取。除此之外,MyBatis 在应用启动时,将 Mapper 配置信息、类型别名、TypeHandler 等注册到 Configuration 组件中,其他组件需要这些信息时,也可以从 Configuration 对象中获取。
  • MappedStatement: MappedStatement 用于描述 Mapper 中的 SQL 配置信息,是对 Mapper XML 配置文件中 <seleet | update | delete | insert> 等标签或者 @Select/@Update 等注解配置信息的封装。
  • SqlSession: SqlSessionMyBatis 提供的面向用户的 API,表示和数据库交互时的会话对象,用于完成数据库的增删改查功能。SqlSessionExecutor 组件的外观,目的是对外提供易于理解和使用的数据库操作接口。
  • Executor:MyBatis 的 SQL 执行器,MyBatis 中对数据库所有的增删改查操作都是由 Executor 组件完成的。
  • StatementHandler: StatementHandler 封装了对 JDBC Statement 对象的操作,比如为 Statement 对象设置参数,调用 Statement 接口提供的方法与数据库交互等。
  • ParameterHandler:当 MyBatis 框架使用的 Statement 类型为 CallableStatementPreparedStatement 时,ParameterHandler 用于为 Statement 对象参数占位符设置值。
  • ResultSetHandler: ResultSetHandler 封装了对 JDBC 中的 ResultSet 对象操作,当执行 SQL类型为 SELECT 语句时,ResultSetHandler 用于将查询结果转换成 Java 对象。
  • TypeHandler: TypeHandlerMyBatis 中的类型处理器,用于处理 Java 类型与 JDBC 类型之间的映射。它的作用主要体现在能够根据 Java 类型调用 PreparedStatementCallableStatement 对象对应的 setXXX() 方法为 Statement 对象设置值,而且能够根据 Java 类型调用 ResultSet 对象对应的 getXXX() 获取 SQL 执行结果。

3.3 SqlSession 的创建过程

上文提到 SqlSession 对象表示 MyBaits 框架与数据库建立的会话,我们可以通过 SqlSession 实例完成对数据库的增删改查操作,SqlSession 的创建过程拆解为3个阶段:

  1. Configuration 实例的创建过程;
  2. SqlSessionFactory 实例的创建过程;
  3. SqlSession 实例化的过程。

3.3.1 XPath 解析 xml 配置文件

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
  try {
    // 内部使用 XPathParser 解析主配置文件和mapper配置文件
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    // parser.parse() 返回 Configuration 对象,传给build() 方法
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      inputStream.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}
复制代码
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
  this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}
复制代码

3.3.2 Configuration 对象的创建

ConfigurationMyBatis 中比较重要的组件,主要有以下3个作用:

  1. 用于描述 MyBatis 配置信息,例如 <settings> 标签配置的参数信息;
  2. 作为容器注册 MyBatis 其他组件,例如 TypeHandlerMappedStatement 等;
  3. 提供工厂方法,创建 ResultSetHandlerStatementHandlerExecutorParameterHandler 等组件实例。

parser.parse() 方法:

public Configuration parse() {
  if (parsed) {
    throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed = true;
  // 解析mybatis/mybatis-config.xml的各个节点元素
  parseConfiguration(parser.evalNode("/configuration"));
  // 返回 Configuration 对象
  return configuration;
}
复制代码

解析 mybatis/mybatis-config.xml 主配置文件的各个节点元素:

3.3.3 创建 SqlSession 实例

DefaultSqlSessionFactory.openSession() 方法:

public SqlSession openSession() {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}
复制代码
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    // 获取MyBatis主配置文件的环境信息
    final Environment environment = configuration.getEnvironment();
    // 创建事务管理器工厂
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    // 创建事管理器
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    // 根据 MyBatis 主配置文件中指定的 Executor 类型创建对应的 Executor 实例
    final Executor executor = configuration.newExecutor(tx, execType);
    // 创建 DefaultSqlSession 实例
    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();
  }
}
复制代码

3.4 SqSession 执行 Mapper 过程

Mapper 由两部分组成,分别为 Mapper 接口和通过注解或者 XML 文件配置的 SQL 语句。SqlSession 执行 Mapper 过程拆解为4部分介绍:

  1. Mapper 接口的注册过程;
  2. MappedStatement 对象的注册过程;
  3. Mapper 方法的调用过程;
  4. SqlSession 执行 Mapper 的过程。

3.4.1 Mapper 接口的注册过程

在上文 3.1 节中,我们通过如下方法获取 UserMapper 实例:

// 获取UserMapper代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.listAllUser();
复制代码

UserMapper 是一个接口,我们调用 SqlSession 对象 getMapper() 返回的到底是什么呢?我们知道,接口中定义的方法必须通过某个类实现该接口,然后创建该类的实例,才能通过实例调用方法。所以 SqlSession 对象的 getMapper() 方法返回的一定是某个类的实例。具体是哪个类的实例呢?实际上 getMapper()方法返回的是一个动态代理对象(InvocationHandler 为 MapperProxy):

public <T> T getMapper(Class<T> type) {
  // 调用 Configuration 实例方法 getMapper 获取 Mapper 接口的代理对象
  return configuration.getMapper(type, this);
}
复制代码
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  // 调用 Configuration 实例的 mapperRegistry 成员的 getMapper 方法
  return mapperRegistry.getMapper(type, sqlSession);
}
复制代码

mapperRegistry.getMapper()方法:

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  // knownMappers 维护了 Mapper 接口与 MapperProxyFactory 的对应关系
  // MapperProxyFactory 是 MapperProxy 的工厂
  // MapperProxy 是创建 Mapper 接口代理对象的 InvocationHandler
  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);
  }
}
复制代码

mapperRegistry 中,knownMappers 维护了 Mapper 接口与 MapperProxyFactory 的对应关系,每个 Mapper 接口对应一个 MapperProxyFactory 对象,由 MapperProxyFactory 生成其动态代理对象:

// MapperProxyFactory.newInstance(SqlsqlSession) 方法
public T newInstance(SqlSession sqlSession) {
  // MapperProxy 实现了 InvocationHandler,代理了对 Mapper 接口的方法调用
  // Mapper 接口 和 MapperProxy 一一对应
  final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
  return newInstance(mapperProxy);
}

// MapperProxyFactory.newInstance(MapperProxy) 方法
protected T newInstance(MapperProxy<T> mapperProxy) {
  // 生成 Mapper 接口的动态代理对象
  return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
复制代码

如上面的代码所示,MapperRegistry 类有一个 knownMappers 属性,用于注册 Mapper 接口和 MapperProxyFactory 对象之间的关系。另外,MapperRegistry 提供了 addMapper() 方法,用于向 knownMappers 属性中注册 Mapper 接口信息。在 addMapper() 方法中,为每个 Mapper 接口创建一个 MapperProxyFactory 对象,然后添加到 knownMappers 属性中。 MyBatis 框架在应用启动时会解析所有的 Mapper 接口,然后调用 MapperRegistry 对象的addMapper() 方法将 Mapper 接口信息和对应的 MapperProxyFactory 对象注册到 MapperRegistry 对象中。

3.4.2 MappedStatement 对象的注册过程 (实现接口方法)

MappedStatement 组件的作用,可参考 3.2 节。 MyBatis 通过 MappedStatement 类描述 Mapper 的 SQL 配置信息。SQL 配置有两种方式:一种是通过 XML 文件配置;另一种是通过 Java 注解,而 Java 注解的本质就是一种轻量级的配置信息。 Configuration.mappedStatements 属性用于注册 MyBatis 中所有的 MappedStatement 对象,mappedStatements 属性是一个 Map 对象,它的 Key 为 Mapper SQL 配置的 id,如果 SQL 是通过 XML 配置的,则 id 为命名空间加上 <select | update | delete | insert> 标签的 id,如果 SQL 通过 Java 注解配置,则 id 为 Mapper 接口的完全限定名加上方法名称。 注册 MapperStatement 的在上文提到的创建 ConfigurationparseConfiguration() 方法中: 解析 <select | update | delete | insert> 节点,生成 MapperStatement: 在 parseStatementNode() 中通过 MapperBuilderAssistantMpperStatement 加到 Configuration 中:

3.4 3 Mapper方法的调用过程

本节将介绍 Mapper 方法的执行过程以及 Mapper 接口与 Mapper SQL 配置是如何进行关联的。 上文提到,为了执行 Mapper 接口中定义的方法,我们首先需要调用 SqlSession 对象的 getMapper() 方法获取一个动态代理对象,然后通过代理对象调用方法即可,其代理逻辑在 MapperProxy 类中,代码如下:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    if (Object.class.equals(method.getDeclaringClass())) {
      // 继承自Object的方法,直接执行原有方法
      return method.invoke(this, args);
    } else {
      // 找到 Mapper 接口方法对应的 MapperMethod,并调用
      return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}
复制代码
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
  try {
    // A workaround for https://bugs.openjdk.java.net/browse/JDK-8161372
    // It should be removed once the fix is backported to Java 8 or
    // MyBatis drops Java 8 support. See gh-1929
    MapperMethodInvoker invoker = methodCache.get(method);
    if (invoker != null) {
      return invoker;
    }

    return methodCache.computeIfAbsent(method, m -> {
      if (m.isDefault()) {
        // 接口默认方法的包装
        try {
          if (privateLookupInMethod == null) {
            return new DefaultMethodInvoker(getMethodHandleJava8(method));
          } else {
            return new DefaultMethodInvoker(getMethodHandleJava9(method));
          }
        } catch (IllegalAccessException | InstantiationException | InvocationTargetException
            | NoSuchMethodException e) {
          throw new RuntimeException(e);
        }
      } else {
        // 接口方法对应的 MapperMethod,并用 PlainMethodInvoker 包装
        return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      }
    });
  } catch (RuntimeException re) {
    Throwable cause = re.getCause();
    throw cause == null ? re : cause;
  }
}
复制代码

其中,PlainMethodInvoker.invoke() 方法如下:

public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
  return mapperMethod.execute(sqlSession, args);
}
复制代码

可见,对 Mapper 接口方法的调用,最终转换成对表示该方法的 MapperMethod 对象的 execute() 的调用:

public class MapperMethod {
  private final SqlCommand command;
  private final MethodSignature method;

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }

  public Object execute(SqlSession sqlSession, Object[] args) {
      ...
  }
}
复制代码

看一下 MapperMethod 的两个成员:SqlCommandMethodSignature:

public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
  // 方法名称
  final String methodName = method.getName();
  // 方法所在的类,可能是mapperInterface,也可能是mapperInterface的子类
  final Class<?> declaringClass = method.getDeclaringClass();
  // 从Configuration对象中,通过构造id, 找到之前注册的该接口方法对应的 MapperStatement
  MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
      configuration);
  if (ms == null) {
    if (method.getAnnotation(Flush.class) != null) {
      name = null;
      type = SqlCommandType.FLUSH;
    } else {
      throw new BindingException("Invalid bound statement (not found): "
          + mapperInterface.getName() + "." + methodName);
    }
  } else {
    // MapperMethod 对应的 MapperStatement id
    name = ms.getId();
    // SQL 的类型,Insert、update ...
    type = ms.getSqlCommandType();
    if (type == SqlCommandType.UNKNOWN) {
      throw new BindingException("Unknown execution method for: " + name);
    }
  }
}
复制代码

SqlCommand 对象封装了 SQL 语句的类型和 Mapper 接口对应的 id。

public MethodSignature(Configuration configuration, Class<?> mapperInterface, Method method) {
  // Mapper Interface 方法的返回类型
  Type resolvedReturnType = TypeParameterResolver.resolveReturnType(method, mapperInterface);
  if (resolvedReturnType instanceof Class<?>) {
    this.returnType = (Class<?>) resolvedReturnType;
  } else if (resolvedReturnType instanceof ParameterizedType) {
    this.returnType = (Class<?>) ((ParameterizedType) resolvedReturnType).getRawType();
  } else {
    this.returnType = method.getReturnType();
  }
  this.returnsVoid = void.class.equals(this.returnType);
  this.returnsMany = configuration.getObjectFactory().isCollection(this.returnType) || this.returnType.isArray();
  this.returnsCursor = Cursor.class.equals(this.returnType);
  this.returnsOptional = Optional.class.equals(this.returnType);
  this.mapKey = getMapKey(method);
  this.returnsMap = this.mapKey != null;
  // rowBounds 参数位置索引
  this.rowBoundsIndex = getUniqueParamIndex(method, RowBounds.class);
  // resultHandler 参数位置索引
  this.resultHandlerIndex = getUniqueParamIndex(method, ResultHandler.class);
  // 用于解析 Mapper Interface 方法的参数
  this.paramNameResolver = new ParamNameResolver(configuration, method);
}
复制代码

MethodSignature 构造方法中做了 3 件事情:

  1. 获取 Mapper 方法的返回值类型,具体是哪种类型,通过 boolean 类型的属性进行标记。例如,当返回值类型为 void 时,returnsVoid 属性值为 true;
  2. 记录 RowBounds 参数位置,用于处理后续的分页查询,同时记录 ResultHandler 参数位用于处理从数据库中检索的每一行数据;
  3. 创建 ParamNameResolver 对象,用于解析 Mapper 方法中的参数名称及参数注解信息。

了解这两个成员后,最后看下 MapperProxyexecute() 方法:

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  // 根据SQL语句类型,执行不同操作
  switch (command.getType()) {
    case INSERT: {
      // 将参数顺序与实参对应好
      Object param = method.convertArgsToSqlCommandParam(args);
      // 执行操作并返回结果
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    case SELECT:
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {
        result = executeForCursor(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
        if (method.returnsOptional()
            && (result == null || !method.getReturnType().equals(result.getClass()))) {
          result = Optional.ofNullable(result);
        }
      }
      break;
    case FLUSH:	// 清空缓存语句
      result = sqlSession.flushStatements();
      break;
    default:
      throw new BindingException("Unknown execution method for: " + command.getName());
  }
  if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
    throw new BindingException("Mapper method '" + command.getName()
        + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
  }
  return result;
}
复制代码

SqlSession 的执行,借助 Executor 对象进行查询、结果转换,以 SqlSession.selectList() 为例继续往下追踪: 从上图可知,SqlSession 借助 CachingExecutor 进行底层查询,而 CachingExecutor 又借助 SimpleExecutorSimpleExecutor 继承自 BaseExecutorSimpleExecutor 负责真正的查询逻辑,而 CachingExecutor 提供了缓存的能力。

3.4.4 Mapper 方法的执行过程

这一节以 SELECT 语句为例,介绍 SqlSession 执行 Mapper 的过程。

public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
    throws SQLException {
  // 获取MappedStatement对应的缓存,可能的结果有:该命名空间的缓存、共享的其它命名空间的缓存、无缓存
  Cache cache = ms.getCache();
  // 如果映射文件未设置<cache>或<cache-ref>则,此处cache变量为null
  if (cache != null) { // 存在缓存
    // 根据要求判断语句执行前是否要清除二级缓存,如果需要,清除二级缓存
    flushCacheIfRequired(ms);
    if (ms.isUseCache() && resultHandler == null) { // 该语句使用缓存且没有输出结果处理器
      // 二级缓存不支持含有输出参数的CALLABLE语句,故在这里进行判断
      ensureNoOutParams(ms, boundSql);
      // 从缓存中读取结果
      @SuppressWarnings("unchecked")
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) { // 缓存中没有结果
        // 交给被包装的执行器执行
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        // 缓存被包装执行器返回的结果
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  // 交由被包装的实际执行器执行
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
复制代码
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  List<E> list;
  // 向缓存中增加占位符,表示正在查询
  localCache.putObject(key, EXECUTION_PLACEHOLDER);
  try {
    // 真正执行查询任务
    list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
  } finally {
    // 删除占位符
    localCache.removeObject(key);
  }
  // 将查询结果写入缓存
  localCache.putObject(key, list);
  if (ms.getStatementType() == StatementType.CALLABLE) {
    localOutputParameterCache.putObject(key, parameter);
  }
  return list;
}
复制代码
@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  try {
    Configuration configuration = ms.getConfiguration();
    // 通过 configuration 获取对应的 StatementHandler
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    // 转成 JDBC 规范的 Statement,用于执行查询
    stmt = prepareStatement(handler, ms.getStatementLog());
    // handler 借助 Statement 规范执行查询
    return handler.query(stmt, resultHandler);
  } finally {
    closeStatement(stmt);
  }
}
复制代码

此处 StatementHandlerRoutingStatementHandler,其内部根据 Statement 的类型调用,不同的 StatementHandler 实例执行,默认情况下为 PreparedStatementHandlerPreparedStatementHandler.query() 方法:

  public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    // 执行 PrepareStament
    ps.execute();
    // 获取 PrepareStament 执行后的结果集,并进行结果集转换
    return resultSetHandler.handleResultSets(ps);
  }
复制代码

4 总结

通过以下内容了解了 MyBatis 的执行原理:

  1. 如何使用 JDBC API 完成数据库的增删改查操作,并掌握了 JDBC API中一些组件,包括 Connection、Statement、 ResultSet 等;
  2. JDBC Driver 注册原理及Java SPI 机制;
  3. MyBatis 的核心组件以及这些组件之间的关系,了解 MyBatis 框架执行 SQL 语句的基本流程;
  4. MyBatis 配置、Mapper 绑定过程、SqlSession 操作数据库原理。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值