MyBatis 入门教程之七

大纲

MyBatis 四大对象

四大对象介绍

MyBatis 的四大对象包括:Executor、StatementHandler、ParameterHandler、ResultSetHandler。四大对象的工作职责如下:

  • Executor(执行器):负责整个 SQL 执行过程的总体控制
  • StatementHandler(语句处理器):负责和 JDBC 层交互,包括预编译 SQL 语句和执行 SQL 语句,以及调用 ParameterHandler 设置参数
  • ParameterHandler(参数处理器):负责设置预编译参数
  • ResultSetHandler(结果集处理器):负责将 JDBC 查询结果映射到 JavaBean 对象

四大对象的工作流程

MyBatis 插件开发

本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-lesson-20

插件介绍

  • MyBatis 在四大对象的创建过程中,都允许有插件进行介入。插件可以利用动态代理机制一层层的包装目标对象,从而实现在目标对象指向目标方法之前进行拦截的效果。
  • MyBatis 自定义插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截哪个对象的哪个方法,即可介入四大对象的任何一个方法的执行。
  • MyBatis 允许在已映射语句执行过程中的某一点进行拦截调用。
  • 默认情况下,MyBatis 允许使用插件来拦截的方法包括:
    • Executor:update, query, flushStatements, commit, rollback, getTransaction, close, isClosed
    • StatementHandler:prepare, parameterize, batch, update, query
    • ParameterHandler:getParameterObject, setParameters
    • ResultSetHandler:handlerResultSets, handlerOuutputParameters

特别注意

自定义 MyBatis 插件时,如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。这些都是更底层的类和方法,所以使用插件的时候需要特别小心。

插件原理

  • 在 MyBatis 的全局配置文件里注册插件后,会按照插件的配置顺序,依次调用插件的 plugin() 方法来生成被拦截对象的动态代理对象
  • 存在多个插件时,会依次生成目标对象的动态代理对象,层层包裹,先声明的先包裹,以此形成代理链
  • 目标方法执行时,是按照插件配置信息的逆向顺序来执行 intercept() 方法,即先配置的插件会后执行
  • 使用多个插件的情况下,往往需要在某个插件中分离出来目标对象,可以借助 MyBatis 提供的 SystemMateObject 类来获取最后一层的 h 以及 target 属性的值

插件开发案例

Interceptor 接口

  • Interceptor.intercept():拦截目标方法的执行
  • Interceptor.plugin():创建动态代理对象,可以使用 MyBatis 提供的 Plugin 类的 wrap 方法
  • Interceptor.setProperties():注入配置插件时设置的属性

插件开发案例一

  • 实现 Interceptor 接口,并通过插件签名指定要拦截哪个对象的哪个方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.clay.mybatis.plugin;

import java.sql.Connection;
import java.util.Properties;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;

@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) })
public class MyFirstPlugin implements Interceptor {

private Properties properties = new Properties();

/**
* 拦截目标对象的目标方法的执行
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("MyFirstPlugin ==> " + invocation.getTarget().getClass().getName() + "." + invocation.getMethod().getName() + "() 方法准备执行");
Object result = invocation.proceed();
System.out.println("MyFirstPlugin ==> " + invocation.getTarget().getClass().getName() + "." + invocation.getMethod().getName() + "() 方法执行完成");
return result;
}

/**
* 为目标对象创建一个代理对象
*/
@Override
public Object plugin(Object target) {
System.out.println("MyFirstPlugin ==> 要包装的对象: " + target);
Object wrap = Plugin.wrap(target, this);
return wrap;
}

/**
* 将插件注册时的Property属性设置进来
*/
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}

}
  • 在 MyBatis 的全局配置文件中注册插件
1
2
3
4
5
6
7
8
9
10
<configuration>

<!-- 注册插件 -->
<plugins>
<plugin interceptor="com.clay.mybatis.plugin.MyFirstPlugin">
<property name="name" value="FirstPlugin" />
</plugin>
</plugins>

</configuration>
  • 上面自定义的插件将会拦截在 StatementHandler 实例中所有的 prepare() 方法调用,MyBatis 执行普通的 SQL 查询语句后,控制台输出的日志信息如下:
1
2
3
4
5
6
7
8
9
10
11
12
MyFirstPlugin ==> 要包装的对象: org.apache.ibatis.executor.CachingExecutor@69fb6037
MyFirstPlugin ==> 要包装的对象: org.apache.ibatis.scripting.defaults.DefaultParameterHandler@11bd0f3b
MyFirstPlugin ==> 要包装的对象: org.apache.ibatis.executor.resultset.DefaultResultSetHandler@696da30b
MyFirstPlugin ==> 要包装的对象: org.apache.ibatis.executor.statement.RoutingStatementHandler@4e7912d8
22:16:06,391 DEBUG JdbcTransaction:137 - Opening JDBC Connection
22:16:06,755 DEBUG PooledDataSource:434 - Created connection 208684473.
22:16:06,755 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@c7045b9]
MyFirstPlugin ==> org.apache.ibatis.executor.statement.RoutingStatementHandler.prepare() 方法准备执行
22:16:06,761 DEBUG getEmpById:137 - ==> Preparing: select id, last_name as lastName, gender, email from t_employee where id = ?
MyFirstPlugin ==> org.apache.ibatis.executor.statement.RoutingStatementHandler.prepare() 方法执行完成
22:16:06,801 DEBUG getEmpById:137 - ==> Parameters: 1(Long)
22:16:06,831 DEBUG getEmpById:137 - <== Total: 0

插件开发案例二

在下面的案例代码里,演示了如何动态更改 SQL 语句运行时的参数,例如查询员工信息时,动态更改查询的员工 ID 为 11

  • Mapper 接口
1
2
3
4
5
public interface EmployeeMapper {

public Employee getEmpById(Long id);

}
  • SQL 映射文件
1
2
3
4
5
6
7
8
9
<mapper namespace="com.clay.mybatis.dao.EmployeeMapper">

<select id="getEmpById" parameterType="Long" resultType="com.clay.mybatis.bean.Employee">
select id, last_name as lastName, gender, email
from t_employee
where id = #{id}
</select>

</mapper>
  • 自定义插件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package com.clay.mybatis.plugin;

import java.sql.Connection;
import java.util.Properties;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

/**
* 插件签名:告诉 MyBatis 当前插件要拦截哪个对象的哪个方法
*/
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) })
public class MyThirdPlugin implements Interceptor {

private Properties properties = new Properties();

/**
* 拦截目标对象的目标方法的执行
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {

// 获取目标对象
Object target = invocation.getTarget();

// 分离被代理对象的元数据
MetaObject metaObject = SystemMetaObject.forObject(target);

// 更改 SQL 语句要用的参数
metaObject.setValue("parameterHandler.parameterObject", 11L);

// 执行目标方法
Object proceed = invocation.proceed();

// 返回执行结果
return proceed;
}

/**
* 为目标对象创建一个代理对象
*/
@Override
public Object plugin(Object target) {
Object wrap = Plugin.wrap(target, this);
return wrap;
}

/**
* 将插件注册时的Property属性设置进来
*/
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}

}
  • 注册插件
1
2
3
4
5
6
7
8
9
10
<configuration>

<!-- 注册插件 -->
<plugins>
<plugin interceptor="com.clay.mybatis.plugin.MyThirdPlugin">
<property name="name" value="MyThirdPlugin" />
</plugin>
</plugins>

</configuration>
  • 业务代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyBatisApplication {

public static void main(String[] args) throws IOException {
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

SqlSession session = sqlSessionFactory.openSession();
try {
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
mapper.getEmpById(5L);
} finally {
if (session != null) {
session.close();
}
}
}

}
  • 执行结果
1
2
3
22:36:57,053 DEBUG getEmpById:137 - ==>  Preparing: select id, last_name as lastName, gender, email from t_employee where id = ?
22:36:57,089 DEBUG getEmpById:137 - ==> Parameters: 11(Long)
22:36:57,118 DEBUG getEmpById:137 - <== Total: 0

多个插件的执行顺序

MyBatis 在创建动态代理时,是按照插件的配置顺序依次调用 plugin() 方法创建层层的代理对象,但插件的 intercept() 方法是按照配置信息的逆向顺序来执行,即先配置的插件会后执行。

  • 在上面案例代码的基础上,增加一个 MyBatis 插件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.clay.mybatis.plugin;

import java.sql.Connection;
import java.util.Properties;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;

/**
* 插件签名:告诉 MyBatis 当前插件要拦截哪个对象的哪个方法
*/
@Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) })
public class MySecondPlugin implements Interceptor {

private Properties properties = new Properties();

/**
* 拦截目标对象的目标方法的执行
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("MySecondPlugin ==> " + invocation.getTarget().getClass().getName() + "." + invocation.getMethod().getName() + "() 方法准备执行");
Object result = invocation.proceed();
System.out.println("MySecondPlugin ==> " + invocation.getTarget().getClass().getName() + "." + invocation.getMethod().getName() + "() 方法执行完成");
return result;
}

/**
* 为目标对象创建一个代理对象
*/
@Override
public Object plugin(Object target) {
System.out.println("MySecondPlugin ==> 要包装的对象: " + target);
Object wrap = Plugin.wrap(target, this);
return wrap;
}

/**
* 将插件注册时的Property属性设置进来
*/
@Override
public void setProperties(Properties properties) {
this.properties = properties;
}

}
  • 在 MyBatis 的全局配置文件中同时注册多个插件
1
2
3
4
5
6
7
8
9
10
11
12
13
<configuration>

<!-- 注册插件 -->
<plugins>
<plugin interceptor="com.clay.mybatis.plugin.MyFirstPlugin">
<property name="name" value="FirstPlugin" />
</plugin>
<plugin interceptor="com.clay.mybatis.plugin.MySecondPlugin">
<property name="name" value="SecondPlugin" />
</plugin>
</plugins>

</configuration>
  • MyBatis 执行普通的 SQL 查询语句后,控制台输出的日志信息如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
MyFirstPlugin ==> 要包装的对象: org.apache.ibatis.executor.CachingExecutor@36d585c
MySecondPlugin ==> 要包装的对象: org.apache.ibatis.executor.CachingExecutor@36d585c
MyFirstPlugin ==> 要包装的对象: org.apache.ibatis.scripting.defaults.DefaultParameterHandler@c333c60
MySecondPlugin ==> 要包装的对象: org.apache.ibatis.scripting.defaults.DefaultParameterHandler@c333c60
MyFirstPlugin ==> 要包装的对象: org.apache.ibatis.executor.resultset.DefaultResultSetHandler@4e7912d8
MySecondPlugin ==> 要包装的对象: org.apache.ibatis.executor.resultset.DefaultResultSetHandler@4e7912d8
MyFirstPlugin ==> 要包装的对象: org.apache.ibatis.executor.statement.RoutingStatementHandler@53976f5c
MySecondPlugin ==> 要包装的对象: org.apache.ibatis.executor.statement.RoutingStatementHandler@53976f5c
22:57:50,780 DEBUG JdbcTransaction:137 - Opening JDBC Connection
22:57:51,071 DEBUG PooledDataSource:434 - Created connection 261748192.
22:57:51,071 DEBUG JdbcTransaction:101 - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@f99f5e0]
MySecondPlugin ==> com.sun.proxy.$Proxy9.prepare() 方法准备执行
MyFirstPlugin ==> org.apache.ibatis.executor.statement.RoutingStatementHandler.prepare() 方法准备执行
22:57:51,078 DEBUG getEmpById:137 - ==> Preparing: select id, last_name as lastName, gender, email from t_employee where id = ?
MyFirstPlugin ==> org.apache.ibatis.executor.statement.RoutingStatementHandler.prepare() 方法执行完成
MySecondPlugin ==> com.sun.proxy.$Proxy9.prepare() 方法执行完成
22:57:51,133 DEBUG getEmpById:137 - ==> Parameters: 1(Long)
22:57:51,173 DEBUG getEmpById:137 - <== Total: 0

MyBatis 代码生成器

MyBatis Generator(简称 MBG),是一个专门为 MyBatis 框架使用者定制的代码生成器(逆向工程),可以快速地根据数据库表生成对应的 SQL 映射文件、Mapper 接口以及 JavaBean 类。支持基本的增删改查,以及 QBC 风格的条件查询。但是像数据库表连接、存储过程等这些复杂 SQL 的定义需要开发者手工编写。更多的介绍内容,可查看 GitHub 仓库MyBatis 官方文档

准备工作

下述的案例代码是基于以下的数据库表结构编写的,因此需要提前执行 SQL 语句来初始化数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CREATE DATABASE `mybatis_lesson` DEFAULT CHARACTER SET utf8mb4;

CREATE TABLE `t_department` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `t_employee` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`last_name` varchar(255) DEFAULT NULL,
`gender` char(1) DEFAULT NULL,
`email` varchar(255) DEFAULT NULL,
`dept_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into t_department(id, name) values(1, '开发部门'), (2, '测试部门');
insert into t_employee(id, last_name, gender, email, dept_id) values(1, 'Jim','1', 'jim@gmail.com', 1);
insert into t_employee(id, last_name, gender, email, dept_id) values(2, 'Peter','1', 'peter@gmail.com', 1);

使用案例

本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-lesson-19

引入 Maven 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>

<dependency>
<groupId>org.mybatis.dynamic-sql</groupId>
<artifactId>mybatis-dynamic-sql</artifactId>
<version>1.4.0</version>
</dependency>

<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.1</version>
</dependency>

创建 XML 配置文件

在项目的 src/test/resources 目录下创建 mybatis-generator.xml 配置文件,其中的配置内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>

<context id="MySQLTables" targetRuntime="MyBatis3Simple">

<!-- 数据库连接信息 -->
<jdbcConnection driverClass="com.mysql.cj.jdbc.Driver" connectionURL="jdbc:mysql://127.0.0.1:3306/mybatis_lesson" userId="root" password="123456" />

<!-- Java 类型解析 -->
<javaTypeResolver>
<property name="forceBigDecimals" value="false" />
</javaTypeResolver>

<!-- JavaBean 生成策略 -->
<javaModelGenerator targetPackage="com.clay.mybatis.bean" targetProject="./src/main/java">
<property name="enableSubPackages" value="true" />
<property name="trimStrings" value="true" />
</javaModelGenerator>

<!-- SQL 映射文件生成策略 -->
<sqlMapGenerator targetPackage="com.clay.mybatis.dao" targetProject="./src/main/java">
<property name="enableSubPackages" value="true" />
</sqlMapGenerator>

<!-- Mapper 接口类生成策略 -->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.clay.mybatis.dao" targetProject="./src/main/java">
<property name="enableSubPackages" value="true" />
</javaClientGenerator>

<!-- 数据库表与 JavaBean 的映射 -->
<table tableName="t_employee" domainObjectName="Employee" />
<table tableName="t_department" domainObjectName="Department" />

</context>

</generatorConfiguration>

上述 XML 配置文件里的 targetRuntime="MyBatis3Simple" 表示只生成基础的 CRUD 代码和少量动态 SQL 语句,可选值有 MyBatis3DynamicSql | MyBatis3Kotlin | MyBatis3 | MyBatis3Simple。值得一提的是,在企业项目开发中,用得最多的是 targetRuntime="MyBatis3",它支持复杂条件查询(QBC 查询)和自动生成大量动态 SQL 语句。MyBatis Generator 完整的 XML 标签介绍可看 这里

运行代码生成器

MyBatis Generator (MBG) 可以通过以下方式运行:

这里采用 Java 代码 + XML 配置文件的方式运行代码生成器,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.internal.DefaultShellCallback;

public class GeneratorTest {

public static void main(String[] args) throws Exception {
List<String> warnings = new ArrayList<String>();
boolean overwrite = true;
URL url = GeneratorTest.class.getClassLoader().getResource("mybatis-generator.xml");
File configFile = new File(url.getFile());
ConfigurationParser cp = new ConfigurationParser(warnings);
Configuration config = cp.parseConfiguration(configFile);
DefaultShellCallback callback = new DefaultShellCallback(overwrite);
MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
myBatisGenerator.generate(null);
}

}

最后会在项目的目录内自动生成 JavaBean 类、Mapper 接口和 SQL 映射文件,项目的目录结构图如下:

MyBatis 工作原理

工作原理图一

工作原理图二

工作原理图三

MyBatis 源码分析