MyBatis拦截器实现原理:在Statement/Parameter/ResultSet阶段的代码增强

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();
        }
    }
}

代码解释:

  1. @Intercepts 注解: 指定拦截的接口和方法。type 属性指定拦截 StatementHandler 接口,method 属性指定拦截 prepare 方法,args 属性指定 prepare 方法的参数类型。
  2. intercept 方法: 拦截器的核心方法,在该方法中实现具体的拦截逻辑。
  3. 获取原始 SQL 语句: 通过 statementHandler.getBoundSql().getSql() 方法获取原始 SQL 语句。
  4. 判断是否需要分页: needPagination 方法根据业务逻辑判断是否需要分页。
  5. 构建分页 SQL: buildPaginationSql 方法根据数据库方言构建分页 SQL。
  6. 修改 SQL 语句: 通过反射修改 statementHandler.getBoundSql() 对象的 sql 字段,将原始 SQL 替换为分页 SQL。
  7. 继续执行: invocation.proceed() 方法调用原始方法,继续执行 SQL 语句。
  8. plugin 方法: 使用 Plugin.wrap() 方法将拦截器包装到目标对象中。
  9. 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");
    }
}

代码解释:

  1. @Intercepts 注解: 指定拦截的接口和方法。type 属性指定拦截 ParameterHandler 接口,method 属性指定拦截 setParameters 方法,args 属性指定 setParameters 方法的参数类型。
  2. intercept 方法: 拦截器的核心方法,在该方法中实现具体的拦截逻辑。
  3. 获取参数对象: 通过 parameterHandler.getParameterObject() 方法获取参数对象。
  4. 判断参数类型并进行加密: 判断参数类型是否为 String,如果是,则进行加密。
  5. 加密: encrypt 方法实现具体的加密逻辑。
  6. 修改参数对象: 通过反射修改 parameterHandler 对象的 parameterObject 字段,将原始参数替换为加密后的参数。 或者直接操作PreparedStatement,但需要知道参数的索引。
  7. 继续执行: invocation.proceed() 方法调用原始方法,继续执行 SQL 语句。
  8. plugin 方法: 使用 Plugin.wrap() 方法将拦截器包装到目标对象中。
  9. 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");
    }
}

代码解释:

  1. @Intercepts 注解: 指定拦截的接口和方法。type 属性指定拦截 ResultSetHandler 接口,method 属性指定拦截 handleResultSets 方法,args 属性指定 handleResultSets 方法的参数类型。
  2. intercept 方法: 拦截器的核心方法,在该方法中实现具体的拦截逻辑。
  3. 获取结果集: 通过 invocation.proceed() 方法获取结果集。
  4. 遍历结果集,对需要解密的字段进行解密: 遍历结果集,判断字段类型是否为 String,如果是,则进行解密。
  5. 解密: decrypt 方法实现具体的解密逻辑。
  6. 修改结果集: 将解密后的值设置回结果集。
  7. plugin 方法: 使用 Plugin.wrap() 方法将拦截器包装到目标对象中。
  8. 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 应用添加强大的可扩展性和定制能力。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注