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 操作数据源的一般步骤:
- 与数据源建立连接;
- 执行 SQL 语句;
- 检索 SQL 执行结果;
- 关闭连接。
下面是一个 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 对象:
- 通过 JDBC API 中提供的 DriverManager 类获取;
- 通过 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));
}
复制代码
SqlSession
是 MyBatis
中提供的与数据库交互的接口,SqlSession
实例通过工厂模式创建。为了创建 SqlSession
对象,首先需要创建 SqlSessionFactory
对象,而 SqlSessionFactory
对象的创建依赖于 SqlSessionFactoryBuilder
类的 build()
方法,以主配置文件的输入流作为参数调用 bulid()
方法,该方法返回一个 SqlSessionFactory
对象。有了 SqlSessionFactory
对象之后,调用 SqlSessionFactory
对象的 openSession()
方法即可获取一个与数据库建立连接的 SqlSession
实例。最后调用 SqlSession
的 getMapper()
方法创建一个动态代理对象,然后调用 UserMapper
代理实例的方法即可完成与数据库的交互。
3.2 Mybatis 核心组件
3.2.2 核心组件操纵数据库流程
使用 MyBatis 的核心组件操作数据库的过程:
- 在上面3.1节 Mybatis 基本使用中,我们使用到了
SqlSession
组件,它是用户层面的 API; - 实际上
SqlSession
是Executor
组件的外观,真正执行 SQL 操作的是Executor
组件,Executor
可以理解为 SQL 执行器,它会使用StatementHandler
组件对 JDBC 的Statement
对象进行操作; - 当 Statement 类型为
CallableStatement
和PreparedStatement
时,会通过ParameterHandler
组件为参数占位符赋值; ParameterHandler
组件中会根据 Java 类型找到对应的 TypeHlandler 对象,TypeHandler
中会通过Statement
对象提供的 setXXX()方法 (例如,setString()方法) 为 Statement 对象中的参数占位符设置值;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:
SqlSession
是MyBatis
提供的面向用户的 API,表示和数据库交互时的会话对象,用于完成数据库的增删改查功能。SqlSession
是Executor
组件的外观,目的是对外提供易于理解和使用的数据库操作接口。 - Executor:
MyBatis
的 SQL 执行器,MyBatis 中对数据库所有的增删改查操作都是由Executor
组件完成的。 - StatementHandler:
StatementHandler
封装了对 JDBC Statement 对象的操作,比如为Statement
对象设置参数,调用Statement
接口提供的方法与数据库交互等。 - ParameterHandler:当
MyBatis
框架使用的Statement
类型为CallableStatement
和PreparedStatement
时,ParameterHandler
用于为Statement
对象参数占位符设置值。 - ResultSetHandler:
ResultSetHandler
封装了对 JDBC 中的ResultSet
对象操作,当执行 SQL类型为 SELECT 语句时,ResultSetHandler
用于将查询结果转换成Java
对象。 - TypeHandler:
TypeHandler
是MyBatis
中的类型处理器,用于处理 Java 类型与JDBC
类型之间的映射。它的作用主要体现在能够根据Java
类型调用PreparedStatement
或CallableStatement
对象对应的setXXX()
方法为Statement
对象设置值,而且能够根据 Java 类型调用ResultSet
对象对应的getXXX()
获取 SQL 执行结果。
3.3 SqlSession 的创建过程
上文提到 SqlSession
对象表示 MyBaits
框架与数据库建立的会话,我们可以通过 SqlSession
实例完成对数据库的增删改查操作,SqlSession
的创建过程拆解为3个阶段:
Configuration
实例的创建过程;SqlSessionFactory
实例的创建过程;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 对象的创建
Configuration
是 MyBatis
中比较重要的组件,主要有以下3个作用:
- 用于描述
MyBatis
配置信息,例如<settings>
标签配置的参数信息; - 作为容器注册
MyBatis
其他组件,例如TypeHandler
、MappedStatement
等; - 提供工厂方法,创建
ResultSetHandler
、StatementHandler
、Executor
、ParameterHandler
等组件实例。
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部分介绍:
Mapper
接口的注册过程;MappedStatement
对象的注册过程;Mapper
方法的调用过程;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
的在上文提到的创建 Configuration
的 parseConfiguration()
方法中: 解析 <select | update | delete | insert> 节点,生成 MapperStatement
: 在 parseStatementNode() 中通过 MapperBuilderAssistant
将 MpperStatement
加到 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
的两个成员:SqlCommand
、MethodSignature
:
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 件事情:
- 获取
Mapper
方法的返回值类型,具体是哪种类型,通过 boolean 类型的属性进行标记。例如,当返回值类型为 void 时,returnsVoid
属性值为 true; - 记录
RowBounds
参数位置,用于处理后续的分页查询,同时记录ResultHandler
参数位用于处理从数据库中检索的每一行数据; - 创建
ParamNameResolver
对象,用于解析 Mapper 方法中的参数名称及参数注解信息。
了解这两个成员后,最后看下 MapperProxy
的 execute()
方法:
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
又借助 SimpleExecutor
,SimpleExecutor
继承自 BaseExecutor
,SimpleExecutor
负责真正的查询逻辑,而 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);
}
}
复制代码
此处 StatementHandler
为 RoutingStatementHandler
,其内部根据 Statement
的类型调用,不同的 StatementHandler
实例执行,默认情况下为 PreparedStatementHandler
。 PreparedStatementHandler.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 的执行原理:
- 如何使用 JDBC API 完成数据库的增删改查操作,并掌握了 JDBC API中一些组件,包括 Connection、Statement、 ResultSet 等;
- JDBC Driver 注册原理及Java SPI 机制;
- MyBatis 的核心组件以及这些组件之间的关系,了解 MyBatis 框架执行 SQL 语句的基本流程;
- MyBatis 配置、Mapper 绑定过程、SqlSession 操作数据库原理。