SpringBoot集成MyBatis的SQL拦截器

一、慢查询监控拦截器

监控所有SQL执行时间,超过阈值(如500ms)则打印警告日志,包含:

  • • SQL执行时间

  • • 完整SQL语句(带参数占位符)

  • • 参数值(防止SQL注入排查)

1、拦截器类

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import java.sql.Connection;
import java.sql.Statement;
import java.util.Properties;

@Slf4j
@Intercepts({
    // 拦截查询方法
    @Signature(
        type = StatementHandler.class,
        method = "query",
        args = {Statement.class, ResultHandler.class}
    ),
    // 拦截更新方法(insert/update/delete)
    @Signature(
        type = StatementHandler.class,
        method = "update",
        args = {Statement.class}
    )
})
public class SlowSqlInterceptor implements Interceptor {

    // 慢查询阈值(毫秒),可通过配置文件注入
    private long slowThreshold = 500;

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 记录开始时间
        long startTime = System.currentTimeMillis();
        
        try {
            // 2. 执行原方法(继续SQL执行流程)
            return invocation.proceed();
        } finally {
            // 3. 计算执行耗时(无论成功失败都记录)
            long costTime = System.currentTimeMillis() - startTime;
            
            // 4. 获取SQL语句和参数
            StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
            String sql = statementHandler.getBoundSql().getSql();  // 获取SQL语句(带?占位符)
            Object parameterObject = statementHandler.getBoundSql().getParameterObject();  // 获取参数
            
            // 5. 判断是否慢查询
            if (costTime > slowThreshold) {
                log.warn("[慢查询警告] 执行时间: {}ms, SQL: {}, 参数: {}", 
                         costTime, sql, parameterObject);
            } else {
                log.info("[SQL监控] 执行时间: {}ms, SQL: {}", costTime, sql);
            }
        }
    }

    @Override
    public Object plugin(Object target) {
        // 生成代理对象(MyBatis提供的工具方法,避免自己写代理逻辑)
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 从配置文件读取阈值(如application.yml中配置)
        String threshold = properties.getProperty("slowThreshold");
        if (threshold != null) {
            slowThreshold = Long.parseLong(threshold);
        }
    }
}

2、SpringBoot注册拦截器


import com.example.interceptor.SensitiveInterceptor;
import com.example.interceptor.SlowSqlInterceptor;
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.example.mapper")  // Mapper接口所在包
public class MyBatisConfig {

    // 注册慢查询拦截器
    @Bean
    public SlowSqlInterceptor slowSqlInterceptor() {
        SlowSqlInterceptor interceptor = new SlowSqlInterceptor();
        
        // 设置属性(也可通过application.yml配置)
        Properties properties = new Properties();
        properties.setProperty("slowThreshold", "500");  // 慢查询阈值500ms
        interceptor.setProperties(properties);
        
        return interceptor;
    }


    // 将拦截器添加到SqlSessionFactory
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource, SlowSqlInterceptor slowSqlInterceptor) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        
        // 设置Mapper.xml路径(如果需要)
        sessionFactory.setMapperLocations(
            new PathMatchingResourcePatternResolver()
                .getResources("classpath:mapper/*.xml")
        );
        
        // 添加拦截器
        sessionFactory.setPlugins(slowSqlInterceptor);

        return sessionFactory.getObject();
    }
}

3、测试效果

接口样例

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
    
    public User getUserById(Long id) {
        return userMapper.selectById(id);
    }
}

输出结果

[SQL监控] 执行时间: 30ms, SQL: SELECT id,username,phone FROM user WHERE id = ?

如果SQL执行时间超过500ms(比如查询大数据量表):

[慢查询警告] 执行时间: 1430ms, SQL: SELECT * FROM user WHERE id = ?, 参数: {id=1, param1=1}

踩坑提示:如果拦截不到SQL,检查@Signature注解的args参数是否与方法参数类型完全匹配!

二、数据脱敏拦截器(敏感信息保护)

1、自定义脱敏注解

import java.lang.annotation.*;

// 作用在字段上

@Target(ElementType.FIELD)

// 运行时生效

@Retention(RetentionPolicy.RUNTIME)

public @interface Sensitive {

    // 脱敏类型(手机号、身份证号等)

    SensitiveType type();

}

// 脱敏类型枚举

public enum SensitiveType {

    PHONE,    // 手机号

    ID_CARD   // 身份证号

}

2、实体类添加注解

@Data

public class User {

    private Long id;

    private String username;

    

    @Sensitive(type = SensitiveType.PHONE)  // 手机号脱敏

    private String phone;

    

    @Sensitive(type = SensitiveType.ID_CARD)  // 身份证号脱敏

    private String idCard;

}

3、脱敏工具类

public class SensitiveUtils {

    // 手机号脱敏:保留前3位和后4位

    public static String maskPhone(String phone) {

        if (phone == null || phone.length() != 11) {

            return phone;  // 非手机号格式不处理

        }

        return phone.replaceAll("(\d{3})\d{4}(\d{4})", "$1****$2");

    }

    

    // 身份证号脱敏:保留最后2位

    public static String maskIdCard(String idCard) {

        if (idCard == null || idCard.length() < 18) {

            return idCard;  // 非身份证格式不处理

        }

        return idCard.replaceAll("\d{16}(\d{2})", "****************$1");

    }

}

4、结果集拦截器

import lombok.extern.slf4j.Slf4j;

import org.apache.ibatis.executor.resultset.ResultSetHandler;

import org.apache.ibatis.plugin.*;

import java.lang.reflect.Field;

import java.sql.Statement;

import java.util.List;

import java.util.Properties;

@Slf4j

@Intercepts({

    @Signature(

        type = ResultSetHandler.class,

        method = "handleResultSets",

        args = {Statement.class}

    )

})

public class SensitiveInterceptor implements Interceptor {

    @Override

    public Object intercept(Invocation invocation) throws Throwable {

        // 1. 执行原方法,获取查询结果

        Object result = invocation.proceed();

        

        // 2. 如果结果是List,遍历处理每个元素

        if (result instanceof List<?>) {

            List<?> resultList = (List<?>) result;

            for (Object obj : resultList) {

                // 3. 对有@Sensitive注解的字段进行脱敏

                desensitize(obj);

            }

        }

        return result;

    }

    // 反射处理对象中的敏感字段

    private void desensitize(Object obj) throws IllegalAccessException {

        if (obj == null) {

            return;

        }

        Class<?> clazz = obj.getClass();

        Field[] fields = clazz.getDeclaredFields();  // 获取所有字段(包括私有)

        

        for (Field field : fields) {

            // 4. 检查字段是否有@Sensitive注解

            if (field.isAnnotationPresent(Sensitive.class)) {

                Sensitive annotation = field.getAnnotation(Sensitive.class);

                field.setAccessible(true);  // 开启私有字段访问权限

                Object value = field.get(obj);  // 获取字段值

                

                if (value instanceof String) {

                    String strValue = (String) value;

                    // 5. 根据脱敏类型处理

                    switch (annotation.type()) {

                        case PHONE:

                            field.set(obj, SensitiveUtils.maskPhone(strValue));

                            break;

                        case ID_CARD:

                            field.set(obj, SensitiveUtils.maskIdCard(strValue));

                            break;

                        default:

                            break;

                    }

                }

            }

        }

    }

    @Override

    public Object plugin(Object target) {

        return Plugin.wrap(target, this);

    }

    @Override

    public void setProperties(Properties properties) {

        // 可配置更多脱敏规则,此处省略

    }

}

5、注册多个拦截器

@Configuration

@MapperScan("com.example.mapper")

public class MyBatisConfig {

    // ... 慢查询拦截器配置 ...

    @Bean

    public SensitiveInterceptor sensitiveInterceptor() {

        return new SensitiveInterceptor();

    }

    @Bean

    public SqlSessionFactory sqlSessionFactory(DataSource dataSource, 

                                             SlowSqlInterceptor slowSqlInterceptor,

                                             SensitiveInterceptor sensitiveInterceptor) throws Exception {

        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();

        sessionFactory.setDataSource(dataSource);

        sessionFactory.setMapperLocations(

            new PathMatchingResourcePatternResolver().getResources("classpath:mapper/*.xml")

        );

        

        // 注册多个拦截器(注意顺序!先执行的拦截器先注册)

        sessionFactory.setPlugins(slowSqlInterceptor, sensitiveInterceptor);

        

        return sessionFactory.getObject();

    }

}

6、测试效果

User user = userService.getUserById(1L);

System.out.println(user); 

// 输出:User(id=1, username=张三, phone=138****5678, idCard=****************34)

三、mybatis通用字段自动填充

1、拦截器类

package com.datasource.config.mybatis;

import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;

import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.util.*;

/**
 * 针对insert update操作对 创建人 创建时间 删除标志 更新人 更新时间 拦截填充
 *
 * @author Neoooo
 * @since 2023-08-28
 */
@Slf4j
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class MyBatisOperateInterceptor implements Interceptor {

    private static final String CREATE_BY = "createBy";
    private static final String UPDATE_BY = "updateBy";
    private static final String CREATE_TIME = "createTime";
    private static final String UPDATE_TIME = "updateTime";
    private static final String IS_DELETE = "isDelete";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
        // 操作类型 只对 insert update 进行拦截
        SqlCommandType sqlCommandType = statement.getSqlCommandType();
        if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
            Object arg = invocation.getArgs()[1];
            if (arg instanceof Map<?, ?>) {
                for (Object obj : ((Map<?, ?>) arg).values()) {

                  if (obj instanceof Collection) {
                
                for (Object item : (Collection<?>) obj) {
           insertOrUpdateOperate(item, sqlCommandType);
                }
            } else if (obj.getClass().isArray()) {
                // 处理数组参数
            for (Object item : (Object[]) obj) {
           insertOrUpdateOperate(obj, sqlCommandType);
                }
            } else {
                // 处理单个对象插入
                insertOrUpdateOperate(obj, sqlCommandType);
            }
                   
                }
            } else {
                insertOrUpdateOperate(arg, sqlCommandType);
            }
        }
        return invocation.proceed();
    }


    /**
     * 添加或者
     *
     * @param object         数据对象
     * @param sqlCommandType 操作行为 insert or update
     */
    private void insertOrUpdateOperate(Object object, SqlCommandType sqlCommandType) throws IllegalAccessException {
        if (object == null) {
            log.info("object set properties ,object must is not null");
            return;
        }
        List<Field> declaredFields = new ArrayList<>(Arrays.asList(object.getClass().getDeclaredFields()));
        if (object.getClass().getSuperclass() != null &amp;&amp; object.getClass().getSuperclass() != Object.class) {
            // 当前类具有超类父类(所有类都是继承于Object 所以要排除掉)
            Field[] superClassFields = object.getClass().getSuperclass().getDeclaredFields();
            declaredFields.addAll(Arrays.asList(superClassFields));
        }
        // 添加
        for (Field declaredField : declaredFields) {
            declaredField.setAccessible(true);
            if (SqlCommandType.INSERT.equals(sqlCommandType)) {
                System.out.println(declaredField.getName());
                switch (declaredField.getName()) {
                    case CREATE_BY:
                        // 创建人
                        declaredField.set(object, "Neoooo");
                        break;
                    case CREATE_TIME:
                        // 创建时间
                        declaredField.set(object, LocalDateTime.now());
                        break;
                    case IS_DELETE:
                        // 删除标志
                        declaredField.set(object, false);
                        break;
                    default:
                        break;
                }
            } else if (SqlCommandType.UPDATE.equals(sqlCommandType)) {
                switch (declaredField.getName()) {
                    case UPDATE_BY:
                        // 更新人 TODO 可获取当前登录用户
                        declaredField.set(object, "admin");
                        break;
                    case UPDATE_TIME:
                        // 更新时间
                        declaredField.set(object, LocalDateTime.now());
                        break;
                    default:
                        break;
                }
            }
        }
    }


    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }
}
 

2、拦截器注册

如上操作即可

四、mybatis拦截器备份delete删除数据

1、拦截器类

import com.alibaba.fastjson.JSON;
import com.sgcc.dlsc.pxsettelementinfpubquery.config.DB.DataSourceNameContextHolder;
import com.sgcc.dlsc.pxsettelementinfpubquery.entity.DelBak;
import com.sgcc.dlsc.pxsettelementinfpubquery.service.ITestService;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.RowBounds;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.UUID;

/**
 * @ClassName DeleteBackupInterceptor
 * @Description 删除备份拦截器
 **/
@Slf4j
@Intercepts({
        //拦截查询方法
        @Signature(type = Executor.class,method = "update",args = {MappedStatement.class, Object.class})
})
public class DeleteBackupInterceptor implements Interceptor {

    @Autowired
    private ITestService service;


    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1];
        //只处理删除语句
        if(mappedStatement.getSqlCommandType() == SqlCommandType.DELETE){
            //1、获取原始sql
            String originaSql = mappedStatement.getBoundSql(parameter).getSql();
            //2、构造查询sql
            String selectSql = originaSql.toLowerCase().replace("delete from","select * from");
            log.info("构造查询sql:{}", selectSql);
            //3、创建新的MapperStatement用于查询
            MappedStatement newMappedStatement = MappedStatementBuilder.buildMappedStatement(
                    mappedStatement,invocation,selectSql, Map.class
            );
            //4、执行查询
            Executor executor = (Executor) invocation.getTarget();
            List<Map<String,Object>> dataDelete = executor.query(
                newMappedStatement,parameter, RowBounds.DEFAULT,Executor.NO_RESULT_HANDLER
            );
            //5、备份数据
            log.info("查询到的备份数据:{}", dataDelete);
            //多数据源切换一手
            String dataSourceName = DataSourceNameContextHolder.getDataSourceName();
            DataSourceNameContextHolder.setDataSourceName("primary");
            DelBak delBak = new DelBak();
            delBak.setId(UUID.randomUUID().toString());
            delBak.setDeleteSql(originaSql);
            delBak.setBakData(JSON.toJSONString(dataDelete));
            service.insertDelBak(delBak);
            DataSourceNameContextHolder.setDataSourceName(dataSourceName);
        }
        return invocation.proceed();
    }


    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        //可以接收配置参数
    }
}

2、MappedStatementBuilder

import org.apache.ibatis.builder.StaticSqlSource;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.session.Configuration;
import java.util.ArrayList;
import java.util.Collections;

/**
 * @ClassName MappedStatementBuilder
 * @Description 创建新的MappedStatement用于查询
 **/
public class MappedStatementBuilder {
    public static MappedStatement buildMappedStatement(MappedStatement ms, Invocation invocation,String sql, Class<?> resultType) {
        Configuration configuration = ms.getConfiguration();
        Object parameter = invocation.getArgs()[1];
        //创建SqlSource
        StaticSqlSource sqlSource = new StaticSqlSource(configuration,sql,ms.getBoundSql(parameter).getParameterMappings());
        //构建ResultMap
        ResultMap resultMap = new ResultMap.Builder(
                configuration,"dynamicResultMpa",resultType,new ArrayList<>()
        ).build();
        //创建新的MappedStatement
        MappedStatement.Builder builder = new MappedStatement.Builder(
                configuration, ms.getId()+"-dynamicQuery",sqlSource,ms.getSqlCommandType()
        );
        //复制原始MappedStatement的重要属性
        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        builder.timeout(ms.getTimeout());
        builder.parameterMap(ms.getParameterMap());
        builder.resultMaps(Collections.singletonList(resultMap));
        builder.resultSetType(ms.getResultSetType());
        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());
        return builder.build();
    }
}

多数据源配置

springboot整合druid(多数据源配置)-优快云博客

3、拦截器注册

如上操作即可

注意⚠️

1、多个拦截器时,注册顺序就是执行顺序。比如先注册慢查询拦截器,再注册脱敏拦截器

如果顺序反了,脱敏拦截器会先处理结果,慢查询拦截器记录的SQL就看不到原始参数了。解决:按"执行SQL前→执行SQL后→处理结果"的顺序注册。

2、@Signature的args参数类型写错,导致拦截不到方法。比如StatementHandler.prepare方法有两个重载

// 正确的参数类型

prepare(Connection connection, Integer transactionTimeout)

// 错误示例:写成了(int)

@Signature(args = {Connection.class, int.class})  // 出现下面的异常!

java.lang.NoSuchMethodException: org.apache.ibatis.executor.statement.StatementHandler.prepare(java.sql.Connection,int)

3、在拦截器中做复杂操作(如反射遍历所有字段)会影响性能

解决:

• 反射操作缓存Class信息

• 非必要不拦截(如只拦截查询方法)

• 敏感字段脱敏可考虑在DTO层处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值