MyBatis 插件
1. 前言
MyBatis 允許我們以插件的形式對已映射語句執(zhí)行過程中的某一點(diǎn)
進(jìn)行攔截調(diào)用
,通俗一點(diǎn)來說,MyBatis 的插件其實(shí)更應(yīng)該被稱作為攔截器。
MyBatis 插件的使用十分廣泛,分頁、性能分析、樂觀鎖、邏輯刪除等等常用的功能都可以通過插件來實(shí)現(xiàn)。既然插件如此好用,本小節(jié)我們就一起來探索插件并且實(shí)現(xiàn)一個簡單的 SQL 執(zhí)行時間計時插件。
2. 介紹
2.1 可攔截對象
MyBatis 允許插件攔截如下 4 個對象的方法。
- Executor的 update, query, flushStatements, commit, rollback, getTransaction, close, isClosed 方法
- ParameterHandler的 getParameterObject, setParameters 方法
- ResultSetHandler的 handleResultSets, handleOutputParameters 方法
- StatementHandler的 prepare, parameterize, batch, update, query 方法
注意,這四個對象都是接口,插件會攔截實(shí)現(xiàn)了該接口的對象。
2.2 插件接口
插件必須實(shí)現(xiàn) Interceptor 接口。Interceptor 接口共有 3 個方法,如下:
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 方法都是默認(rèn)實(shí)現(xiàn)的方法,我們可以選擇不覆蓋實(shí)現(xiàn),而 intercept 方法則必須實(shí)現(xiàn)。如下:
- intercept : 核心方法,通過 Invocation 我們可以拿到被攔截的對象,從而實(shí)現(xiàn)自己的邏輯。
- plugin: 給 target 攔截對象生成一個代理對象,已有默認(rèn)實(shí)現(xiàn)。
- setProperties: 插件的配置方法,在插件初始化的時候調(diào)用。
2.3 攔截器簽名
插件可對多種對象進(jìn)行攔截,因此我們需要通過攔截器簽名來告訴 MyBatis 插件應(yīng)該攔截何種對象的何種方法。舉例如下:
@Intercepts({@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)})
public class XXXPlugin implements Interceptor {}
類 XXXPlugin 上有兩個注解:
- Intercepts注解: 攔截聲明,只有 Intercepts 注解修飾的插件才具有攔截功能。
- Signature注解: 簽名注解,共 3 個參數(shù),type 參數(shù)表示攔截的對象,如 StatementHandler,另外還有Executor、ParameterHandler和ResultSetHandler;method 參數(shù)表示攔截對象的方法名,即對攔截對象的某個方法進(jìn)行攔截,如 prepare,代表攔截 StatementHandler 的 prepare 方法;args 參數(shù)表示攔截方法的參數(shù),因為方法可能會存在重載,因此方法名加上參數(shù)才能唯一標(biāo)識一個方法。
推斷可知 XXXPlugin 插件會攔截 StatementHandler對象的 prepare(Connection connection, Integer var2) 方法。
一個插件可以攔截多個對象的多個方法,因此在 Intercepts 注解中可以添加上多個 Signature注解。
3. 實(shí)踐
接下來,我們一起來實(shí)現(xiàn)一個簡單的 SQL 執(zhí)行時間計時插件。插件的功能是日志輸出每一條 SQL 的執(zhí)行用時。
在 com.imooc.mybatis 包下,我們新建 plugin 包,并在包中添加 SqlStaticsPlugin 類。SqlStaticsPlugin 會攔截 StatementHandler的prepare方法,如下:
package com.imooc.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();
}
}
我們一起來完善這個插件。
- 首先需要得到 invocation 的攔截對象 StatementHandler,并從 StatementHandler 中拿到 SQL 語句。
- 得到當(dāng)前的時間戳 startTime。
- 執(zhí)行 SQL。
- 得到執(zhí)行后的時間戳 endTime。
- 計算時間差,并打印 SQL 耗時。
對應(yīng)的 intercept 方法代碼如下:
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();
// 執(zhí)行SQL
Object res = invocation.proceed();
// 結(jié)束時間
long endTime = System.currentTimeMillis();
long sqlCost = endTime - startTime;
// 去掉無用的換行符,打印美觀
logger.info("sql: {} - cost: {}ms", sql.replace("\n", ""), sqlCost);
// 返回執(zhí)行的結(jié)果
return res;
}
注意,通過反射調(diào)用后的結(jié)果 res,我們一定要記得返回。MyBatis 提供了 MetaObject 這個類來方便我們進(jìn)行攔截對象屬性的修改,這里我們簡單的使用了getValue
方法來得到 SQL 語句。
我們在全局配置文件注冊這個插件:
<plugins>
<plugin interceptor="com.imooc.mybatis.plugin.SqlStaticsPlugin" />
</plugins>
到這,這個插件已經(jīng)可以工作了,但是我們希望它能更加靈活一點(diǎn),通過配置來攔截某些類型的 SQL,如只計算 select 類型SQL的耗時。
插件會在初始化的時候通過 setProperties 方法來加載配置,利用它我們可以得到哪些方法需要被計時。如下:
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 參數(shù)默認(rèn)可通過 select、insert、update 和 delete 類型的SQL語句,如果插件存在配置項 methods,那么則根據(jù)插件配置來覆蓋默認(rèn)配置。
在全局配置文件中,我們來添加上 methods 這個配置:
<plugins>
<plugin interceptor="com.imooc.mybatis.plugin.SqlStaticsPlugin">
<property name="methods" value="select,update"/>
</plugin>
</plugins>
類型之間以 , 隔開,MyBatis 會在插件初始化時,自動將 methods 對應(yīng)的值通過 setProperties 方法來傳遞給SqlStaticsPlugin插件。插件拿到 Properties 后解析并替換默認(rèn)的 methods 配置。
再次完善一下 intercept 方法,使其支持配置攔截:
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類型,則無需計時,直接返回調(diào)用
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;
}
當(dāng)插件注冊后,應(yīng)用程序會打印出如下的日志語句:
17:48:14.110 [main] INFO com.imooc.mybatis.plugin.SqlStaticsPlugin - sql: INSERT INTO blog(info,tags) VALUES(?, ?) - cost: 87ms
至此,一個簡單的 SQL 計時插件就開發(fā)完畢了。
4. 小結(jié)
- MyBatis 插件強(qiáng)大且易用,是深入掌握 MyBatis 的必備知識點(diǎn)。
- 不少 MyBatis 三方庫都提供了很多好用的插件,如 Pagehelper 分頁插件,我們可以拿來即用。