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、拦截器注册

如上操作即可

注意⚠️

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层处理