MyBatis 攔截器,帶你輕松搞定數據脫敏!
1. 引言
1.1 什么是 MyBatis 攔截器
MyBatis 攔截器是一種插件機制,用于在 MyBatis 執行 SQL 語句時對其進行攔截、修改或增強。攔截器可以插入到 MyBatis 的執行過程中的不同位置,從而實現自定義的行為,例如記錄日志、修改 SQL 查詢、增強性能等。
- 定義:MyBatis 攔截器是一種自定義插件,可以通過它攔截 MyBatis 核心組件(如 Executor、StatementHandler、ParameterHandler、ResultSetHandler)的方法調用。通過攔截器,我們可以在不修改 MyBatis 源碼的情況下,改變其行為或增強其功能。
- 功能:
修改 SQL 語句,例如根據某些業務規則動態拼接 SQL。
記錄 SQL 執行日志、性能監控,統計執行時間等。
控制事務或實現緩存邏輯等。
1.2 為什么使用攔截器
使用 MyBatis 攔截器的主要原因是需要在不修改核心代碼的情況下,靈活地擴展 MyBatis 的功能。常見的應用場景包括:
- 日志記錄:通過攔截器記錄每個 SQL 語句的執行情況,包括 SQL 本身、執行時間、返回結果等信息,用于后期分析和調試。
- SQL 性能監控:攔截器可以用于統計 SQL 執行的時間,從而評估 SQL 的性能。長時間執行的 SQL 可以被識別出來,作為性能優化的目標。
- 修改 SQL 語句:通過攔截器可以動態修改 SQL 語句,例如,在查詢中動態插入條件、修改排序規則,或者添加分頁邏輯。
- 事務控制:在執行 SQL 操作之前、之后,或者在某些異常發生時,攔截器可以用來增強事務管理。
2. MyBatis 攔截器工作原理
2.1 攔截器的核心概念
MyBatis 攔截器工作時,核心組件是 Invocation、Interceptor、Method 和 Target 對象等:
- Interceptor:這是所有自定義攔截器的接口,MyBatis 會根據配置找到并調用實現該接口的類。
- Invocation:封裝了方法調用的對象,它包含了目標方法的信息以及方法的參數。通過 Invocation 對象,我們可以對方法的執行進行控制。
- Method:表示目標方法,它是通過反射來獲取的。
- Target:表示目標對象,它是被攔截的對象。例如,Executor、StatementHandler 等都是目標對象,攔截器會通過 Target 對象來訪問和控制這些對象的行為。
2.2 攔截器的生命周期
MyBatis 中,攔截器的生命周期通常包含三個階段:
- 插件初始化:當 MyBatis 啟動時,它會加載并初始化所有配置的攔截器。這時,攔截器會準備好攔截邏輯。
- 攔截執行:當 MyBatis 執行某個 SQL 語句時,會觸發攔截器的 intercept() 方法,這時攔截器會獲取執行方法的參數,可以進行修改、增強或替換方法的執行。
- 插件銷毀:攔截器在 MyBatis 銷毀時會清理資源,釋放占用的內存或線程等。
2.3 目標對象和方法
在 MyBatis 中,主要有四個目標對象可以被攔截:
- Executor:執行 SQL 語句的核心對象。它的 update()、query() 等方法負責執行實際的增、刪、改、查操作。
- StatementHandler:處理 SQL 語句的對象。它負責將 SQL 語句和參數綁定,并將其傳遞給數據庫。
- ResultSetHandler:處理 SQL 查詢結果的對象。它負責將從數據庫返回的 ResultSet 轉換為 Java 對象。
- ParameterHandler:處理 SQL 參數綁定的對象。它負責將參數設置到 SQL 語句中。
3. MyBatis 攔截器的實現
這里小編會分享工作中實際的案例: 數據脫敏。
3.1 自定義脫敏注解
首先需要知曉具體是哪個類中的哪些屬性需要進行脫敏處理,因此,需要自定義注解來實現對需要脫敏的屬性進行標注。
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 脫敏策略
有了標注后,對于脫敏也會涉及到脫敏策略的問題。不同的屬性,應該對應不同的脫敏方式,例如,名字只保留姓氏,而身份證和電話號碼,則需要對中間的數字打碼。因此,在使用自定義注解進行標注的同時,也要指定這個屬性對應的脫敏策略,這里使用枚舉類枚舉出不同屬性對應的正則處理。
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 脫敏執行者
對于脫敏處理還需要一個執行者,將屬性值和正則表達式進行匹配和替換,進而完成脫敏處理。這里我們利用了JDK8提供的一個非常好用的接口Fuction,它提供了apply方法,這個方法作用是為了實現函數映射,也就是將一個值轉換為另一個值。如果不了解的同學可以百度下 Fuction 接口。
import java.util.function.Function;
public interface Desensitizer extends Function<String, String> {
}
3.4 自定義數據脫敏攔截器
因為要對結果集進行脫敏處理,所以要攔截的對象肯定是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;
}
來看下具體的實現:
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 {
// 獲取結果集
List<Object> records = (List<Object>) invocation.proceed();
// 處理結果集
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);
}
}
}
數據脫敏字段:
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;
}
最后看下脫敏結果:
圖片
4. 總結
根據上面的說明,我們來看看MyBatis 攔截器的優勢和不足
- 優勢:
非侵入式:通過攔截器機制,不需要修改 MyBatis 源碼即可定制功能。
靈活性:可以在多個階段對 SQL 操作進行干預,從而實現豐富的功能。
- 不足:
性能開銷:如果攔截器過多或者邏輯復雜,可能會導致性能下降。
調試困難:攔截器的執行過程較為隱式,調試時可能會遇到一定的困難。
因此,我們攔截器不能創建過多,如果攔截的對象同一個,那么我們可以將多個功能放到同一個攔截器當中,從而減少攔截器的創建。