MyBatis Interceptor 深度解析與應用實踐
一、MyBatis Interceptor概述
1.1 什么是MyBatis Interceptor
MyBatis Interceptor,也稱為MyBatis 插件,是 MyBatis 提供的一種擴展機制,用于在 MyBatis 執行 SQL 的過程中插入自定義邏輯。它類似于 AOP 中的切面或攔截器概念,可在不修改核心 MyBatis 源碼的情況下,對核心對象(如執行器、語句處理器等)的方法調用進行攔截和增強。開發者通過實現 org.apache.ibatis.plugin.Interceptor
接口,并在類上使用 @Intercepts
注解指定需要攔截的方法簽名,MyBatis 會在運行時為目標對象動態生成代理,在對應方法執行前后調用攔截器邏輯,實現諸如日志審計、性能監控、SQL 改寫等功能。
簡而言之,MyBatis Interceptor 是 MyBatis 提供的插件式攔截機制,允許開發者“插入”代碼到 MyBatis 核心流程中,從而實現對 SQL 執行流程的監控、修改或增強。典型的攔截目標包括 SQL 執行前后的操作、參數綁定、結果處理等環節。與 Spring AOP 不同,MyBatis Interceptor 專注于 MyBatis 的內部執行過程,可看作 MyBatis 專屬的“攔截器”。
1.2 MyBatis Interceptor的核心價值
MyBatis 攔截器的核心價值在于 靈活擴展和解耦關注點。通過攔截器,開發者可以將一些常見的橫切關注點邏輯(如日志記錄、性能統計、安全校驗、動態 SQL 構建等)模塊化,不侵入業務代碼地整合到 MyBatis 層。這帶來以下好處:
-
增強擴展能力:無需修改 MyBatis 核心源碼,即可在查詢、更新等關鍵時刻插入自定義邏輯。例如,可以記錄每條 SQL 的執行時間、自動為分頁添加限制、對敏感數據進行脫敏等。
-
關注點分離:將通用功能(如日志、監控、安全)從業務代碼中剝離,使業務邏輯更清晰,同時方便統一維護和測試。
-
復用性:一個攔截器插件可以在不同項目中復用,只需根據需要注冊即可,提升開發效率。
-
低侵入:與直接在業務層或 DAO 層寫重復代碼相比,攔截器只需配置一次即可全局生效,降低維護成本。
通過這些優勢,MyBatis Interceptor 成為提升項目可維護性、可擴展性的重要工具,尤其適用于需要統一管控 SQL 行為的場景(如審計、安全、性能調優等)。
1.3 MyBatis Interceptor與其他框架的對比
MyBatis 攔截器與其他常見技術的比較主要體現在其攔截范圍和方式上:
-
與 Spring AOP 對比:Spring AOP 主要針對普通的 Java Bean 方法調用進行橫切(如服務層或 DAO 層方法)。而 MyBatis 攔截器專注于MyBatis 的內部執行流程,攔截的是 MyBatis 核心接口的方法調用(如
Executor.update
、StatementHandler.prepare
等)。Spring AOP 通常需要在 Bean 方法上織入代理,而 MyBatis 插件直接對 MyBatis 框架級別的組件做代理,關注點更底層。二者可以結合使用,但職責不同。 -
與 Hibernate Interceptor 對比:Hibernate 提供的
Interceptor
接口側重于實體對象的生命周期(如保存、刪除事件等),而 MyBatis 插件攔截的是 SQL 執行流程本身。Hibernate 攔截器更關注 ORM 映射層面,MyBatis 插件則對 SQL 和參數處理過程進行干預。 -
與 Servlet 過濾器對比:Servlet 過濾器作用于 Web 請求流程,攔截 HTTP 請求和響應;MyBatis 攔截器作用于數據庫層,攔截 SQL 語句的準備和執行。盡管都是責任鏈模式的應用,但層級不同,一個是應用級別,一個是持久層級別。
綜上,MyBatis Interceptor 屬于 MyBatis 專用的插件化機制,定位于數據訪問層的 SQL 執行流程管控,與其他通用 AOP 框架并不沖突,而是可補充和專注于 ORM 層面的需求。
二、MyBatis Interceptor原理詳解
2.1 攔截器的工作機制
MyBatis 攔截器的工作機制基于 Java 動態代理 實現,主要涉及以下幾個部分:
-
Interceptor 接口:所有自定義攔截器需實現
org.apache.ibatis.plugin.Interceptor
接口,其中定義了intercept(Invocation)
、plugin(Object)
和setProperties(Properties)
三個方法。業務邏輯主要寫在intercept
方法中,plugin
用于包裝目標對象,setProperties
用于接收配置屬性。 -
@Intercepts 注解:自定義攔截器類上必須標注
@Intercepts({@Signature(...)})
注解,用以聲明攔截哪些類型(Executor、StatementHandler 等)及其對應的方法簽名。MyBatis 在加載攔截器時會讀取注解信息,構建一個 signatureMap(映射要攔截的類到方法集合)。 -
InterceptorChain:配置解析完成后,MyBatis 將所有自定義的攔截器對象注冊到
org.apache.ibatis.plugin.InterceptorChain
中,該鏈表按照配置順序保存攔截器實例。 -
目標對象動態代理:當 MyBatis 創建核心組件(如 Executor、StatementHandler、ParameterHandler、ResultSetHandler)時,會調用
InterceptorChain.pluginAll(target)
方法。該方法遍歷攔截器鏈,對目標對象逐一執行interceptor.plugin(target)
,即對目標對象進行動態代理包裝。實際上,plugin(target)
默認實現為Plugin.wrap(target, this)
,其中 Plugin 采用 JDK 代理創建一個代理對象,代理時會攔截實現了指定接口的方法調用。 -
Invocation 觸發:執行 SQL 操作時,實際調用會落到代理對象上。代理對象的
InvocationHandler
會判斷當前方法是否在前面@Signature
指定的方法列表中,如果是則會構造一個Invocation
對象并調用interceptor.intercept(invocation)
;否則直接調用目標對象的原方法。Invocation
封裝了目標對象(target)、方法(method)和參數(args),并提供proceed()
方法用于繼續調用下一個攔截器或最終的目標方法。
流程示例:在執行 Executor.update()
時,實際會調用動態代理對象的 invoke
方法,該方法檢測到 update
是攔截目標,就執行自定義攔截器的 intercept
邏輯。在自定義攔截器中可以在 Invocation.proceed()
前后添加自己的處理。最終,當所有攔截器鏈條執行完畢后,才真正調用底層的 Executor 完成數據庫操作。
2.2 攔截器的執行流程(結合源碼分析)
結合源碼,可總結 MyBatis 攔截器的執行流程如下:
-
加載注冊攔截器:在解析 MyBatis 配置文件時,識別
<plugins>
節點并實例化配置的攔截器類(自動調用無參構造),然后調用Configuration.addInterceptor(interceptor)
將其加入InterceptorChain
(此時addInterceptor
內部其實是調用InterceptorChain.addInterceptor
,見源碼[20])。 -
創建核心對象并包裝:當 MyBatis 初始化時創建
Executor
、StatementHandler
、ParameterHandler
、ResultSetHandler
等對象時,會調用configuration.getInterceptorChain().pluginAll(target)
。例如在DefaultSqlSession
中創建Executor
后即包裝之(偽代碼示例):Executor executor = new SimpleExecutor(...); executor = (Executor) configuration.getInterceptorChain().pluginAll(executor);
此時,InterceptorChain 會遍歷所有注冊的攔截器,對 executor 對象進行包裝,形成多層動態代理:
target = interceptor1.plugin(target); target = interceptor2.plugin(target); // ...
具體源碼中
pluginAll
方法顯示:public Object pluginAll(Object target) {for (Interceptor interceptor : interceptors) {target = interceptor.plugin(target);}return target; }
-
動態代理攔截:
Interceptor.plugin(Object target)
的默認實現是Plugin.wrap(target, this)
(見)。Plugin.wrap
方法首先通過getSignatureMap(interceptor)
讀取攔截器類上的@Intercepts
注解信息,得到一個映射關系,指明要攔截的類型及方法集合。然后,判斷目標對象是否實現了這些接口中的任意一個:如果有匹配,則通過Proxy.newProxyInstance
創建代理對象;否則直接返回原對象。 -
代理對象的
invoke
邏輯(見源碼):當代理對象的方法被調用時,會進入Plugin.invoke
方法。該方法從signatureMap
中取出本次調用的目標方法所在接口的Method
集合,如果調用的方法在其中,則執行interceptor.intercept(new Invocation(target, method, args))
;否則調用method.invoke(target, args)
直接執行原方法。如下關鍵片段:@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {Set<Method> methods = signatureMap.get(method.getDeclaringClass());if (methods != null && methods.contains(method)) {// 方法匹配,調用自定義攔截器邏輯return interceptor.intercept(new Invocation(target, method, args));}// 方法未配置攔截,直接調用目標方法return method.invoke(target, args); }
-
執行自定義邏輯并繼續調用:進入
interceptor.intercept(Invocation)
后,即執行用戶在攔截器中實現的邏輯。Invocation
對象封裝了target
、method
、args
,可通過調用invocation.proceed()
來繼續調用下一個攔截器鏈條或目標方法。在intercept
方法內部,開發者可以在invocation.proceed()
前后添加自己的業務邏輯,例如記錄日志、修改參數、性能計時等。Invocation.proceed()
最終會調用原始對象的方法(或下一個攔截器),完成真實的數據庫操作。
以上流程說明了 MyBatis 插件機制的核心原理:通過動態代理將多個攔截器按配置順序“串聯”在目標對象上,調用方法時逐個進入攔截器的 intercept
方法,最后執行目標方法。這樣就形成了一條攔截器鏈(責任鏈模式)的執行路徑,實現了對 MyBatis SQL 執行過程的靈活增強。
2.3 攔截器的生命周期管理
MyBatis 攔截器的生命周期主要由 MyBatis 框架自行管理,特征如下:
-
單例模式:在配置加載階段,MyBatis 通過反射創建攔截器對象(通常是單例的,可以在 MyBatis 配置或 Spring 容器中配置)。一個
SqlSessionFactory
生成期間,配置的每個攔截器類只會被實例化一次,保存到InterceptorChain
中。之后執行的所有 SQL 操作都共享這些攔截器實例,因此要求攔截器實現是線程安全的(參見 5.2 小節)。 -
配置屬性注入:對于
<plugin>
配置中的<property>
屬性,MyBatis 在創建攔截器實例后會調用setProperties(Properties)
方法,將配置的屬性傳入攔截器,以便初始化參數或外部配置。可以利用這一機制動態改變攔截器行為。 -
與 SqlSessionFactory 關聯:攔截器實例存儲在
Configuration
對象中,Configuration
決定了SqlSessionFactory
的行為。只有在配置了特定攔截器時,才會加入攔截鏈。通常情況下,攔截器生命周期等同于整個SqlSessionFactory
生命周期。 -
支持熱重載(可選):在開發環境或動態運行時,部分項目可能通過自定義手段實現攔截器的熱插拔,例如重新加載配置或用 Spring 上下文刷新插件。MyBatis 本身并不內置攔截器的熱重載機制,但由于可編程性強,可以結合 Spring 等框架提供類似功能。
總體上,MyBatis 攔截器由框架自動創建、配置與調用,開發者無需手動管理其創建和銷毀。需要注意的是,由于其單例且跨線程調用的特性,在設計攔截器時應避免使用非線程安全的可變共享狀態。后續5.2節將詳細討論攔截器的線程安全及狀態管理策略。
三、自定義攔截器開發指南
開發自定義 MyBatis 攔截器需要關注注解配置、核心方法實現、注冊方式和測試等方面。以下將逐步介紹具體步驟和注意事項。
3.1 攔截器注解的使用規范
自定義攔截器類必須使用 @Intercepts
注解來聲明要攔截的目標接口和方法。常見使用示例如下:
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class ExampleInterceptor implements Interceptor {// ...
}
注解說明:
-
@Intercepts:用于標記該類是 MyBatis 攔截器,包含多個
@Signature
條目。 -
@Signature 屬性:
-
type
:要攔截的接口類型(常見有Executor
、StatementHandler
、ParameterHandler
、ResultSetHandler
等)。 -
method
:接口中的方法名(字符串)。 -
args
:方法參數類型數組,對應該方法的參數列表。類型必須準確,否則會找不到對應方法。
-
多組 @Signature
表明一個攔截器可以攔截多個目標方法。使用時需確保簽名對應的方法在指定接口中存在。例如 Executor.update(MappedStatement, Object)
表示攔截 Executor 的 update 方法。MyBatis 在構建 signatureMap
時會通過反射獲取指定類型的 Method
對象(見源碼mybatis.org),找不到會報錯。
常用攔截類型與方法:
-
Executor
(執行器):update
,query
,flushStatements
,commit
,rollback
,getTransaction
,close
,isClosed
等。 -
StatementHandler
(語句處理器):prepare
,parameterize
,batch
,update
,query
等。 -
ParameterHandler
(參數處理器):getParameterObject
,setParameters
。 -
ResultSetHandler
(結果集處理器):handleResultSets
,handleOutputParameters
。
可根據需要選擇合適的類型和方法。以下示例展示了注解聲明及攔截器類定義的典型結構:
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SqlLogInterceptor implements Interceptor {// 實現攔截邏輯
}
3.2 攔截器核心方法的實現要點
自定義攔截器需要實現 Interceptor
接口,其核心方法包括:
-
intercept(Invocation invocation)
:攔截邏輯的入口。Invocation
對象封裝了當前攔截的目標對象(target
)、方法(method
)和參數(args
)。開發者應在此方法中編寫自定義處理邏輯,然后通過invocation.proceed()
繼續調用下一個攔截器或目標方法。示例實現:@Override public Object intercept(Invocation invocation) throws Throwable {// 前置處理邏輯(例如日志、參數檢查等)System.out.println("==> 執行 SQL 之前");// 執行下一個攔截器或實際方法Object result = invocation.proceed();// 后置處理邏輯(例如結果處理、性能統計等)System.out.println("==> 執行 SQL 之后");return result; }
關鍵點:
-
調用
proceed()
:必須調用invocation.proceed()
以繼續執行鏈條,否則會阻斷 SQL 的執行。可以在proceed()
前后分別實現前置和后置邏輯。 -
返回值:通常將
invocation.proceed()
的返回值原樣返回,或在其基礎上進行修改(例如對查詢結果進行包裝、過濾等)。 -
異常處理:
intercept
方法簽名允許拋出Throwable
,出現異常時可捕獲并根據需要處理或包裝后拋出。異常會傳遞給調用者,并可在 MyBatis 層進行統一處理。
-
-
plugin(Object target)
:用于將攔截器應用于目標對象。通常實現只需調用Plugin.wrap(target, this)
。該方法會根據前述簽名決定是否生成代理對象。示例:@Override public Object plugin(Object target) {// 將攔截器與目標對象通過動態代理綁定return Plugin.wrap(target, this); }
一般無需修改此邏輯,但在特殊場景下可以增加對代理創建過程的自定義控制。
Plugin.wrap
方法內部會檢查目標對象是否實現了簽名中指定的接口,若符合條件則生成代理,否則返回原對象。 -
setProperties(Properties properties)
:接收<plugin>
配置中的屬性,用于配置攔截器。例如可以通過properties.getProperty("key")
獲取指定屬性值并初始化攔截器。示例:private Properties properties;@Override public void setProperties(Properties properties) {this.properties = properties;// 可讀取屬性進行初始化String logLevel = properties.getProperty("logLevel", "INFO");System.out.println("設置攔截器屬性:logLevel=" + logLevel); }
在 MyBatis 配置文件中使用時,可為每個
<plugin>
標簽添加<property name="..." value="..."/>
來傳遞參數。
3.3 攔截器配置與注冊方法
自定義攔截器需在 MyBatis 配置中注冊,常見方式有兩種:
-
MyBatis XML 配置(適用于無 Spring 場景):在
mybatis-config.xml
的<plugins>
節點中添加<plugin>
配置。例如:<plugins><plugin interceptor="com.example.MyInterceptor"><property name="threshold" value="1000"/><property name="logSql" value="true"/></plugin> </plugins>
上例將
com.example.MyInterceptor
添加到攔截鏈,同時傳入兩個配置屬性(可以在setProperties
方法中讀取)。注意interceptor
屬性填寫攔截器類的完整類名。 -
Spring Boot 或 Spring 集成:在 Spring 環境中可以通過 Java 配置或 Bean 注冊攔截器。常用方式包括:
-
SqlSessionFactoryBean/SqlSessionTemplate 設置:在 Spring 配置類中,獲取
SqlSessionFactoryBean
或SqlSessionFactory
,然后調用setPlugins
方法注入攔截器數組。例如:@Configuration public class MyBatisConfig {@Beanpublic SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();factoryBean.setDataSource(dataSource);factoryBean.setPlugins(new Interceptor[]{ new MyInterceptor() });return factoryBean.getObject();} }
這樣
MyInterceptor
將被注冊為 MyBatis 的插件。 -
MyBatis-Spring-Boot-Starter 方式(自動掃描 Bean):若使用 MyBatis Spring Boot Starter,可簡單地將攔截器聲明為 Spring Bean,例如在配置類中:
@Bean public MyInterceptor myInterceptor() {MyInterceptor interceptor = new MyInterceptor();Properties props = new Properties();props.setProperty("threshold", "1000");interceptor.setProperties(props);return interceptor; }
MyBatis-Spring 將自動將所有實現了
Interceptor
接口的 Bean 加入攔截器鏈。部分新版 Starter 也支持通過application.yml
指定插件,但更常見的還是通過 Bean 注入完成。
-
攔截器調用鏈測試用例:為了驗證攔截器的功能,通常會編寫單元測試。例如,以下示例測試創建一個簡單目標接口,并在調用時確保攔截器被觸發:
public interface Foo {void bar();
}
public class FooImpl implements Foo {@Overridepublic void bar() {System.out.println("原始業務方法 bar() 執行");}
}
@Intercepts({@Signature(type = Foo.class, method = "bar", args = {})
})
public class FooInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {System.out.println("==> FooInterceptor:方法調用之前");Object result = invocation.proceed();System.out.println("==> FooInterceptor:方法調用之后");return result;}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {}
}
// JUnit 測試代碼
public class InterceptorTest {@Testpublic void testFooInterceptor() {Foo target = new FooImpl();Foo proxy = (Foo) Plugin.wrap(target, new FooInterceptor());proxy.bar();// 預期輸出:// ==> FooInterceptor:方法調用之前// 原始業務方法 bar() 執行// ==> FooInterceptor:方法調用之后}
}
上述測試中,FooImpl.bar()
的調用被 FooInterceptor
成功攔截,從而在方法前后輸出了額外日志,驗證了攔截器調用鏈的正確性。
四、MyBatis Interceptor應用場景
MyBatis 攔截器在實際項目中有多種應用場景,以下介紹常見幾類,并提供示例代碼。
4.1 SQL分頁插件開發
場景需求:在查詢時自動為 SQL 添加分頁參數,無需在 XML 或注解中手動拼接分頁條件。常見做法是攔截語句處理器,在 StatementHandler.prepare()
階段修改即將執行的 SQL。
實現思路:攔截 StatementHandler.prepare(Connection, Integer)
方法,獲取原始 SQL 并在其后添加分頁語句(以 MySQL 為例為 LIMIT offset, size
)。可以通過反射修改 BoundSql
中的 SQL 字符串。示例:
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class PaginationInterceptor implements Interceptor {private static final String MYSQL = "mysql";// 從配置或上下文中獲取當前分頁參數private int offset;private int limit;@Overridepublic Object intercept(Invocation invocation) throws Throwable {StatementHandler handler = (StatementHandler) invocation.getTarget();// 使用反射獲取實際的RoutingStatementHandler和BoundSqlBoundSql boundSql = handler.getBoundSql();String originalSql = boundSql.getSql().trim();// 根據數據庫方言構建分頁 SQLString pageSql = originalSql + " LIMIT " + offset + ", " + limit;// 通過 MetaObject 修改原 BoundSql 的 SQLMetaObject metaStatementHandler = SystemMetaObject.forObject(handler);metaStatementHandler.setValue("delegate.boundSql.sql", pageSql);System.out.println("分頁攔截器修改SQL: " + pageSql);return invocation.proceed();}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {this.offset = Integer.parseInt(properties.getProperty("offset", "0"));this.limit = Integer.parseInt(properties.getProperty("limit", "10"));}
}
注冊配置示例(mybatis-config.xml):
<plugins><plugin interceptor="com.example.PaginationInterceptor"><property name="offset" value="0"/><property name="limit" value="20"/></plugin>
</plugins>
上述攔截器在每次 prepare
時自動為 SQL 追加 LIMIT
分頁語句,實現了簡易的服務器端分頁功能。實際使用中還可結合 RowBounds
或其它分頁參數動態設置 offset
、limit
。
4.2 SQL日志記錄與審計
場景需求:記錄所有執行的 SQL 語句及其參數,方便調試和審計。可以在每次查詢或更新前后打印 SQL。
實現思路:攔截 StatementHandler
的 prepare
方法獲取 SQL,也可以攔截 Executor.query/update
方法獲取參數或計時。示例如下,在 prepare
階段日志輸出 SQL:
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SqlLogInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {StatementHandler handler = (StatementHandler) invocation.getTarget();BoundSql boundSql = handler.getBoundSql();String sql = boundSql.getSql().replaceAll("\\s+", " ");System.out.println("[SQL日志] 執行 SQL: " + sql);return invocation.proceed();}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {}
}
使用示例:
<plugins><plugin interceptor="com.example.SqlLogInterceptor"/>
</plugins>
每次執行 SQL 時,上述攔截器都會打印標準化后的 SQL 語句到控制臺或日志文件。結合日志框架(如 Log4j、SLF4J)可將輸出記錄到文件或監控系統中。
4.3 性能監控與調優
場景需求:監控 SQL 執行時間、慢查詢告警或統計接口響應時間,以便性能優化。
實現思路:攔截執行語句的環節(如 StatementHandler.query/update
或 Executor.query/update
),在 intercept
方法中記錄開始時間和結束時間,計算耗時,超過閾值時打印警告。示例代碼:
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class PerformanceInterceptor implements Interceptor {private long threshold = 500; // 毫秒@Overridepublic Object intercept(Invocation invocation) throws Throwable {long start = System.currentTimeMillis();Object result = invocation.proceed();long end = System.currentTimeMillis();long time = end - start;if (time > threshold) {MappedStatement ms = (MappedStatement) invocation.getArgs()[0];System.err.println("[性能警告] SQL 執行超時 " + time + " ms - " + ms.getId());}return result;}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {this.threshold = Long.parseLong(properties.getProperty("threshold", "500"));}
}
注冊示例:
<plugins><plugin interceptor="com.example.PerformanceInterceptor"><property name="threshold" value="300"/></plugin>
</plugins>
該攔截器對所有 Executor.update
和 Executor.query
進行監控,記錄每次調用耗時。當耗時超過配置的閾值(如300毫秒)時,輸出警告信息。這樣可用于統計慢查詢,并在開發或生產環境中給予提示,幫助定位性能瓶頸。
4.4 數據脫敏與安全校驗
場景需求:在從數據庫查詢并返回結果前,對敏感數據進行脫敏處理(如隱去身份證號中間位、手機號后幾位),或對輸入參數做安全校驗。
實現思路:可以攔截 ResultSetHandler.handleResultSets(ResultSet)
方法,對返回的結果集進行遍歷和處理;也可以攔截 ParameterHandler.setParameters
方法,對 SQL 參數進行校驗或修改。下面演示對返回結果進行脫敏的攔截器示例:
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
})
public class DataMaskInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {List<Object> results = (List<Object>) invocation.proceed();if (results != null) {for (Object obj : results) {if (obj instanceof User) { // 假設返回的是 User 對象列表User user = (User) obj;String email = user.getEmail();// 簡單示例:只顯示郵箱前3位,其余替換為星號if (email != null && email.length() > 3) {user.setEmail(email.substring(0, 3) + "****");}}}}return results;}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {}
}
在這個示例中,對返回的 List<Object>
進行遍歷,如果元素是 User
對象,就對其 email
字段進行脫敏處理。注冊該攔截器后,所有查詢結果中的 email
字段都會在返回給調用方之前經過該邏輯脫敏。類似地,可以在插入或更新前校驗參數或在查詢前校驗用戶權限等。
4.5 動態SQL構建與參數處理
場景需求:在不修改原 SQL 的情況下,根據某些條件動態更改 SQL 或參數。例如在執行前自動為 SQL 追加查詢條件,或修改輸入參數。
實現思路:可攔截 Executor.update
或 Executor.query
,分析 MappedStatement
和參數對象,構造新的 SQL 或參數,然后通過 MappedStatement
的 SqlSource
和 BoundSql
生成新的 MappedStatement
。示例思路(簡化版):
@Intercepts({@Signature(type = Executor.class, method = "query",args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})
})
public class DynamicSqlInterceptor implements Interceptor {@Overridepublic Object intercept(Invocation invocation) throws Throwable {MappedStatement ms = (MappedStatement) invocation.getArgs()[0];Object parameter = invocation.getArgs()[1];// 根據業務需求動態生成或修改 SQLBoundSql boundSql = ms.getBoundSql(parameter);String originalSql = boundSql.getSql();// 例如如果參數中包含某個標志,則追加條件if (parameter instanceof Map && ((Map) parameter).containsKey("activeOnly")) {String newSql = originalSql + " AND active = 1";// 這里為了簡單示例,不做 MappedStatement 完全復制// 在實際中可使用 MetaObject 修改boundSql.sql字段System.out.println("[動態SQL攔截] 新SQL: " + newSql);}return invocation.proceed();}@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {}
}
在實際生產場景,完全復制并替換 MappedStatement
及其 SqlSource
需要較多代碼(可參考 MyBatis 源碼中的插件實現邏輯),上述示例僅演示概念。在需要動態 SQL 時,常見做法是結合 MyBatis 提供的 <if>
標簽或 SqlSessionFactory
監聽等,但攔截器也可實現更靈活的邏輯。
五、MyBatis Interceptor性能優化
雖然攔截器功能強大,但錯誤或過多使用會帶來性能負擔。以下優化技巧可供參考。
5.1 攔截器鏈的性能瓶頸分析
-
動態代理開銷:每個攔截目標都會被多重代理包裝,方法調用需經過多次
InvocationHandler.invoke
調用,增加了額外的反射開銷。隨著攔截器數量增多,這種鏈式包裝帶來的開銷成倍增長。 -
頻繁構造對象:如果在攔截器中頻繁創建對象(例如每次調用都 new 復雜對象),會增加垃圾回收壓力。特別是處理大量 SQL 時,應盡量重用可復用對象或使用局部緩沖區。
-
數據結構復雜性:如果攔截器內部邏輯復雜,如使用了深度反射或大規模集合遍歷,會加長單次調用時間。
優化建議:
-
僅攔截必要方法:注解中盡量精確指定需要攔截的方法,避免對不需要的接口進行攔截。
-
合并相似攔截器:如果多個攔截器作用相似,可合并為一個攔截器,在
intercept
內部分支處理,減少代理層數。 -
提前過濾:在
intercept
中盡早檢查條件,不滿足時盡快返回invocation.proceed()
,減少不必要的處理。 -
懶加載:對于某些復雜計算,可考慮僅在滿足特定條件時才執行,避免每次都運行所有代碼。
5.2 攔截器的線程安全設計
攔截器實例通常是單例并在多線程環境中被復用,因此需保證線程安全:
-
無狀態設計:盡量不在攔截器中定義可變成員變量,或將其限定為
final
常量(例如數據庫方言常量)。避免使用非線程安全的數據結構(如非同步的ArrayList
等)作為字段。 -
局部變量:在
intercept
方法中使用局部變量存儲臨時數據,局部變量天然線程隔離。 -
線程局部變量:如果需要在線程間隔離數據,可使用
ThreadLocal
,但需注意可能導致內存泄漏(一定要在請求結束時清理)。 -
同步鎖避免:盡量避免在攔截器中引入鎖(如
synchronized
)會嚴重影響并發性能。如需共享資源,可考慮使用無鎖并發容器或外部緩存系統。
5.3 攔截器的緩存策略
攔截器自身可以引入合理的緩存策略來提升性能:
-
方法/元數據緩存:對于反射獲取的方法或配置,可以在攔截器加載時緩存
Method
對象。MyBatisPlugin
已經將匹配的方法緩存到signatureMap
中,無需重復查找。 -
SQL 模板緩存:若動態修改 SQL 的邏輯復雜,可以將常用的 SQL 模板或修改后 SQL 保存到緩存,減少每次重組 SQL 的開銷。
-
結果/參數緩存:如果某些攔截邏輯依賴于計算結果(如脫敏規則表、加密密鑰等),可將此類靜態信息緩存到攔截器實例中,避免頻繁讀取外部資源。
需要注意避免緩存失效帶來的一致性問題。任何緩存必須與底層數據更新機制兼容,例如在數據表變化后清空相關緩存或使用緩存穿透策略。
5.4 攔截器的異步執行方案
對于一些非關鍵路徑的耗時操作,如日志記錄、審計信息存儲等,可考慮異步執行以減少對主線程的阻塞:
-
日志異步化:在攔截器
intercept
中生成要記錄的日志內容后,使用異步框架(如異步隊列、獨立線程、消息系統)將日志寫入任務推送出去,不在當前線程寫文件或數據庫。 -
審計信息異步化:類似日志,將審計數據封裝后放入線程池或消息中間件,讓后臺進程處理。
-
定時任務:對于周期性或批量統計,也可在攔截器中簡單收集數據(如計數、時間等),而真正的匯總和告警由定時任務完成。
異步策略需注意線程安全和異常處理,確保異步任務失敗不會影響主流程,以及及時處理失敗的任務。
六、MyBatis Interceptor的局限性與替代方案
6.1 攔截器的局限性分析
雖然 MyBatis 插件非常靈活,但也存在一些局限,需要合理評估:
-
作用范圍有限:MyBatis 攔截器只能攔截
Executor
、StatementHandler
、ParameterHandler
、ResultSetHandler
這四類接口的方法。對于其他自定義的業務方法、非 MyBatis 的流程,攔截器無能為力。 -
簽名硬編碼:攔截方法需要在注解中硬編碼指定方法名和參數類型,不能模糊匹配或使用表達式。一旦對應方法重命名或參數改變,攔截配置就會失效或拋出異常。
-
與插件沖突:多個插件如果攔截了同一目標,執行順序取決于配置順序(默認后配置的先執行,見第8.1節),容易造成難以預測的交互問題。不同插件間的相互影響需人工管理。
-
調試困難:由于攔截器通過代理隱式工作,對鏈條調試不直觀。排查問題時需依賴日志或斷點,復雜場景下分析鏈路較為繁瑣。
-
性能開銷:如前所述,每個攔截器會帶來額外方法調用開銷,過多插件會明顯拖慢執行速度,需要謹慎使用。
6.2 MyBatis Interceptor與Spring AOP的對比
對比 MyBatis 插件與 Spring AOP,可從下面幾個方面進行分析:
特性 | MyBatis Interceptor | Spring AOP |
---|---|---|
織入對象 | MyBatis 核心接口(Executor、StatementHandler 等) | Spring 管理的 Bean 方法 |
攔截點粒度 | SQL 執行前后、參數綁定、結果處理等數據庫層面 | 方法調用層面,可攔截任何 public 方法(默認) |
配置方式 | MyBatis 配置文件或 MyBatis-Spring 插件方式 | 注解或 XML 配置切面(@Aspect、XML) |
實現方式 | 動態代理/反射(不依賴 AOP 框架) | 代理(JDK/CGLIB)或字節碼增強 |
依賴環境 | 必須在 MyBatis 環境中使用 | 任意 Spring 應用,不依賴數據庫 |
執行時機 | 數據庫操作時(SQL 執行鏈) | 方法執行時 |
使用場景 | 日志、SQL 重寫、性能監控、數據過濾等 | 事務、權限、安全、通用日志等 |
兩者并不沖突,在一個項目中可以同時使用:Spring AOP 用于業務層、服務層的切面邏輯,而 MyBatis 插件專注于數據訪問層面。例如,在業務方法前可以用 AOP 進行權限檢查,用 MyBatis 插件記錄 SQL 日志。
6.3 替代方案:基于責任鏈模式的自定義實現
當 MyBatis 插件的局限無法滿足需求時,可以考慮自定義實現責任鏈模式來達到類似攔截的效果。例如:
-
手動調用鏈:在 DAO 層或服務層封裝一個調用流程,將多個“處理器”串聯,每個處理器實現某個前置或后置邏輯。通過在執行數據庫操作前后手動調用這些處理器,實現與攔截器類似的效果。
-
使用 Spring AOP:如果業務方法正好位于 Spring 管理的 Bean 中,也可以使用 Spring AOP 切面攔截 DAO 層的方法,并在切面中操作
SqlSession
或參數。Spring AOP 的切面靈活度高,但無法像 MyBatis 插件那樣直接操作 SQL。 -
代理包裝 SqlSession:自行對
SqlSession
或 Mapper 接口進行代理,插入攔截邏輯。例如使用 JDK 動態代理或 CGLIB 對 Mapper 接口生成代理類,在調用select
/update
方法時執行預處理或后處理。
這些方案相比 MyBatis 插件更為應用層,需要自行維護鏈條調用邏輯和順序。雖然開發成本可能更高,但在某些特殊場合(如需要插入無法通過 MyBatis API 訪問的步驟)提供了替代可能。
七、MyBatis Interceptor源碼深度解析
進一步深入 MyBatis 源碼,可了解攔截器機制的實現細節。以下重點解析關鍵類:InterceptorChain
、Plugin
和 Invocation
。
7.1 InterceptorChain源碼解析
org.apache.ibatis.plugin.InterceptorChain
維護了所有攔截器實例列表,并負責對目標對象進行包裝。其源碼核心如下:
public class InterceptorChain {private final List<Interceptor> interceptors = new ArrayList<>();public Object pluginAll(Object target) {for (Interceptor interceptor : interceptors) {target = interceptor.plugin(target);}return target;}public void addInterceptor(Interceptor interceptor) {interceptors.add(interceptor);}public List<Interceptor> getInterceptors() {return Collections.unmodifiableList(interceptors);}
}
-
interceptors 列表:存儲所有已注冊的攔截器,添加順序即配置順序。
-
addInterceptor():在
Configuration
解析<plugin>
時調用,將新攔截器加入列表。 -
pluginAll(Object target):將給定對象(如 Executor)依次傳遞給每個攔截器的
plugin
方法。plugin
返回一個可能的代理對象,因此最終得到的是多層嵌套的代理。
該設計確保了多個攔截器可以組合使用。順序上,第一個攔截器的 plugin
先被應用(最里層代理),最后一個攔截器包裹在最外層,導致配置文件中后注冊的攔截器先執行的效果(詳見第8.1節)。
7.2 Plugin類的動態代理實現
Plugin
類實現了 InvocationHandler
,負責創建并處理代理對象的調用。源碼關鍵部分:
public class Plugin implements InvocationHandler {private final Object target;private final Interceptor interceptor;private final Map<Class<?>, Set<Method>> signatureMap;public static Object wrap(Object target, Interceptor interceptor) {Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);Class<?> type = target.getClass();Class<?>[] interfaces = getAllInterfaces(type, signatureMap);if (interfaces.length > 0) {return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));}return target;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {Set<Method> methods = signatureMap.get(method.getDeclaringClass());if (methods != null && methods.contains(method)) {return interceptor.intercept(new Invocation(target, method, args));}return method.invoke(target, args);}// ... 獲取signatureMap和getAllInterfaces方法 ...
}
-
wrap() 方法:獲取攔截器的
@Signature
信息構造signatureMap
,再查找目標類實現的接口,將匹配接口傳入Proxy.newProxyInstance
構造代理。只有當目標類實現了注解中指定的接口時,才會生成代理;否則返回原始對象。 -
invoke() 方法:每次代理對象調用方法時都會進入此處。根據
method.getDeclaringClass()
在signatureMap
查找對應的方法集合,如果當前方法匹配,則構造Invocation
調用攔截器的intercept
;否則直接調用目標對象的方法。 -
getSignatureMap():解析攔截器類上的
@Intercepts
注解,將每個@Signature
的type
、method
和args
轉為java.lang.reflect.Method
并存入Map<Class<?>, Set<Method>>
中。這樣每個目標接口對應一個需要攔截的方法列表。 -
getAllInterfaces():收集目標類及其父類實現的所有接口,篩選出包含在
signatureMap
中的接口,用于代理實現。
通過 Plugin.wrap
,MyBatis 能夠在運行時動態為目標對象生成代理對象,實現對指定方法的攔截。代理對象與目標對象實現了相同接口,調用時透明地進入 Plugin.invoke
,從而觸發開發者編寫的 intercept
邏輯。
7.3 Invocation調用鏈的執行機制
Invocation
是攔截器執行過程中的上下文對象,源碼(見 MyBatis Javadoc)簡單描述如下:
public class Invocation {private final Object target;private final Method method;private final Object[] args;public Invocation(Object target, Method method, Object[] args) { ... }public Object proceed() throws InvocationTargetException, IllegalAccessException {return method.invoke(target, args);}// 還有 getTarget(), getMethod(), getArgs() 等輔助方法
}
-
作用:封裝了攔截點的信息,包含目標對象、目標方法和調用參數。
-
proceed() 方法:當開發者在
intercept
方法中調用invocation.proceed()
時,會繼續調用目標方法或下一個攔截器。實際上,Invocation
持有的target
是原始對象或下一個代理實例,因此調用method.invoke(target, args)
會進入攔截鏈的下一層,最終到達最里層目標實現。 -
調用鏈:如果有多個攔截器鏈在一個方法上,則每次
proceed()
都會觸發鏈中下一個攔截器的intercept
;最后一個攔截器的proceed()
會調用原始目標的方法并返回結果。
在實際代碼中,我們通常不需要直接操作 Invocation
以外的部分,只要遵循**在攔截方法內適時調用 invocation.proceed()
**即可實現鏈式調用。例如,一個簡單的 intercept
中可能是:
public Object intercept(Invocation invocation) throws Throwable {// 攔截前邏輯Object result = invocation.proceed(); // 執行下一個攔截器或原方法// 攔截后邏輯return result;
}
Invocation
的作用機制確保了 責任鏈模式 的串聯執行特性:多個攔截器按順序包裹目標,并逐個執行前置和后置邏輯。
八、進階:攔截器鏈的執行順序控制
8.1 攔截器優先級的配置方法
MyBatis 不提供顯式的優先級配置注解。攔截器的執行順序僅通過配置順序控制:在 mybatis-config.xml
中 <plugins>
節點配置多個 <plugin>
時,后聲明的攔截器會先執行,這與 InterceptorChain.pluginAll()
的實現順序相符(后加入的攔截器處于列表后端,調用時代理層數最外層,先被觸發)。例如:
<plugins><plugin interceptor="com.example.InterceptorA"/><plugin interceptor="com.example.InterceptorB"/>
</plugins>
在這個配置中,InterceptorB
會在 InterceptorA
之前執行(因為 B 被包裝在 A 的外層)。如果需要調整順序,只需調整 <plugin>
標簽的順序即可。MyBatis 官方論壇和文檔也說明了這一點。
8.2 攔截器鏈的調試技巧
調試攔截器鏈可以采取以下方法:
-
日志輸出:在每個攔截器的
intercept
方法中添加日志打印,標識進入和退出方法的位置,或者打印攔截器名稱和方法名。通過日志可了解鏈條中各攔截器執行的先后順序以及方法調用情況。 -
使用斷點:在 IDE 中為每個攔截器類設置斷點,調試時觀察調用棧和攔截鏈情況。由于鏈式調用較復雜,可在
Plugin.invoke
中斷點查看Invocation
對象內容,確認目標方法的真正執行過程。 -
MyBatis 調試日志:啟用 MyBatis SQL 日志(在
log4j.properties
或application.yml
中設置 log level 為 DEBUG),可以看到代理生成的 SQL 語句執行日志,間接反映攔截器是否修改了 SQL。 -
測試覆蓋:編寫單元測試對每個攔截器方法進行覆蓋測試,確保鏈路中各攔截器都按預期運行。可以使用 Mockito 等框架模擬
Invocation
對象來測試intercept
方法的邏輯。
8.3 攔截器鏈的異常處理機制
-
異常傳播:如果攔截器的
intercept
方法中拋出異常,MyBatis 會將該異常向上拋出,最終由調用方捕獲或傳播。開發者應根據需要捕獲并處理攔截器中的異常,或者將其封裝為自定義異常拋出。 -
攔截器內部處理:可以在
intercept
方法內部使用try-catch
捕獲異常,對可預見的錯誤進行處理,并決定是否重新拋出。例如,日志攔截器遇到打印錯誤時一般應捕獲異常避免影響業務流程。 -
鏈中斷:一旦鏈中某個攔截器出現未捕獲異常,后續攔截器將不會執行,直接中斷整個 SQL 調用。需要注意配置順序和異常可能造成的影響范圍。
-
MyBatis 異常轉化:在
Plugin.invoke
中(源碼)會捕獲底層方法拋出的異常并使用ExceptionUtil.unwrapThrowable
進行解包拋出,以保證拋出的是可理解的異常類型。因此,在攔截器中如果使用反射或代理調用目標方法而產生InvocationTargetException
,最終調用方看到的將是其根本原因。
合理的異常處理策略可以保證攔截器出現問題時,系統能快速定位并采取措施,而不會因為攔截器問題導致更嚴重的后果。