MyBatis 攔截器,帶你輕松搞定數(shù)據(jù)脫敏!
1. 引言
1.1 什么是 MyBatis 攔截器
MyBatis 攔截器是一種插件機制,用于在 MyBatis 執(zhí)行 SQL 語句時對其進行攔截、修改或增強。攔截器可以插入到 MyBatis 的執(zhí)行過程中的不同位置,從而實現(xiàn)自定義的行為,例如記錄日志、修改 SQL 查詢、增強性能等。
- 定義:MyBatis 攔截器是一種自定義插件,可以通過它攔截 MyBatis 核心組件(如 Executor、StatementHandler、ParameterHandler、ResultSetHandler)的方法調(diào)用。通過攔截器,我們可以在不修改 MyBatis 源碼的情況下,改變其行為或增強其功能。
- 功能:
修改 SQL 語句,例如根據(jù)某些業(yè)務(wù)規(guī)則動態(tài)拼接 SQL。
記錄 SQL 執(zhí)行日志、性能監(jiān)控,統(tǒng)計執(zhí)行時間等。
控制事務(wù)或?qū)崿F(xiàn)緩存邏輯等。
1.2 為什么使用攔截器
使用 MyBatis 攔截器的主要原因是需要在不修改核心代碼的情況下,靈活地擴展 MyBatis 的功能。常見的應(yīng)用場景包括:
- 日志記錄:通過攔截器記錄每個 SQL 語句的執(zhí)行情況,包括 SQL 本身、執(zhí)行時間、返回結(jié)果等信息,用于后期分析和調(diào)試。
- SQL 性能監(jiān)控:攔截器可以用于統(tǒng)計 SQL 執(zhí)行的時間,從而評估 SQL 的性能。長時間執(zhí)行的 SQL 可以被識別出來,作為性能優(yōu)化的目標。
- 修改 SQL 語句:通過攔截器可以動態(tài)修改 SQL 語句,例如,在查詢中動態(tài)插入條件、修改排序規(guī)則,或者添加分頁邏輯。
- 事務(wù)控制:在執(zhí)行 SQL 操作之前、之后,或者在某些異常發(fā)生時,攔截器可以用來增強事務(wù)管理。
2. MyBatis 攔截器工作原理
2.1 攔截器的核心概念
MyBatis 攔截器工作時,核心組件是 Invocation、Interceptor、Method 和 Target 對象等:
- Interceptor:這是所有自定義攔截器的接口,MyBatis 會根據(jù)配置找到并調(diào)用實現(xiàn)該接口的類。
- Invocation:封裝了方法調(diào)用的對象,它包含了目標方法的信息以及方法的參數(shù)。通過 Invocation 對象,我們可以對方法的執(zhí)行進行控制。
- Method:表示目標方法,它是通過反射來獲取的。
- Target:表示目標對象,它是被攔截的對象。例如,Executor、StatementHandler 等都是目標對象,攔截器會通過 Target 對象來訪問和控制這些對象的行為。
2.2 攔截器的生命周期
MyBatis 中,攔截器的生命周期通常包含三個階段:
- 插件初始化:當 MyBatis 啟動時,它會加載并初始化所有配置的攔截器。這時,攔截器會準備好攔截邏輯。
- 攔截執(zhí)行:當 MyBatis 執(zhí)行某個 SQL 語句時,會觸發(fā)攔截器的 intercept() 方法,這時攔截器會獲取執(zhí)行方法的參數(shù),可以進行修改、增強或替換方法的執(zhí)行。
- 插件銷毀:攔截器在 MyBatis 銷毀時會清理資源,釋放占用的內(nèi)存或線程等。
2.3 目標對象和方法
在 MyBatis 中,主要有四個目標對象可以被攔截:
- Executor:執(zhí)行 SQL 語句的核心對象。它的 update()、query() 等方法負責(zé)執(zhí)行實際的增、刪、改、查操作。
- StatementHandler:處理 SQL 語句的對象。它負責(zé)將 SQL 語句和參數(shù)綁定,并將其傳遞給數(shù)據(jù)庫。
- ResultSetHandler:處理 SQL 查詢結(jié)果的對象。它負責(zé)將從數(shù)據(jù)庫返回的 ResultSet 轉(zhuǎn)換為 Java 對象。
- ParameterHandler:處理 SQL 參數(shù)綁定的對象。它負責(zé)將參數(shù)設(shè)置到 SQL 語句中。
3. MyBatis 攔截器的實現(xiàn)
這里小編會分享工作中實際的案例: 數(shù)據(jù)脫敏。
3.1 自定義脫敏注解
首先需要知曉具體是哪個類中的哪些屬性需要進行脫敏處理,因此,需要自定義注解來實現(xiàn)對需要脫敏的屬性進行標注。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Desensitization {
StrategyEnum strategy();
}
3.2 脫敏策略
有了標注后,對于脫敏也會涉及到脫敏策略的問題。不同的屬性,應(yīng)該對應(yīng)不同的脫敏方式,例如,名字只保留姓氏,而身份證和電話號碼,則需要對中間的數(shù)字打碼。因此,在使用自定義注解進行標注的同時,也要指定這個屬性對應(yīng)的脫敏策略,這里使用枚舉類枚舉出不同屬性對應(yīng)的正則處理。
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum StrategyEnum {
NAME(s -> s.replaceAll("([\\u4e00-\\u9fa5]{1})(.*)", "$1*")),
ID_CARD(s -> s.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1****$2")),
PHONE(s -> s.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")),
ADDRESS(s -> s.replaceAll("(\\s{8})\\s{4}(\\s*)\\s{4})", "$1****$2****"));
private final Desensitizer desensitizer;
}
3.3 脫敏執(zhí)行者
對于脫敏處理還需要一個執(zhí)行者,將屬性值和正則表達式進行匹配和替換,進而完成脫敏處理。這里我們利用了JDK8提供的一個非常好用的接口Fuction,它提供了apply方法,這個方法作用是為了實現(xiàn)函數(shù)映射,也就是將一個值轉(zhuǎn)換為另一個值。如果不了解的同學(xué)可以百度下 Fuction 接口。
import java.util.function.Function;
public interface Desensitizer extends Function<String, String> {
}
3.4 自定義數(shù)據(jù)脫敏攔截器
因為要對結(jié)果集進行脫敏處理,所以要攔截的對象肯定是ResultSetHandler,并且是第一個方法。(可以想一下為啥是第一個方法)
public interface ResultSetHandler {
<E> List<E> handleResultSets(Statement var1) throws SQLException;
<E> Cursor<E> handleCursorResultSets(Statement var1) throws SQLException;
void handleOutputParameters(CallableStatement var1) throws SQLException;
}
來看下具體的實現(xiàn):
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.List;
import java.util.stream.Stream;
@Component
@Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = Statement.class))
public class DesensitizationPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 獲取結(jié)果集
List<Object> records = (List<Object>) invocation.proceed();
// 處理結(jié)果集
records.forEach(this::desensitization);
return records;
}
/**
* 2 * 判斷哪些需要脫敏處理
* 3 * @param source 脫敏之前的源對象
* 4
*/
private void desensitization(Object source) {
// 反射獲取類型中的所有屬性,判斷哪個需要進行脫敏
Class<?> sourceClass = source.getClass();
MetaObject metaObject = SystemMetaObject.forObject(source);
Stream.of(sourceClass.getDeclaredFields())
.filter(field -> field.isAnnotationPresent(Desensitization.class))
.forEach(field -> doDesensitization(metaObject, field));
}
/**
* 2 * 真正的脫敏處理
* 3 * @param metaObject
* 4
*/
private void doDesensitization(MetaObject metaObject, Field field) {
String name = field.getName();
Object value = metaObject.getValue(name);
if (value != null && metaObject.getGetterType(name) == String.class) {
Desensitization annotation = field.getAnnotation(Desensitization.class);
StrategyEnum strategy = annotation.strategy();
String apply = strategy.getDesensitizer().apply((String) value);
metaObject.setValue(name, apply);
}
}
}
數(shù)據(jù)脫敏字段:
import com.example.cl.mybatisPlugin.Desensitization;
import com.example.cl.mybatisPlugin.StrategyEnum;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class User {
private Long id;
@Desensitization(strategy = StrategyEnum.NAME)
private String name;
private Integer age;
}
最后看下脫敏結(jié)果:
圖片
4. 總結(jié)
根據(jù)上面的說明,我們來看看MyBatis 攔截器的優(yōu)勢和不足
- 優(yōu)勢:
非侵入式:通過攔截器機制,不需要修改 MyBatis 源碼即可定制功能。
靈活性:可以在多個階段對 SQL 操作進行干預(yù),從而實現(xiàn)豐富的功能。
- 不足:
性能開銷:如果攔截器過多或者邏輯復(fù)雜,可能會導(dǎo)致性能下降。
調(diào)試困難:攔截器的執(zhí)行過程較為隱式,調(diào)試時可能會遇到一定的困難。
因此,我們攔截器不能創(chuàng)建過多,如果攔截的對象同一個,那么我們可以將多個功能放到同一個攔截器當中,從而減少攔截器的創(chuàng)建。