MyBatis 拦截器:代码增强的艺术
大家好!今天我们来深入探讨 MyBatis 拦截器的实现原理,重点关注在 Statement、Parameter 和 ResultSet 三个关键阶段的代码增强。MyBatis 拦截器是一个强大的特性,允许我们在 SQL 执行的不同阶段拦截并修改 SQL 语句、参数或结果集,从而实现诸如分页、性能监控、数据加密等功能。
1. 拦截器的核心概念:责任链模式
MyBatis 拦截器的底层实现基于责任链设计模式。简单来说,责任链模式允许将请求沿着一个处理链传递,直到某个处理器处理它为止。在 MyBatis 中,每个拦截器都充当责任链中的一个处理器。当 MyBatis 执行 SQL 语句时,会依次调用配置的拦截器,每个拦截器可以选择处理或跳过该请求。
责任链模式的优点:
- 松耦合: 调用者不需要知道哪个拦截器最终会处理请求。
- 灵活性: 可以动态地添加或删除拦截器。
- 可扩展性: 容易添加新的拦截器来扩展功能。
2. MyBatis 拦截器的类型
MyBatis 允许我们拦截四个不同的接口:
接口 | 描述 |
---|---|
Executor | 拦截执行器的方法,它是 MyBatis 的核心组件之一,负责执行 SQL 查询、更新、删除等操作。 |
Statement | 拦截 StatementHandler 接口的方法,该接口负责创建和准备 JDBC Statement 对象,以及执行 SQL 语句。 |
ParameterHandler | 拦截 ParameterHandler 接口的方法,该接口负责设置 SQL 语句中的参数。 |
ResultSetHandler | 拦截 ResultSetHandler 接口的方法,该接口负责将 JDBC ResultSet 对象映射到 Java 对象。 |
本文重点关注 Statement、Parameter 和 ResultSet 三个拦截器。
3. Statement 拦截器:修改 SQL 语句
Statement 拦截器允许我们在 SQL 语句被发送到数据库之前对其进行修改。这对于实现诸如分页、SQL 审计等功能非常有用。
3.1 StatementHandler 接口
要实现 Statement 拦截器,我们需要拦截 StatementHandler
接口的方法。StatementHandler
接口负责创建和准备 JDBC Statement
对象,以及执行 SQL 语句。其主要方法包括:
prepare(Connection connection, Integer transactionTimeout)
: 准备 Statement 对象。parameterize(Statement statement)
: 设置 Statement 对象的参数。handleResultSets(Statement statement)
: 处理结果集。update(Statement statement)
: 执行更新操作。query(Statement statement, ResultHandler resultHandler)
: 执行查询操作。
3.2 实现 Statement 拦截器
下面是一个 Statement 拦截器的示例,它在 SQL 语句的末尾添加 LIMIT
子句,实现分页功能:
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.sql.Connection;
import java.util.Properties;
@Intercepts({@Signature(
type= StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class})})
public class PaginationInterceptor implements Interceptor {
private String dialect; // 数据库方言
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
// 获取原始 SQL 语句
String sql = statementHandler.getBoundSql().getSql();
// 判断是否需要分页
if (needPagination(sql)) {
// 构建分页 SQL
String paginationSql = buildPaginationSql(sql);
// 通过反射修改 SQL 语句
ReflectUtil.setFieldValue(statementHandler.getBoundSql(), "sql", paginationSql);
}
// 继续执行
return invocation.proceed();
}
private boolean needPagination(String sql) {
// 这里可以根据业务逻辑判断是否需要分页
// 例如,可以检查 SQL 语句中是否包含特定的分页标记
return sql.contains("/*分页*/");
}
private String buildPaginationSql(String sql) {
// 这里根据数据库方言构建分页 SQL
if ("mysql".equalsIgnoreCase(dialect)) {
return sql + " LIMIT 0, 10"; // 示例:MySQL 分页
} else if ("oracle".equalsIgnoreCase(dialect)) {
return "SELECT * FROM ( SELECT A.*, ROWNUM RN FROM ( " + sql + " ) A WHERE ROWNUM <= 10 ) WHERE RN >= 1"; // 示例:Oracle 分页
} else {
throw new IllegalArgumentException("Unsupported database dialect: " + dialect);
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
this.dialect = properties.getProperty("dialect");
}
}
// 反射工具类,用于修改私有字段
class ReflectUtil {
public static void setFieldValue(Object obj, String fieldName, Object value) {
try {
java.lang.reflect.Field field = obj.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(obj, value);
} catch (Exception e) {
e.printStackTrace();
}
}
}
代码解释:
@Intercepts
注解: 指定拦截的接口和方法。type
属性指定拦截StatementHandler
接口,method
属性指定拦截prepare
方法,args
属性指定prepare
方法的参数类型。intercept
方法: 拦截器的核心方法,在该方法中实现具体的拦截逻辑。- 获取原始 SQL 语句: 通过
statementHandler.getBoundSql().getSql()
方法获取原始 SQL 语句。 - 判断是否需要分页:
needPagination
方法根据业务逻辑判断是否需要分页。 - 构建分页 SQL:
buildPaginationSql
方法根据数据库方言构建分页 SQL。 - 修改 SQL 语句: 通过反射修改
statementHandler.getBoundSql()
对象的sql
字段,将原始 SQL 替换为分页 SQL。 - 继续执行:
invocation.proceed()
方法调用原始方法,继续执行 SQL 语句。 plugin
方法: 使用Plugin.wrap()
方法将拦截器包装到目标对象中。setProperties
方法: 用于配置拦截器的属性,例如数据库方言。
3.3 配置 Statement 拦截器
在 MyBatis 的配置文件 (mybatis-config.xml
) 中配置拦截器:
<configuration>
<plugins>
<plugin interceptor="com.example.PaginationInterceptor">
<property name="dialect" value="mysql"/>
</plugin>
</plugins>
</configuration>
4. Parameter 拦截器:修改 SQL 参数
Parameter 拦截器允许我们在 SQL 语句的参数被设置到 JDBC PreparedStatement
对象之前对其进行修改。这对于实现诸如数据加密、参数校验等功能非常有用。
4.1 ParameterHandler 接口
要实现 Parameter 拦截器,我们需要拦截 ParameterHandler
接口的方法。ParameterHandler
接口负责设置 SQL 语句中的参数。其主要方法包括:
getParameterObject()
: 获取参数对象。setParameters(PreparedStatement ps)
: 设置 PreparedStatement 对象的参数。
4.2 实现 Parameter 拦截器
下面是一个 Parameter 拦截器的示例,它对字符串类型的参数进行加密:
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.plugin.*;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Properties;
@Intercepts({@Signature(
type= ParameterHandler.class,
method = "setParameters",
args = {PreparedStatement.class})})
public class EncryptionInterceptor implements Interceptor {
private String encryptionKey; // 加密密钥
@Override
public Object intercept(Invocation invocation) throws Throwable {
ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget();
PreparedStatement preparedStatement = (PreparedStatement) invocation.getArgs()[0];
// 获取参数对象
Object parameterObject = parameterHandler.getParameterObject();
// 判断参数类型并进行加密
if (parameterObject instanceof String) {
String originalValue = (String) parameterObject;
String encryptedValue = encrypt(originalValue, encryptionKey);
// 使用反射修改参数对象,或者直接在 PreparedStatement 中设置加密后的值
ReflectUtil.setFieldValue(parameterHandler, "parameterObject", encryptedValue);
// 或者 直接设置
//preparedStatement.setString(1, encryptedValue); // 需要知道参数索引
}
// 继续执行
return invocation.proceed();
}
private String encrypt(String originalValue, String key) {
// 这里实现具体的加密逻辑
// 例如,可以使用 AES、DES 等加密算法
return "encrypted_" + originalValue; // 示例:简单加密
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
this.encryptionKey = properties.getProperty("encryptionKey");
}
}
代码解释:
@Intercepts
注解: 指定拦截的接口和方法。type
属性指定拦截ParameterHandler
接口,method
属性指定拦截setParameters
方法,args
属性指定setParameters
方法的参数类型。intercept
方法: 拦截器的核心方法,在该方法中实现具体的拦截逻辑。- 获取参数对象: 通过
parameterHandler.getParameterObject()
方法获取参数对象。 - 判断参数类型并进行加密: 判断参数类型是否为 String,如果是,则进行加密。
- 加密:
encrypt
方法实现具体的加密逻辑。 - 修改参数对象: 通过反射修改
parameterHandler
对象的parameterObject
字段,将原始参数替换为加密后的参数。 或者直接操作PreparedStatement
,但需要知道参数的索引。 - 继续执行:
invocation.proceed()
方法调用原始方法,继续执行 SQL 语句。 plugin
方法: 使用Plugin.wrap()
方法将拦截器包装到目标对象中。setProperties
方法: 用于配置拦截器的属性,例如加密密钥。
4.3 配置 Parameter 拦截器
在 MyBatis 的配置文件 (mybatis-config.xml
) 中配置拦截器:
<configuration>
<plugins>
<plugin interceptor="com.example.EncryptionInterceptor">
<property name="encryptionKey" value="secretKey"/>
</plugin>
</plugins>
</configuration>
5. ResultSet 拦截器:修改结果集
ResultSet 拦截器允许我们在 JDBC ResultSet
对象被映射到 Java 对象之前对其进行修改。这对于实现诸如数据解密、结果集转换等功能非常有用。
5.1 ResultSetHandler 接口
要实现 ResultSet 拦截器,我们需要拦截 ResultSetHandler
接口的方法。ResultSetHandler
接口负责将 JDBC ResultSet
对象映射到 Java 对象。其主要方法包括:
handleResultSets(Statement statement)
: 处理结果集。
5.2 实现 ResultSet 拦截器
下面是一个 ResultSet 拦截器的示例,它对结果集中的字符串类型的字段进行解密:
import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.List;
import java.util.Properties;
@Intercepts({@Signature(
type= ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class})})
public class DecryptionInterceptor implements Interceptor {
private String decryptionKey; // 解密密钥
@Override
public Object intercept(Invocation invocation) throws Throwable {
List<Object> results = (List<Object>) invocation.proceed();
// 遍历结果集,对需要解密的字段进行解密
for (Object result : results) {
decryptObject(result);
}
return results;
}
private void decryptObject(Object obj) throws SQLException {
if (obj == null) {
return;
}
try {
// 判断对象类型,并进行相应的解密操作
// 这里假设对象是 Map 或 JavaBean
if (obj instanceof java.util.Map) {
java.util.Map<String, Object> map = (java.util.Map<String, Object>) obj;
for (String key : map.keySet()) {
Object value = map.get(key);
if (value instanceof String) {
String encryptedValue = (String) value;
String decryptedValue = decrypt(encryptedValue, decryptionKey);
map.put(key, decryptedValue);
}
}
} else {
// 假设对象是 JavaBean
java.lang.reflect.Field[] fields = obj.getClass().getDeclaredFields();
for (java.lang.reflect.Field field : fields) {
field.setAccessible(true);
Object value = field.get(obj);
if (value instanceof String) {
String encryptedValue = (String) value;
String decryptedValue = decrypt(encryptedValue, decryptionKey);
field.set(obj, decryptedValue);
}
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
private String decrypt(String encryptedValue, String key) {
// 这里实现具体的解密逻辑
// 例如,可以使用 AES、DES 等解密算法
return encryptedValue.replace("encrypted_", ""); // 示例:简单解密
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
this.decryptionKey = properties.getProperty("decryptionKey");
}
}
代码解释:
@Intercepts
注解: 指定拦截的接口和方法。type
属性指定拦截ResultSetHandler
接口,method
属性指定拦截handleResultSets
方法,args
属性指定handleResultSets
方法的参数类型。intercept
方法: 拦截器的核心方法,在该方法中实现具体的拦截逻辑。- 获取结果集: 通过
invocation.proceed()
方法获取结果集。 - 遍历结果集,对需要解密的字段进行解密: 遍历结果集,判断字段类型是否为 String,如果是,则进行解密。
- 解密:
decrypt
方法实现具体的解密逻辑。 - 修改结果集: 将解密后的值设置回结果集。
plugin
方法: 使用Plugin.wrap()
方法将拦截器包装到目标对象中。setProperties
方法: 用于配置拦截器的属性,例如解密密钥。
5.3 配置 ResultSet 拦截器
在 MyBatis 的配置文件 (mybatis-config.xml
) 中配置拦截器:
<configuration>
<plugins>
<plugin interceptor="com.example.DecryptionInterceptor">
<property name="decryptionKey" value="secretKey"/>
</plugin>
</plugins>
</configuration>
6. 总结:拦截器提供强大的可扩展性
MyBatis 拦截器通过责任链模式,允许我们在 SQL 执行的不同阶段拦截并修改 SQL 语句、参数或结果集。Statement 拦截器用于修改 SQL 语句,Parameter 拦截器用于修改 SQL 参数,ResultSet 拦截器用于修改结果集。 掌握这些拦截器类型,可以为 MyBatis 应用添加强大的可扩展性和定制能力。