在本篇文章中,我們將深入了解如何編寫一個 MyBatis 攔截器,并通過一個示例來展示如何在執行數據庫操作(如插入或更新)時,自動填充某些字段(例如 createdBy
和 updatedBy
)信息。本文將詳細講解攔截器的工作原理、代碼示例及其在 MyBatis 項目中的應用。
一、為什么需要數據庫操作攔截器?
在典型的企業級應用開發中,我們經常會遇到這樣的需求:
-
需要自動記錄數據行的創建時間和修改時間
-
需要跟蹤記錄數據操作人
-
需要實現全局的軟刪除邏輯
-
需要對敏感數據進行自動加密
-
需要實現SQL執行監控和慢查詢統計
傳統做法是在每個Mapper方法中手動添加這些邏輯,但這樣會導致大量重復代碼。MyBatis攔截器(Interceptor)正是為了解決這類問題而生,它可以在SQL執行的各個階段插入自定義邏輯,實現橫切關注點的統一管理。
二、MyBatis攔截器核心原理
如果對于MyBatis核心組件功能還不了解的小伙伴們建議先跳到最后一章節:第五節去了解MyBatis核心組件功能再倒回來繼續學習!
2.1 攔截器架構
MyBatis采用責任鏈模式實現攔截器機制,主要攔截點包括:
攔截接口 | 攔截時機 | 典型應用場景 |
---|---|---|
Executor | SQL執行前后(增刪改查操作) | 事務管理、分頁處理 |
StatementHandler | SQL語句構建時 | SQL改寫、危險操作攔截 |
ParameterHandler | 參數處理時 | 參數加密、參數校驗 |
ResultSetHandler | 結果集處理時 | 結果解密、數據脫敏 |
2.2 攔截器生命周期
MyBatis 中的攔截器生命周期較為簡單。它的生命周期由 Plugin.wrap()
方法控制,首先會創建代理對象并包裝目標對象。攔截器的 intercept
方法在執行目標方法前被調用,攔截器的 plugin
方法用于對目標方法的代理,setProperties
方法用于注入攔截器所需的配置屬性。
攔截目標方法調用的順序:當攔截器裝載到 MyBatis 配置中后,每次執行目標方法(如數據庫操作的 insert
、update
)時,都會經過攔截器鏈。如果多個攔截器存在,它們會按照配置順序依次執行。可以通過實現Ordered接口或使用@Order注解控制執行順序:
@Intercepts(...)
@Order(Ordered.HIGHEST_PRECEDENCE)
public class FirstInterceptor implements Interceptor {}@Intercepts(...)
@Order(Ordered.LOWEST_PRECEDENCE)
public class LastInterceptor implements Interceptor {}
關鍵接口解析
-
初始化階段:通過@Intercepts注解聲明攔截目標
-
代理階段:通過plugin()方法創建代理對象
-
執行階段:intercept()方法處理攔截邏輯
-
配置階段:通過XML或Java Config注冊攔截器
// 典型攔截器聲明
@Intercepts({@Signature(type = Executor.class, method = "update",args = {MappedStatement.class, Object.class})
})
public class CustomInterceptor implements Interceptor {// 實現方法...
}
2.3 代理模式與責任鏈模式
- 代理模式(Proxy Pattern):MyBatis 攔截器本質上是通過代理模式對目標對象進行包裝,通過
Plugin.wrap()
方法將攔截器包裝在目標對象上,形成一個代理對象。這個代理對象會攔截對目標方法的調用,執行預定的增強邏輯,再將控制權交給目標方法。 - 責任鏈模式(Chain of Responsibility Pattern):MyBatis 的攔截器是按順序進行鏈式調用的,多個攔截器會組成一個鏈,按照聲明順序依次執行,直到所有攔截器都被調用。
三、實戰:實現自動化字段填充
3.1 需求分析
實現以下字段的自動填充:
字段名 | 插入時自動填充 | 更新時自動填充 | 數據類型 |
---|---|---|---|
created_by | ?? | ? | String |
created_time | ?? | ? | Date |
updated_by | ? | ?? | String |
updated_time | ? | ?? | Date |
is_deleted | ??(默認0) | ? | Integer |
?3.2 完整實現代碼解析
package com.wanren.subject.application.interceptor;import com.wanren.subject.common.util.LoginUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.Executor;/***@ClassName MybatisInterceptor*@Description 填充createBy,createTime等公共字段的攔截器*@Author 彭于晏*Date 2025/2/14 0:31*Version 1.0**/
@Component
@Slf4j
//@Signature指明該攔截器需要攔截哪一個接口的哪一個方法,包括如下
// 接口類型:Executor(攔截執行器方法,負責調用StatementHandler操作數據庫)
// StatementHandler(//攔截SQL語法構建處理,直接在數據庫執行SQL腳本的對象)、
// ParameterHandler(//攔截參數處理)和ResultSetHandler(//攔截結果集處理,ResultSet結果集對象轉換成List類型的集合)
//對應接口中的某一個方法的參數,比如Executor中query方法因為重載原因,有多個,args就是指明參數類型,從而確定是具體哪一個方法。
@Intercepts(@Signature(type = Executor.class,method = "update",args = {MappedStatement.class,Object.class
}))
public class MybatisInterceptor implements Interceptor {//當被攔截的數據庫操作(如查詢、插入、更新)發生時,intercept 方法會被調用。//invocation的三個方法:Object target = invocation.getTarget();//被代理對象//Method method = invocation.getMethod();//代理方法//Object[] args = invocation.getArgs();//方法參數@Overridepublic Object intercept(Invocation invocation) throws Throwable {MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];//MyBatis 中封裝 SQL 語句信息的類,表示一個 SQL 映射。MappedStatement 對象包含 SQL 執行所需的所有信息,比如 SQL 命令類型、SQL 語句、參數類型等SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();Object parameter = invocation.getArgs()[1];//parameter 是一個 Object 類型,通常是實體類對象if(parameter == null){//執行目標方法(invocation.proceed())return invocation.proceed();}//獲取當前登錄用戶的idString loginId = LoginUtil.getLoginId();if(StringUtils.isBlank(loginId)){return invocation.proceed();}if(sqlCommandType.INSERT == sqlCommandType || sqlCommandType.UPDATE == sqlCommandType){replaceEntityProperty(parameter,loginId,sqlCommandType);}return invocation.proceed();}private void replaceEntityProperty(Object parameter, String loginId, SqlCommandType sqlCommandType) {if(parameter instanceof Map){replaceMap((Map) parameter,loginId,sqlCommandType);}else{replace(parameter,loginId,sqlCommandType);}}private void replace(Object parameter, String loginId, SqlCommandType sqlCommandType) {if(SqlCommandType.INSERT == sqlCommandType){dealInsert(parameter,loginId);}else{dealUpdate(parameter,loginId);}}private void dealUpdate(Object parameter, String loginId) {Field[] fields = getAllFields(parameter);for (Field field : fields) {try {field.setAccessible(true);//parameter是字段名,field是字段的值Object o = field.get(parameter);//如果該字段不為null,則跳過if (Objects.nonNull(o)) {field.setAccessible(false);continue;}if ("updateBy".equals(field.getName())) {field.set(parameter, loginId);field.setAccessible(false);} else if ("updateTime".equals(field.getName())) {field.set(parameter, new Date());field.setAccessible(false);} else {field.setAccessible(false);}}catch (Exception e){log.error("dealUpdate.error:{}", e.getMessage(), e);}}}private void dealInsert(Object parameter, String loginId) {Field[] fields = getAllFields(parameter);for(Field field : fields){try{//Java 語言的私有字段是不可訪問的。如果你想訪問私有字段(即在類外部訪問 private 字段),// 你必須調用 setAccessible(true) 來打破 Java 的訪問控制,允許訪問這些字段。field.setAccessible(true);Object o = field.get(parameter);if (Objects.nonNull(o)) {field.setAccessible(false);continue;}if("isDeleted".equals(field.getName())){field.set(parameter,0);field.setAccessible(false);}else if ("createdBy".equals(field.getName())) {field.set(parameter, loginId);field.setAccessible(false);} else if ("createdTime".equals(field.getName())) {field.set(parameter, new Date());field.setAccessible(false);}else {field.setAccessible(false);}}catch (Exception e){log.error("dealInsert.error:{}", e.getMessage(), e);}}}private Field[] getAllFields(Object parameter) {Class<?> clazz = parameter.getClass();List<Field> fieldList = new ArrayList<>();while (clazz != null){fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));clazz = clazz.getSuperclass();}Field[] fields =new Field[fieldList.size()];fieldList.toArray(fields);return fields;}private void replaceMap(Map parameter, String loginId, SqlCommandType sqlCommandType) {for(Object val : parameter.values()){replace(val,loginId,sqlCommandType);}}//插件用于封裝目標對象,通過這個方法可以返回目標對象本身,也可以返回一個他的代理,決定//是否要進行攔截,從而進一步決定要返回一個怎樣的對象@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}//如果我們攔截器需要用到一些變量參數,而且這個參數是支持可配置的,類似Spring中的@Value("${}")從application.properties文件獲取自定義變量屬性,這個時候我們就可以使用這個方法。//在攔截器插件的setProperties方法中進行。這些自定義屬性參數會在項目啟動的時候被加載。@Overridepublic void setProperties(Properties properties) {}
}
四、擴展應用場景
4.1 敏感數據加密
public Object intercept(Invocation invocation) {Object parameter = invocation.getArgs()[1];if (parameter instanceof User) {User user = (User) parameter;user.setPassword(encrypt(user.getPassword()));}return invocation.proceed();
}
4.2 SQL執行監控?
public Object intercept(Invocation invocation) {long start = System.nanoTime();try {return invocation.proceed();} finally {long cost = (System.nanoTime() - start)/1000000;if(cost > 1000){log.warn("慢查詢警告: {}ms - {}", cost, getSql(invocation));}Metrics.counter("sql.total.count").increment();Metrics.timer("sql.execute.time").record(cost, MILLISECONDS);}
}
4.3 多租戶數據隔離
public void replaceEntityProperty(Object parameter, String tenantId) {if (parameter instanceof TenantAware) {((TenantAware) parameter).setTenantId(tenantId);}
}
?五、MyBatis核心組件功能詳解
從MyBatis代碼實現的角度來看,MyBatis的核心組成部分涉及到多個組件和接口,它們共同協作完成了數據庫操作的管理、映射及優化等工作。以下是MyBatis的核心部件及其功能的詳細介紹:
1. Configuration(配置對象)
功能概述:
Configuration
?是 MyBatis 的全局配置中心,負責管理所有配置信息,包括數據庫連接、映射器、類型處理器、插件等。它是 MyBatis 初始化的核心類。
核心職責:
-
加載并解析 MyBatis 配置文件(如?
mybatis-config.xml
)。 -
管理 Mapper 文件和接口的配置信息。
-
注冊類型處理器(
TypeHandler
)和對象工廠(ObjectFactory
)。 -
維護攔截器鏈(
InterceptorChain
),用于支持插件擴展。
2. SqlSessionFactory(會話工廠)
功能概述:
SqlSessionFactory
?是 MyBatis 的核心工廠類,用于創建?SqlSession
?實例。它是線程安全的,通常在應用啟動時初始化。
核心職責:
-
根據?
Configuration
?創建?SqlSession
。 -
管理數據庫連接池和事務工廠。
-
加載
Mapper
文件和對應的映射語句。
3. SqlSession(會話)
功能概述:
SqlSession
?是 MyBatis 與數據庫交互的核心接口,提供了執行 SQL、管理事務、獲取 Mapper 接口等功能。
核心職責:
-
執行 CRUD 操作(
select
、insert
、update
、delete
)。 -
提供事務管理、批量操作、緩存等功能。
-
獲取 Mapper 接口的代理對象。
4. Executor(執行器)
功能概述:
Executor
是 MyBatis 的內部執行器,負責執行 SQL 語句并處理結果。它是 MyBatis 中的核心對象之一,負責查詢、插入、更新等數據庫操作。
核心職責:
-
調用
StatementHandler
執行 SQL 語句。 -
管理一級緩存和二級緩存。
-
觸發攔截器鏈,支持插件擴展。
5. StatementHandler(語句處理器)
功能概述:
StatementHandler
負責將 SQL 語句發送到數據庫執行,主要完成 SQL 的解析、生成和執行。
核心職責:
-
創建?
PreparedStatement
?并設置參數。 -
執行 SQL 并返回結果。
-
支持一級緩存,避免重復查詢。
6. ParameterHandler(參數處理器)
功能概述:
ParameterHandler
?負責將 Java 對象轉換為 JDBC 參數,并設置到?PreparedStatement
?中。
核心職責:
-
處理 SQL 參數的映射。
-
將 Java 對象轉換為 JDBC 所需的參數格式。
7. ResultSetHandler(結果集處理器)
功能概述:
ResultSetHandler
?負責將 JDBC 返回的?ResultSet
?轉換為 Java 對象集合。
核心職責:
-
解析?
ResultSet
?結果集。 -
將數據庫查詢結果映射為 Java 對象。
8. TypeHandler(類型處理器)
功能概述:
TypeHandler
?用于 Java 類型和數據庫類型之間的轉換。負責將 Java 對象的屬性值轉換為數據庫支持的數據類型,或者將數據庫查詢結果轉化為 Java 對象。
核心職責:
-
處理 Java 類型與 JDBC 類型的映射。
-
支持自定義類型轉換。
9. MappedStatement(映射語句)
功能概述:
MappedStatement
負責封裝單個 <select>、<insert>、<update>、<delete>
等 SQL 語句的配置信息。包括 SQL 類型、參數映射、結果映射等。
核心職責:
-
存儲 SQL 語句、參數映射、返回值映射等信息。
-
提供 SQL 執行的上下文信息。
10. SqlSource(SQL 源)
功能概述:
SqlSource
?負責根據傳入的 parameterObject
動態生成 SQL 語句。它通過 BoundSql
對象封裝 SQL 和參數,并返回給 StatementHandler
執行。
核心職責:
-
動態生成 SQL 語句。
-
根據傳入的參數對象構建 SQL 語句,并封裝到
BoundSql
中。
11. BoundSql(封裝的 SQL)
功能概述:
BoundSql
負責封裝動態生成的 SQL 語句和參數信息。它是 SqlSource
生成 SQL 后的載體,存儲了 SQL 語句及其相關參數信息。
核心職責:
-
存儲動態生成的 SQL 語句。
-
提供 SQL 執行所需的參數信息。
"優秀的框架設計應該像電路板一樣,允許開發者在不修改核心邏輯的情況下,通過插件機制擴展功能。" —— MyBatis設計哲學
?