【SpringBoot + Mybatis系列】插件機制 Interceptor
在 Mybatis 中,插件機制提供了非常強大的擴展能力,在 sql 最終執行之前,提供了四個攔截點,支持不同場景的功能擴展
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
本文將主要介紹一下自定義 Interceptor 的使用姿勢,並給出一個通過自定義插件來輸出執行 sql,與耗時的 case
I. 環境准備
1. 數據庫准備
使用 mysql 作為本文的實例數據庫,新增一張表
CREATE TABLE `money` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL DEFAULT '' COMMENT '用戶名',
`money` int(26) NOT NULL DEFAULT '0' COMMENT '錢',
`is_deleted` tinyint(1) NOT NULL DEFAULT '0',
`create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
`update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
PRIMARY KEY (`id`),
KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
2. 項目環境
本文借助 SpringBoot 2.2.1.RELEASE
+ maven 3.5.3
+ IDEA
進行開發
pom 依賴如下
<dependencies>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
db 配置信息 application.yml
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password:
II. 實例演示
關於 myabtis 的配套 Entity/Mapper 相關內容,推薦查看之前的系列博文,這里就不貼出來了,將主要集中在 Interceptor 的實現上
1. 自定義 interceptor
實現一個自定義的插件還是比較簡單的,試下org.apache.ibatis.plugin.Interceptor
接口即可
比如定義一個攔截器,實現 sql 輸出,執行耗時輸出
@Slf4j
@Component
@Intercepts(value = {@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
})
public class ExecuteStatInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// MetaObject 是 Mybatis 提供的一個用於訪問對象屬性的對象
MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
BoundSql sql = statement.getBoundSql(invocation.getArgs()[1]);
long start = System.currentTimeMillis();
List<ParameterMapping> list = sql.getParameterMappings();
OgnlContext context = (OgnlContext) Ognl.createDefaultContext(sql.getParameterObject());
List<Object> params = new ArrayList<>(list.size());
for (ParameterMapping mapping : list) {
params.add(Ognl.getValue(Ognl.parseExpression(mapping.getProperty()), context, context.getRoot()));
}
try {
return invocation.proceed();
} finally {
System.out.println("------------> sql: " + sql.getSql() + "\n------------> args: " + params + "------------> cost: " + (System.currentTimeMillis() - start));
}
}
@Override
public Object plugin(Object o) {
return Plugin.wrap(o, this);
}
@Override
public void setProperties(Properties properties) {
}
}
注意上面的實現,核心邏輯在intercept
方法,內部實現 sql 獲取,參數解析,耗時統計
1.1 sql 參數解析說明
上面 case 中,對於參數解析,mybatis 是借助 Ognl 來實現參數替換的,因此上面直接使用 ognl 表達式來獲取 sql 參數,當然這種實現方式比較粗暴
// 下面這一段邏輯,主要是OGNL的使用姿勢
OgnlContext context = (OgnlContext) Ognl.createDefaultContext(sql.getParameterObject());
List<Object> params = new ArrayList<>(list.size());
for (ParameterMapping mapping : list) {
params.add(Ognl.getValue(Ognl.parseExpression(mapping.getProperty()), context, context.getRoot()));
}
除了上面這種姿勢之外,我們知道最終 mybatis 也是會實現 sql 參數解析的,如果有分析過源碼的小伙伴,對下面這種姿勢應該比較熟悉了
源碼參考自: org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters
BoundSql sql = statementHandler.getBoundSql();
DefaultParameterHandler handler = (DefaultParameterHandler) statementHandler.getParameterHandler();
Field field = handler.getClass().getDeclaredField("configuration");
field.setAccessible(true);
Configuration configuration = (Configuration) ReflectionUtils.getField(field, handler);
// 這種姿勢,與mybatis源碼中參數解析姿勢一直
//
MetaObject mo = configuration.newMetaObject(sql.getParameterObject());
List<Object> args = new ArrayList<>();
for (ParameterMapping key : sql.getParameterMappings()) {
args.add(mo.getValue(key.getProperty()));
}
但是使用上面這種姿勢,需要注意並不是所有的切點都可以生效;這個涉及到 mybatis 提供的四個切點的特性,這里也就不詳細進行展開,在后面的源碼篇,這些都是繞不過去的點
1.2 Intercepts 注解
接下來重點關注一下類上的@Intercepts
注解,它表明這個類是一個 mybatis 的插件類,通過@Signature
來指定切點
其中的 type, method, args 用來精確命中切點的具體方法
如根據上面的實例 case 進行說明
@Intercepts(value = {@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
})
首先從切點為Executor
,然后兩個方法的執行會被攔截;這兩個方法的方法名分別是query
, update
,參數類型也一並定義了,通過這些信息,可以精確匹配Executor
接口上定義的類,如下
// org.apache.ibatis.executor.Executor
// 對應第一個@Signature
<E> List<E> query(MappedStatement var1, Object var2, RowBounds var3, ResultHandler var4) throws SQLException;
// 對應第二個@Signature
int update(MappedStatement var1, Object var2) throws SQLException;
1.3 切點說明
mybatis 提供了四個切點,那么他們之間有什么區別,什么樣的場景選擇什么樣的切點呢?
一般來講,攔截ParameterHandler
是最常見的,雖然上面的實例是攔截Executor
,切點的選擇,主要與它的功能強相關,想要更好的理解它,需要從 mybatis 的工作原理出發,這里將只做最基本的介紹,待后續源碼進行詳細分析
- Executor:代表執行器,由它調度 StatementHandler、ParameterHandler、ResultSetHandler 等來執行對應的 SQL,其中 StatementHandler 是最重要的。
- StatementHandler:作用是使用數據庫的 Statement(PreparedStatement)執行操作,它是四大對象的核心,起到承上啟下的作用,許多重要的插件都是通過攔截它來實現的。
- ParameterHandler:是用來處理 SQL 參數的。
- ResultSetHandler:是進行數據集(ResultSet)的封裝返回處理的,它非常的復雜,好在不常用。
借用網上的一張 mybatis 執行過程來輔助說明
原文 https://blog.csdn.net/weixin_39494923/article/details/91534658
2. 插件注冊
上面只是自定義插件,接下來就是需要讓這個插件生效,也有下面幾種不同的姿勢
2.1 Spring Bean
將插件定義為一個普通的 Spring Bean 對象,則可以生效
2.2 SqlSessionFactory
直接通過SqlSessionFactory
來注冊插件也是一個非常通用的做法,正如之前注冊 TypeHandler 一樣,如下
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(
// 設置mybatis的xml所在位置,這里使用mybatis注解方式,沒有配置xml文件
new PathMatchingResourcePatternResolver().getResources("classpath*:mapping/*.xml"));
// 注冊typehandler,供全局使用
bean.setTypeHandlers(new Timestamp2LongHandler());
bean.setPlugins(new SqlStatInterceptor());
return bean.getObject();
}
2.3 xml 配置
習慣用 mybatis 的 xml 配置的小伙伴,可能更喜歡使用下面這種方式,在mybatis-config.xml
全局 xml 配置文件中進行定義
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//ibatis.apache.org//DTD Config 3.1//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 駝峰下划線格式支持 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<typeAliases>
<package name="com.git.hui.boot.mybatis.entity"/>
</typeAliases>
<!-- type handler 定義 -->
<typeHandlers>
<typeHandler handler="com.git.hui.boot.mybatis.handler.Timestamp2LongHandler"/>
</typeHandlers>
<!-- 插件定義 -->
<plugins>
<plugin interceptor="com.git.hui.boot.mybatis.interceptor.SqlStatInterceptor"/>
<plugin interceptor="com.git.hui.boot.mybatis.interceptor.ExecuteStatInterceptor"/>
</plugins>
</configuration>
3. 小結
本文主要介紹 mybatis 的插件使用姿勢,一個簡單的實例演示了如果通過插件,來輸出執行 sql,以及耗時
自定義插件實現,重點兩步
- 實現接口
org.apache.ibatis.plugin.Interceptor
@Intercepts
注解修飾插件類,@Signature
定義切點
插件注冊三種姿勢:
- 注冊為 Spring Bean
- SqlSessionFactory 設置插件
- myabtis.xml 文件配置
III. 不能錯過的源碼和相關知識點
0. 項目
- 工程:https://github.com/liuyueyi/spring-boot-demo
- 源碼:https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/104-mybatis-ano
- 源碼:https://github.com/liuyueyi/spring-boot-demo/tree/master/spring-boot/103-mybatis-xml
mybatis 系列博文
- 【DB 系列】SpringBoo 系列 Mybatis 之自定義類型轉換 TypeHandler
- 【DB 系列】SpringBoot 系列 Mybatis 之 Mapper 接口與 Sql 綁定幾種姿勢
- 【DB 系列】SpringBoot 系列 Mybatis 之 Mapper 注冊的幾種方式
- 【DB 系列】Mybatis-Plus 多數據源配置
- 【DB 系列】Mybatis 基於 AbstractRoutingDataSource 與 AOP 實現多數據源切換
- 【DB 系列】Mybatis 多數據源配置與使用
- 【DB 系列】JdbcTemplate 之多數據源配置與使用
- 【DB 系列】Mybatis-Plus 代碼自動生成
- 【DB 系列】MybatisPlus 整合篇
- 【DB 系列】Mybatis+注解整合篇
- 【DB 系列】Mybatis+xml 整合篇
1. 微信公眾號:一灰灰 Blog
盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的個人博客,記錄所有學習和工作中的博文,歡迎大家前去逛逛
- 一灰灰 Blog 個人博客 https://blog.hhui.top
- 一灰灰 Blog-Spring 專題博客 http://spring.hhui.top