MyBatis自定义拦截器

一、概述

在实际开发中,我们常常需要在不侵入业务代码的前提下,对SQL执行过程进行增强处理,比如SQL耗时统计、参数加密、结果集脱敏、数据权限过滤等场景。MyBatis提供的自定义拦截器(Plugin)机制,正是为这类需求而生。

MyBatis拦截器基于责任链模式和动态代理实现,允许开发者在SQL执行的关键节点插入自定义逻辑,无需修改原有Mapper接口、XML映射文件或业务代码,就能实现功能增强。其核心优势在于低耦合、高复用,广泛应用于日志记录、性能监控、数据安全等场景,是MyBatis生态中极具灵活性的扩展机制。

二、自定义MyBatis拦截器

先实现一个标准的MyBatis拦截器(以「SQL耗时统计」为例),后续以此为例配置:

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.mapping.MappedStatement;

import java.util.Properties;

/**
 * 示例拦截器:统计SQL执行耗时
 */
// 拦截Executor的query/update方法(可根据需求替换拦截点)
@Intercepts({
        @Signature(type = Executor.class, method = "query",
                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "update",
                args = {MappedStatement.class, Object.class})
})
public class SqlCostInterceptor implements Interceptor {

    // 核心拦截逻辑
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        long startTime = System.currentTimeMillis();
        try {
            // 执行原SQL方法
            return invocation.proceed();
        } finally {
            // 统计耗时
            long cost = System.currentTimeMillis() - startTime;
            System.out.println("SQL执行耗时:" + cost + "ms");
        }
    }

    // 包装目标对象(MyBatis自动调用,推荐用Plugin.wrap标准写法)
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    // 读取拦截器配置属性(可选)
    @Override
    public void setProperties(Properties properties) {
        // 示例:读取配置的"logLevel"属性
        String logLevel = properties.getProperty("logLevel", "INFO");
        System.out.println("拦截器配置日志级别:" + logLevel);
    }
}

三、注册拦截器到SqlSessionFactory

Spring Boot中无需XML配置,通过@Configuration配置类注册SqlSessionFactory,并将拦截器注入其中。

3.1 方式1:基础版(单拦截器,无自定义属性)

适合拦截器无需配置属性、仅需简单注册的场景:

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

@Configuration
@MapperScan("com.yourpackage.mapper") // 替换Mapper接口包路径
public class MyBatisConfig {

    /**
     * 配置SqlSessionFactory,注册拦截器
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        // 1. 设置数据源(Spring Boot自动配置的数据源,直接注入)
        sessionFactoryBean.setDataSource(dataSource);

        // 2. 可选:加载Mapper.xml(注解版Mapper可省略)
        sessionFactoryBean.setMapperLocations(
                new PathMatchingResourcePatternResolver()
                        .getResources("classpath:mapper/**/*.xml") // 替换Mapper.xml路径
        );

        // 3. 核心:创建拦截器实例并注册
        SqlCostInterceptor sqlCostInterceptor = new SqlCostInterceptor();
        sessionFactoryBean.setPlugins(sqlCostInterceptor); // 单拦截器直接传入

        // 4. 可选:MyBatis全局配置(如驼峰命名、日志等)
        org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
        configuration.setMapUnderscoreToCamelCase(true); // 下划线转驼峰
        sessionFactoryBean.setConfiguration(configuration);
        return sessionFactoryBean.getObject();
    }
}

3.2 方式2:进阶版(多拦截器+自定义属性)

适合需要注册多个拦截器、或给拦截器配置自定义属性的场景(如加密/解密拦截器):

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;
import java.util.Properties;

@Configuration
@MapperScan("com.yourpackage.mapper")
public class MyBatisConfig {

    // ========== 1. 声明拦截器 Bean(便于依赖注入、属性配置) ==========
    @Bean
    public SqlCostInterceptor sqlCostInterceptor() {
        SqlCostInterceptor interceptor = new SqlCostInterceptor();
        // 给拦截器设置自定义属性(对应intercept.setProperties方法)
        Properties props = new Properties();
        props.setProperty("logLevel", "DEBUG");
        interceptor.setProperties(props);
        return interceptor;
    }

    // 示例:加密拦截器
    @Bean
    public EncryptParameterInterceptor encryptParameterInterceptor() {
        return new EncryptParameterInterceptor();
    }

    // 示例:解密拦截器
    @Bean
    public DecryptResultSetInterceptor decryptResultSetInterceptor() {
        return new DecryptResultSetInterceptor();
    }

    // ========== 2. 注册拦截器到 SqlSessionFactory ==========
    @Bean
    public SqlSessionFactory sqlSessionFactory(
            DataSource dataSource,
            SqlCostInterceptor sqlCostInterceptor,
            EncryptParameterInterceptor encryptInterceptor,
            DecryptResultSetInterceptor decryptInterceptor
    ) throws Exception {
        SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        sessionFactoryBean.setDataSource(dataSource);

        // 可选:加载Mapper.xml
        sessionFactoryBean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")
        );

        // 核心:注册多个拦截器(数组形式传入)
        sessionFactoryBean.setPlugins(sqlCostInterceptor, encryptInterceptor, decryptInterceptor);

        return sessionFactoryBean.getObject();
    }
}

3.3 方式3:极简版(仅Spring Boot+注解版MyBatis)

若项目完全使用注解版Mapper(无XML),且拦截器无自定义属性,可简化配置:

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.sql.DataSource;

@Configuration
@MapperScan("com.yourpackage.mapper")
public class MyBatisConfig {

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        sessionFactoryBean.setDataSource(dataSource);
        
        // 直接注册拦截器
        sessionFactoryBean.setPlugins(
                new SqlCostInterceptor(),
                new EncryptParameterInterceptor(),
                new DecryptResultSetInterceptor()
        );
        return sessionFactoryBean.getObject();
    }
}

四、关键注意事项

4.1 拦截器执行顺序

MyBatis拦截器遵循责任链模式:setPlugins(拦截器A, 拦截器B, 拦截器C) 中,注册顺序越靠前,执行顺序越靠后;例如:注册顺序为「加密拦截器 → 解密拦截器 → 耗时统计拦截器」,执行时顺序为「耗时统计 → 解密 → 加密」;
若拦截不同接口(如ParameterHandler/ResultSetHandler/Executor),顺序不影响;若拦截同一接口,需按业务逻辑调整。

4.2 拦截器生效的前提

  • @MapperScan必须扫描到你的Mapper接口(否则Mapper无法被MyBatis管理,拦截器不生效);
  • 拦截器的@Intercepts注解必须正确:type(拦截接口)、method(拦截方法)、args(方法参数类型)需与MyBatis源码完全匹配;
  • SqlSessionFactory必须被Spring扫描到(配置类加@Configuration)。

4.3 常见拦截点参考

拦截接口 拦截方法 适用场景
Executor query/update SQL执行耗时、分页、数据权限
ParameterHandler setParameters 参数加密、参数校验
ResultSetHandler handleResultSets 结果集解密、数据脱敏
StatementHandler prepare SQL改写、SQL日志打印

4.4 调试拦截器是否生效

  1. 在拦截器的intercept方法中加System.out.println或日志,执行Mapper方法查看是否输出;
  2. 打断点:在intercept方法中打断点,调试执行Mapper方法,确认是否进入拦截逻辑;
  3. 检查Spring上下文:启动项目后,查看SqlSessionFactory Bean是否包含你的拦截器(可通过Spring Boot Actuator或调试模式查看)。

五、常见问题排查

5.1 拦截器不生效

  1. 检查@Intercepts注解的type/method/args是否正确(参数类型必须是全限定类名);
  2. 检查SqlSessionFactory是否注册了拦截器(断点查看sessionFactoryBean.getPlugins());
  3. 检查Mapper接口是否被@MapperScan扫描(未扫描则MyBatis不会处理该Mapper)。

5.2 拦截器执行顺序错误

  1. 调整setPlugins中拦截器的传入顺序(注册顺序与执行顺序相反);
  2. 若需精准控制,可自定义InterceptorChain,但不推荐(破坏MyBatis原生逻辑)。

5.3 反射报错(如字段不可访问)

  1. 在拦截器中处理实体字段时,需调用field.setAccessible(true)
  2. 避免拦截String/Integer等基础类型(无字段可反射)。

通过以上配置,即可在Spring Boot中灵活配置MyBatis拦截器,适配单/多拦截器、注解/XMLMyBatis、自定义属性等场景。

posted @ 2025-12-07 22:19  夏尔_717  阅读(36)  评论(0)    收藏  举报