MyBatis 缓存

前言

频繁地查询必然会给数据库带来巨大的压力,为此 MyBatis 提供了丰富的缓存功能。缓存可以有效的提升查询效率、缓解数据库压力,提高应用的稳健性。

MyBatis 的缓存有两层,默认情况下会开启一级缓存,并提供了开启二级缓存的配置。本小节我们将一起学习 MyBatis 的缓存,充分地了解和使用它。

一级缓存

MyBatis 一级缓存是默认开启的,缓存的有效范围是一个会话内。一个会话内的 select 查询语句的结果会被缓存起来,当在该会话内调用 update、delete 和 insert 时,会话缓存会被刷新,以前的缓存会失效。

使用一级缓存

下面,我们以一个简单的例子来看看 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
package com.ahao.mybatis.cache;

import com.ahao.mybatis.mapper.UserMapper;
import com.ahao.mybatis.model.User;
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 java.io.IOException;
import java.io.InputStream;

@SuppressWarnings({"Duplicates"})
public class CacheTest1 {
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();
// 得到 mapper
UserMapper userMapper = session.getMapper(UserMapper.class);
// 查询得到 user1
User user1 = userMapper.selectUserById(1);
System.out.println(user1);
// 查询得到 user2
User user2 = userMapper.selectUserById(1);
// 通过 == 判断 user1 和 user2 是否指向同一内存区间
System.out.println(user1 == user2);
session.commit();
session.close();
}
}

结果:

1
2
User{id=1, username='peter-gao', age=180, score=1000}
true

在这个例子中,我们连续两次调用了 userMapper 的 selectUserById 方法,但是在程序输出中,user1 和 user2 却指向了同一块内存区域。这就是 MyBatis 缓存的作用,当第二次调用查询时,MyBatis 没有查询数据库而是直接从缓存中拿到了数据。

弃用一级缓存

select 配置关闭缓存

select 默认会启用一级缓存,我们也可通过配置来关闭掉 select 缓存。

如下,我们通过 flushCache 属性来关闭 select 查询的缓存。

1
2
3
4
<select id="selectUserById" flushCache="true" parameterType="java.lang.Integer"
resultType="com.ahao.mybatis.model.User">
SELECT * FROM user WHERE id = #{id}
</select>

再次运行程序,结果如下:

1
2
User{id=1, username='peter-gao', age=180, score=1000}
false

此时 user1 与 user2 不再指向同一内存区,缓存失效了。

调用 insert、update、delete 刷新缓存

一般情况下,我们都推荐开启 select 的缓存,因为这会节省查询时间。当然在一个会话中,调用 insert、update、delete 语句时,会话中的缓存也会被刷新。

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
UserMapper userMapper = session.getMapper(UserMapper.class);
User user1 = userMapper.selectUserById(1);
System.out.println(user1);
User user = new User();
user.setUsername("cache test");
user.setAge(10);
user.setScore(100);
userMapper.insertUser(user);
User user2 = userMapper.selectUserById(1);
System.out.println(user1 == user2);
session.commit();
session.close();
代码块123456789101112
User{id=1, username='peter', age=18, score=100}
false

在第一个查询调用前,我们先进行了一次 insert 操作,此时会刷新缓存,user1 和 user2 又没有指向同一处内存。

二级缓存

MyBatis 二级缓存默认关闭,我们可以通过简单的设置来开启二级缓存。二级缓存的有效范围为一个 SqlSessionFactory 生命周期,绝大多数情况下,应用都会只有一个 SqlSessionFactory,因此我们可以把二级缓存理解为全局缓存。

全局可用

在 MyBatis 全局配置文件中,即 mybatis-config.xml 文件,二级缓存可由 settings 下的 cacheEnabled 属性开启。如下:

1
2
3
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>

当打开 cacheEnabled 属性后,二级缓存全局可用。

TIPS:注意,这里是可用,cacheEnabled 的默认值其实也是 true,即全局可用,由于二级缓存需要对 mapper 配置后才真正生效,简单来说就是双层开关。当将其设置为 false 后,则全局关闭,mapper 中即使配置了,二级缓存也会失效。

mapper 中开启

xml 开启

在二级缓存全局可用的情况下,mapper 才可通过 cache 配置开启二级缓存。如,在 UserMapper.xml 文件中开启二级缓存:

1
<cache/>

这种情况下,缓存的行为如下:

  • mapper 下的所有 select 语句会被缓存;
  • mapper 下的 update,insert,delete 语句会刷新缓存;
  • 使用 LRU 算法来回收对象;
  • 最大缓存 1024 个对象;
  • 缓存可读、可写。
  • 缓存不会根据时间来刷新。

cache 提供了诸多属性来修改缓存行为,示例如下:

1
2
3
4
5
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>

这个例子下的缓存使用 FIFO 算法来回收对象,并每隔 60 秒刷新一次,最多缓存 512 个对象,且缓存只可读。

cache 有 4 个属性可配置,从而改变缓存的行为。

属性 描述
eviction 回收策略,默认 LRU,可选择的有 FIFO(先进先出),SOFT(软引用),WEAK(弱引用)
flushInterval 刷新时间
size 最多缓存对象数
readOnly 是否只读

注解开启

如果你不使用 mapper.xml 文件,也可以使用注解来开启。

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.ahao.mybatis.mapper;

import org.apache.ibatis.annotations.CacheNamespace;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.cache.decorators.FifoCache;

@Mapper
@CacheNamespace(
eviction = FifoCache.class,
flushInterval = 60000,
size = 512,
readWrite = false
)
public interface BlogMapper {
}

注解 CacheNamespace 的配置与 xml 配置保持一致,唯一区别在于若使用注解,那么 eviction 属性需直接给出缓存实现类。

缓存共享

xml 共享

有时候,我们想在不同的 mapper 中共享缓存,为了解决这类问题,MyBatis 提供了 cache-ref 配置。

使用也很简单,如下:

1
<cache-ref namespace="com.ahao.mybatis.mapper.UserMapper"/>

mapper 由 namespace 来唯一标识,因此只需在另一个 mapper 文件中添加上 cache-ref 配置,并加上相应的 namespace 即可。

这样当前的 mapper 可以共享来自 UserMapper 的缓存。

注解共享

同样的,我们也可以使用注解来共享缓存。

如下:

1
2
3
@CacheNamespaceRef(UserMapper.class)
public interface BlogMapper {
}

这里,BlogMapper 共享了 UserMapper 的缓存。

TIPS: 注意,CacheNamespaceRef 与 CacheNamespace 不能共存,既然选择了共享就不能再独立开辟缓存区了。

小结

  • MyBatis 的一级缓存默认可用,有效范围小,不会影响到其它会话,因此无特殊情况,不推荐丢弃一级缓存。
  • MyBatis 二级缓存默认使用程序内存缓存,但这显然不够安全,一般情况下我们都推荐使用 Redis 等专业的缓存。

MyBatis 类型处理器

前言

MyBatis 提供了诸多类型处理器,但是相较于丰富的数据库类型仍然略显不足,比如 MyBatis 只能将 JSON 数据类型当成普通的字符串处理。因此 MyBatis 提供了类型处理器接口,让开发者可以根据具体的业务需求来自定义适合的类型处理器。

本小节,我们将以 JSON 类型处理器作为落脚点,来介绍类型处理器,并自定义 JSON 类型处理器。

JSON 数据类型

首先,我们需要为 MyBatis 内置类型处理器增加一个它无法处理的数据类型,这里我们选择 MySQL5.7 中新增的 JSON 数据类型,这也是大家普遍使用的一个数据类型。在可用的数据库环境中,我们运行如下脚本:

1
2
3
4
5
6
7
8
9
10
DROP TABLE IF EXISTS blog;
CREATE TABLE blog
(
id int(11) unsigned primary key auto_increment,
info json,
tags json
);
INSERT INTO blog(info, tags)
VALUES ('{"title": "世界更大", "content": "世界更大的内容", "rank": 1}', '["世界观"]'),
('{"title": "人生更短", "content": "人生更短的内容", "rank": 2}', '["人文"]');

在这个脚本中,我们新建了一个 blog 数据表,blog 数据表除 id 外有 info 和 tags 两个字段,这两个字段都是 JSON 类型,并通过 insert 语句添加了两条记录。

类型处理器

MyBatis 默认是无法很好处理 info 和 tags 这两个字段的,只能将它们当成字符串类型来处理,但显然这不是我们想要的效果。我们希望新增 json 类型处理器来处理好这两个字段。

MyBatis 提供了 TypeHandler 接口,自定义类型处理器需要实现了这个接口才能工作。考虑到很多开发者不够熟练,MyBatis 还提供了一个 BaseTypeHandler 抽象类来帮助我们做自定义类型处理器,只需继承这个基类,然后实现它的方法即可。

JsonObject 处理器

JSON 可分为 object 和 array 两大类,分别对应 info 和 tags 字段,这两类需要分别实现类型处理器。由于需要对 JSON 进行处理,我们在 pom.xml 文件中添加上对应的依赖。

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>

这里,我们使用阿里巴巴开源的 fastjson库。

在 com.ahao.mybatis 包下新建 handler 包,并向 handler 包中添加上 json object 的类型处理器 JsonObjectTypeHandler。如下:

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
package com.ahao.mybatis.handler;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.ibatis.type.*;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

@MappedJdbcTypes(JdbcType.VARCHAR) // 对应jdbc 类型
@MappedTypes({JSONObject.class}) // 对应处理后类型
public class JsonObjectTypeHandler extends BaseTypeHandler<JSONObject> {
// 当为 PreparedStatement 参数时,如何处理对象
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, JSONObject o, JdbcType jdbcType) throws SQLException {
preparedStatement.setString(i, JSON.toJSONString(o));
}

// 当通过名称从结果中取json字段时如何处理
@Override
public JSONObject getNullableResult(ResultSet resultSet, String s) throws SQLException {
String t = resultSet.getString(s);
return JSON.parseObject(t);
}

// 当通过序列号从结果中取json字段时如何处理
@Override
public JSONObject getNullableResult(ResultSet resultSet, int i) throws SQLException {
String t = resultSet.getString(i);
return JSON.parseObject(t);
}

// 当通过序列号从 CallableStatement 中取json字段时如何处理
@Override
public JSONObject getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
String t = callableStatement.getString(i);
return JSON.parseObject(t);
}
}

有了 BaseTypeHandler 作为基础后,实现一个类型处理器就比较简单了,我们只需要为其中 4 个方法添加上对应的实现即可。

类型处理器有两个作用,第一处理 Java 对象到 JdbcType 类型的转换,对应 setNonNullParameter 方法;第二处理 JdbcType 类型到 Java 类型的转换,对应 getNullableResult 方法,getNullableResult 有 3 个重载方法。下面我们依次来说明这四个方法的作用:

  • setNonNullParameter:处理 PreparedStatement 中的 JSONObject 参数,当调用 PreparedStatement 执行 SQL 语句时,调用该处理 JSONObject 类型的参数,这里我们通过 fastjson 的JSON.toJSONString(o)函数将 JSONObject 转化为字符串类型即可。
  • getNullableResult:从结果集中获取字段,这里 CallableStatement 和 ResultSet 分别对应不同的执行方式,对于 JDBC 而言 JSON 类型也会当做字符串来处理,因此这里我们需要将字符串类型转化为 JSONObject 类型,对应 JSON.parseObject(t)代码。

JsonArray 处理器

与 JsonObjectTypeHandler 一样,在 handler 包下新建 JsonArrayTypeHandler 类,继承 BaseTypeHandler 类,并将具体方法的实现从 JSON.parseObject 改变为 JSON.parseArray,如下:

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
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes({JSONArray.class})
public class JsonArrayTypeHandler extends BaseTypeHandler<JSONArray> {
@Override
public void setNonNullParameter(PreparedStatement preparedStatement, int i, JSONArray o, JdbcType jdbcType) throws SQLException {
preparedStatement.setString(i, JSON.toJSONString(o));
}

@Override
public JSONArray getNullableResult(ResultSet resultSet, String s) throws SQLException {
String t = resultSet.getString(s);
// // 变成了 parseArray
return JSON.parseArray(t);
}

@Override
public JSONArray getNullableResult(ResultSet resultSet, int i) throws SQLException {
String t = resultSet.getString(i);
// // 变成了 parseArray
return JSON.parseArray(t);
}

@Override
public JSONArray getNullableResult(CallableStatement callableStatement, int i) throws SQLException {
String t = callableStatement.getString(i);
// 变成了 parseArray
return JSON.parseArray(t);
}
}

注册类型处理器

自定义类型处理器无法直接被 MyBatis 加载,我们需要增加相关的配置告诉 MyBatis 加载类型处理器。

全局注册

在全局配置配置文件中可通过 typeHandlers 属性来注册类型处理器。如下:

1
2
3
<typeHandlers>
<package name="com.ahao.mybatis.handler"/>
</typeHandlers>

通过 package 项来指定类型处理器所在的包路径,这样 handler 包中的所有类型处理器都会注册到全局。

当然如果你的类型处理器分散在其它地方,也可以通过如下方式来注册。

1
2
3
<typeHandlers>
<typeHandler handler="com.ahao.mybatis.handler.JsonArrayTypeHandler"/>
</typeHandlers>

全局注册的类型处理器会自动被 MyBatis 用来处理所有符合类型的参数。如 JsonArrayTypeHandler 通过 MappedJdbcTypes 注解表明了自己将会处理 JdbcType.VARCHAR 类型,MyBatis 会自动将字符串类型的参数交给 JsonArrayTypeHandler 来进行处理。

但是,这样显然有问题,因为 JsonObjectTypeHandler 注册的类型也是 JdbcType.VARCHAR 类型,所以全局注册是不推荐的,除非你需要对所有参数都做类型转换。

局部注册

由于全局注册会对其它类型产生歧义和污染,因此我们选择更加精准的局部注册。在 BlogMapper 中,我们来注册和使用类型处理器。

在 BlogMapper.xml 文件中,我们添加上如下配置。

1
2
3
4
5
6
7
8
9
<resultMap id="blogMap" type="com.ahao.mybatis.model.Blog">
<result column="id" property="id"/>
<result column="info" property="info" typeHandler="com.ahao.mybatis.handler.JsonObjectTypeHandler"/>
<result column="tags" property="tags" typeHandler="com.ahao.mybatis.handler.JsonArrayTypeHandler"/>
</resultMap>

<select id="selectById" resultMap="blogMap">
SELECT * FROM blog WHERE id = #{id}
</select>

我们定义了 名为 blogMap 的 resultMap 和名为 selectById 的查询。在 result 映射中,我们注册了相关的类型处理器,info 字段对应

JsonObjectTypeHandler 类型处理器,tags 字段对应 JsonArrayTypeHandler 类型处理器。

这样自定义的类型处理器不会污染到其它数据,blogMap 的类型 com.ahao.mybatis.model.Blog 定义如下:

1
2
3
4
5
6
7
8
9
10
11
package com.ahao.mybatis.model;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;

public class Blog {
private Long id;
private JSONObject info;
private JSONArray tags;
// 省略了 getter 和 setter 方法
}

处理 JDBC 类型

在对应的 BlogMapper.java 接口上添加上对应的 selectById 方法:

1
2
3
4
5
6
7
8
9
package com.ahao.mybatis.mapper;

import com.ahao.mybatis.model.Blog;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface BlogMapper {
Blog selectById(Integer id);
}

我们测试一下 selectById 方法:

1
2
3
4
5
6
7
BlogMapper blogMapper = session.getMapper(BlogMapper.class);
Blog blog = blogMapper.selectById(1);
System.out.println(blog.toString());
String title = blog.getInfo().getString("title");
System.out.println(title);
String tag = blog.getTags().getString(0);
System.out.println(tag);

输出结果如下:

1
2
3
Blog{id=1, info={"rank":1,"title":"世界更大","content":".......****............"}, tags=["世界观"]}
世界更大
世界观

从结果中可以看出,类型处理器成功的处理了查询的数据,info 和 tags 字段都能够通过 fastjson 的 API 来获取里面的内容。

处理 JSON 类型

在查询可以工作的情况下,那么如何通过 insert 插入 JSON 对象了。

我们在 BlogMapper 中新增一个 insertBlog 方法,如下:

1
2
3
4
5
6
7
8
9
<insert id="insertBlog">
INSERT INTO blog(info,tags)
VALUES(#{info,typeHandler=com.ahao.mybatis.handler.JsonObjectTypeHandler},
#{tags,typeHandler=com.ahao.mybatis.handler.JsonArrayTypeHandler})
</insert>

public interface BlogMapper {
int insertBlog(@Param("info") JSONObject info, @Param("tags") JSONArray tags);
}

这样 MyBatis 就可以处理 JSON 类型的参数了,我们再次测试一下:

1
2
3
4
JSONObject info = new JSONObject().fluentPut("title", "测试案例").fluentPut("rank", 1);
JSONArray tags = new JSONArray().fluentAdd("测试");
int rows = blogMapper.insertBlog(info, tags);
System.out.println(rows);

输出结果:

1
1

可以看到类型处理器成为了 Java JSON 类型和 JDBC 类型转换桥梁,在查询的时候主动将数据库类型转化为了可用的 JSON 类型,而在插入的时候将 JSON 类型又转化为了数据库可识别的字符串类型。

小结

  • 自定义类型处理器并不难,MyBatis 已经帮我们做好了大多数工作,我们只需在适当的位置适当的配置就可以了。
  • 数据库 JSON 类型的用处会越来越广泛,在 MyBatis 官方未内置处理器之前,我们也可以通过本小节的方式来提早的使用它。

MyBatis 插件

前言

MyBatis 允许我们以插件的形式对已映射语句执行过程中的某一点进行拦截调用,通俗一点来说,MyBatis 的插件其实更应该被称作为拦截器。

MyBatis 插件的使用十分广泛,分页、性能分析、乐观锁、逻辑删除等等常用的功能都可以通过插件来实现。既然插件如此好用,本小节我们就一起来探索插件并且实现一个简单的 SQL 执行时间计时插件。

介绍

可拦截对象

MyBatis 允许插件拦截如下 4 个对象的方法。

  • Executor的 update, query, flushStatements, commit, rollback, getTransaction, close, isClosed 方法
  • ParameterHandler的 getParameterObject, setParameters 方法
  • ResultSetHandler的 handleResultSets, handleOutputParameters 方法
  • StatementHandler的 prepare, parameterize, batch, update, query 方法

注意,这四个对象都是接口,插件会拦截实现了该接口的对象。

插件接口

插件必须实现 Interceptor 接口。Interceptor 接口共有 3 个方法,如下:

1
2
3
4
5
6
7
8
9
10
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;

default Object plugin(Object target) {
return Plugin.wrap(target, this);
}

default void setProperties(Properties properties) {
}
}

其中 plugin 和 setProperties 方法都是默认实现的方法,我们可以选择不覆盖实现,而 intercept 方法则必须实现。如下:

  • intercept : 核心方法,通过 Invocation 我们可以拿到被拦截的对象,从而实现自己的逻辑。
  • plugin: 给 target 拦截对象生成一个代理对象,已有默认实现。
  • setProperties: 插件的配置方法,在插件初始化的时候调用。

拦截器签名

插件可对多种对象进行拦截,因此我们需要通过拦截器签名来告诉 MyBatis 插件应该拦截何种对象的何种方法。举例如下:

1
2
3
4
5
6
@Intercepts({@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)})
public class XXXPlugin implements Interceptor {}

类 XXXPlugin 上有两个注解:

  • Intercepts注解: 拦截声明,只有 Intercepts 注解修饰的插件才具有拦截功能。
  • Signature注解: 签名注解,共 3 个参数,type 参数表示拦截的对象,如 StatementHandler,另外还有Executor、ParameterHandler和ResultSetHandler;method 参数表示拦截对象的方法名,即对拦截对象的某个方法进行拦截,如 prepare,代表拦截 StatementHandler 的 prepare 方法;args 参数表示拦截方法的参数,因为方法可能会存在重载,因此方法名加上参数才能唯一标识一个方法。

推断可知 XXXPlugin 插件会拦截 StatementHandler对象的 prepare(Connection connection, Integer var2) 方法。

一个插件可以拦截多个对象的多个方法,因此在 Intercepts 注解中可以添加上多个 Signature注解。

实践

接下来,我们一起来实现一个简单的 SQL 执行时间计时插件。插件的功能是日志输出每一条 SQL 的执行用时。

在 com.ahao.mybatis 包下,我们新建 plugin 包,并在包中添加 SqlStaticsPlugin 类。SqlStaticsPlugin 会拦截 StatementHandler的prepare方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.ahao.mybatis.plugin;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;

@Intercepts({@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)})
public class SqlStaticsPlugin implements Interceptor {
private Logger logger = LoggerFactory.getLogger(SqlStaticsPlugin.class);

@Override
public Object intercept(Invocation invocation) throws Throwable {
return invocation.proceed();
}
}

我们一起来完善这个插件。

  1. 首先需要得到 invocation 的拦截对象 StatementHandler,并从 StatementHandler 中拿到 SQL 语句。
  2. 得到当前的时间戳 startTime。
  3. 执行 SQL。
  4. 得到执行后的时间戳 endTime。
  5. 计算时间差,并打印 SQL 耗时。

对应的 intercept 方法代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Object intercept(Invocation invocation) throws Throwable {
// 得到拦截对象
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObj = SystemMetaObject.forObject(statementHandler);
String sql = (String) metaObj.getValue("delegate.boundSql.sql");
// 开始时间
long startTime = System.currentTimeMillis();
// 执行SQL
Object res = invocation.proceed();
// 结束时间
long endTime = System.currentTimeMillis();
long sqlCost = endTime - startTime;
// 去掉无用的换行符,打印美观
logger.info("sql: {} - cost: {}ms", sql.replace("\n", ""), sqlCost);
// 返回执行的结果
return res;
}

注意,通过反射调用后的结果 res,我们一定要记得返回。MyBatis 提供了 MetaObject 这个类来方便我们进行拦截对象属性的修改,这里我们简单的使用了getValue方法来得到 SQL 语句。

我们在全局配置文件注册这个插件:

1
2
3
<plugins>
<plugin interceptor="com.ahao.mybatis.plugin.SqlStaticsPlugin" />
</plugins>

到这,这个插件已经可以工作了,但是我们希望它能更加灵活一点,通过配置来拦截某些类型的 SQL,如只计算 select 类型SQL的耗时。

插件会在初始化的时候通过 setProperties 方法来加载配置,利用它我们可以得到哪些方法需要被计时。如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class SqlStaticsPlugin implements Interceptor {
private List<String> methods = Arrays.asList("SELECT", "INSERT", "UPDATE", "DELETE");

@Override
public void setProperties(Properties properties) {
String methodsStr = properties.getProperty("methods");
if (methodsStr == null || methodsStr.isBlank())
return;
String[] parts = methodsStr.split(",");
methods = Arrays.stream(parts).map(String::toUpperCase).collect(Collectors.toList());
}
}

methods 参数默认可通过 select、insert、update 和 delete 类型的SQL语句,如果插件存在配置项 methods,那么则根据插件配置来覆盖默认配置。

在全局配置文件中,我们来添加上 methods 这个配置:

1
2
3
4
5
<plugins>
<plugin interceptor="com.ahao.mybatis.plugin.SqlStaticsPlugin">
<property name="methods" value="select,update"/>
</plugin>
</plugins>

类型之间以 , 隔开,MyBatis 会在插件初始化时,自动将 methods 对应的值通过 setProperties 方法来传递给SqlStaticsPlugin插件。插件拿到 Properties 后解析并替换默认的 methods 配置。

再次完善一下 intercept 方法,使其支持配置拦截:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObj = SystemMetaObject.forObject(statementHandler);
// 得到SQL类型
String sqlCommandType = metaObj.getValue("delegate.mappedStatement.sqlCommandType").toString();
// 如果方法配置中没有SQL类型,则无需计时,直接返回调用
if (!methods.contains(sqlCommandType)) {
return invocation.proceed();
}
String sql = (String) metaObj.getValue("delegate.boundSql.sql");
long startTime = System.currentTimeMillis();
Object res = invocation.proceed();
long endTime = System.currentTimeMillis();
long sqlCost = endTime - startTime;
logger.info("sql: {} - cost: {}ms", sql.replace("\n", ""), sqlCost);
return res;
}

当插件注册后,应用程序会打印出如下的日志语句:

1
17:48:14.110 [main] INFO com.ahao.mybatis.plugin.SqlStaticsPlugin - sql: INSERT INTO blog(info,tags)    VALUES(?,    ?) - cost: 87ms

至此,一个简单的 SQL 计时插件就开发完毕了。

小结

  • MyBatis 插件强大且易用,是深入掌握 MyBatis 的必备知识点。
  • 不少 MyBatis 三方库都提供了很多好用的插件,如 Pagehelper 分页插件,我们可以拿来即用。