一、mybatis拦截器配置
类基本信息
类名: DataSourceConfig
包路径: com.mdgyl.hussar.sourcing.config
注解: 使用了 @Configuration,表明这是一个Spring配置类
主要功能
- 数据源配置
通过 @Resource 注入了外部配置的 DataSource 数据源
在 SqlSessionFactoryBeanCustomizer 中将数据源设置到 customizeFactoryBean 上
- 自定义拦截器配置
注入了两个自定义拦截器:
MybatisUpdateInterceptor: 更新操作拦截器
MybatisQueryInterceptor: 查询操作拦截器
将这两个拦截器注册到 SqlSessionFactoryBean 中,形成拦截器链
- MyBatis定制化配置
实现了 SqlSessionFactoryBeanCustomizer 接口
通过 customize 方法对 SqlSessionFactoryBean 进行定制化配置
这是官方推荐的MyBatis配置方式
java
package com.mdgyl.hussar.sourcing.config;
import org.apache.ibatis.plugin.Interceptor;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.boot.autoconfigure.SqlSessionFactoryBeanCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.annotation.Resource;
import javax.sql.DataSource;
/**
* description: mybatis拦截器配置
* author: wuyc
* createTime: 2023-12-02 14:13:54
*/
@Configuration
public class DataSourceConfig {
// 注入数据源
@Resource
private DataSource dataSource;
// 注入自定义参数拦截器
@Resource
private MybatisUpdateInterceptor updateInterceptor;
@Resource
private MybatisQueryInterceptor queryInterceptor;
// 官方推荐的实现方式
@Bean
SqlSessionFactoryBeanCustomizer sqlSessionFactoryBeanCustomizer() {
return new SqlSessionFactoryBeanCustomizer() {
@Override
public void customize(SqlSessionFactoryBean customizeFactoryBean) {
// 设置数据源
customizeFactoryBean.setDataSource(dataSource);
// 设置自定义拦截器(可设置多个)
Interceptor[] plugins = {queryInterceptor, updateInterceptor};
customizeFactoryBean.setPlugins(plugins);
}
};
}
}
二、 MybatisUpdateInterceptor 类介绍
MybatisUpdateInterceptor\](file://E:\\SupplyChain\\mdgylxt_sourcing\\md-gyl-sourcing-start\\src\\main\\java\\com\\mdgyl\\hussar\\sourcing\\config\\MybatisUpdateInterceptor.java#L24-L174) 是一个 MyBatis 拦截器,用于在数据库操作前后自动填充通用字段。 基本信息 - 类名: \[MybatisUpdateInterceptor\](file://E:\\SupplyChain\\mdgylxt_sourcing\\md-gyl-sourcing-start\\src\\main\\java\\com\\mdgyl\\hussar\\sourcing\\config\\MybatisUpdateInterceptor.java#L24-L174) - 注解: - \`@Component\`: 标识为 Spring 组件 - \`@Intercepts\`: 定义拦截点,拦截 \`Executor\` 的 \[update\]方法 - \`@Slf4j\`: 提供日志记录功能 主要功能 1. 拦截数据库操作 - 拦截 MyBatis 的 \[update\](操作(包括 INSERT 和 UPDATE) - 通过 \`@Intercepts\` 注解配置拦截点 2. 自动填充字段 针对不同的数据库操作自动填充相应字段: INSERT 操作\*\*: - \`CREATE_TIME\`: 创建时间 - \`CREATE_USER_ID\`: 创建用户ID - \`CREATE_USER_NAME\`: 创建用户名 - \`UPDATE_TIME\`: 更新时间 - \`UPDATE_USER_ID\`: 更新用户ID - \`UPDATE_USER_NAME\`: 更新用户名 - \[TENANT_ID\](file://E:\\SupplyChain\\mdgylxt_sourcing\\md-gyl-sourcing-api\\src\\main\\java\\com\\mdgyl\\hussar\\sourcing\\inquiry\\service\\impl\\InquiryApiServiceImpl.java#L36-L36): 租户ID - \`DELETE_FLAG\`: 删除标识(默认正常状态)\*UPDATE 操作\*\*: - \`UPDATE_TIME\`: 更新时间 - \`UPDATE_USER_ID\`: 更新用户ID - \`UPDATE_USER_NAME\`: 更新用户名 技术特点 - 使用反射机制动态设置对象属性 - 通过 \`BaseSecurityUtil\` 获取当前登录用户信息 - 支持对 \`MapperMethod.ParamMap\` 类型参数的处理 - 具备字段存在性检查,避免对不存在的字段进行操作 - 异常处理完善,出现错误时记录日志但不影响主流程 ```java package com.mdgyl.hussar.sourcing.config; import com.jxdinfo.hussar.common.security.BaseSecurityUtil; import com.jxdinfo.hussar.common.security.SecurityUser; import com.mdgyl.common.constants.DatabaseConstants; import com.mdgyl.common.enums.DeleteStatusEnum; import com.mdgyl.common.util.ReflectionUtils; import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.binding.MapperMethod; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.*; import org.springframework.stereotype.Component; import java.lang.reflect.Field; import java.time.LocalDateTime; import java.util.Objects; import java.util.Properties; /** * description: mybatis全局拦截器 * author: wuyc * createTime: 2023-12-02 14:03:19 */ @Slf4j @Component @Intercepts({ @Signature( type = Executor.class, method = "update", args = {MappedStatement.class, Object.class} ) }) public class MybatisUpdateInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; // 获取Mybatis的当前操作方法名称 String sqlCommandType = mappedStatement.getSqlCommandType().name(); if (!DatabaseConstants.SQL_COMMAND_TYPE_LIST.contains(sqlCommandType)) { return invocation.proceed(); } if (invocation.getArgs().length < 2) { return invocation.proceed(); } // 获取Mybatis插入或更新时传入的参数对象 Object paramEntity = invocation.getArgs()[1]; this.interceptInsertOrUpdateMethod(sqlCommandType, paramEntity); return invocation.proceed(); } /** * 插入或更新时的参数拦截方法 * * @param methodName Mybatis方法名称 * @param paramEntity 参数实体类 */ private void interceptInsertOrUpdateMethod(String methodName, Object paramEntity) { SecurityUser securityUser = getSecurityUser(); LocalDateTime systemTime = LocalDateTime.now(); // 若Mybatis的当前方法为INSERT if (DatabaseConstants.INSERT.equals(methodName)) { fillInsertMethodProperties(paramEntity, securityUser, systemTime); return; } // 若Mybatis的当前方法为UPDATE if (DatabaseConstants.UPDATE.equals(methodName)) { fillUpdateMethodProperties(paramEntity, securityUser, systemTime); } } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { } // -------------------------------------- private method start, 私有方法放公有方法下面 -------------------------------------------- /** * 新增数据时填充创建人、创建时间 * * @param paramEntity 新增数据参数对象 * @param securityUser 用户信息 * @param systemTime 当前系统时间 */ private void fillInsertMethodProperties(Object paramEntity, SecurityUser securityUser, LocalDateTime systemTime) { fillProperties(paramEntity, DatabaseConstants.CREATE_TIME, systemTime); fillProperties(paramEntity, DatabaseConstants.CREATE_USER_ID, securityUser.getUserId()); fillProperties(paramEntity, DatabaseConstants.CREATE_USER_NAME, securityUser.getUserName()); fillProperties(paramEntity, DatabaseConstants.UPDATE_TIME, systemTime); fillProperties(paramEntity, DatabaseConstants.UPDATE_USER_ID, securityUser.getUserId()); fillProperties(paramEntity, DatabaseConstants.UPDATE_USER_NAME, securityUser.getUserName()); fillProperties(paramEntity, DatabaseConstants.TENANT_ID, securityUser.getTenantId()); fillProperties(paramEntity, DatabaseConstants.DELETE_FLAG, DeleteStatusEnum.NORMAL.getCode()); } /** * 编辑数据时填充创建人、创建时间 * * @param paramEntity 新增数据参数对象 * @param securityUser 登录用户信息 * @param systemTime 当前系统时间 */ private void fillUpdateMethodProperties(Object paramEntity, SecurityUser securityUser, LocalDateTime systemTime) { if (!(paramEntity instanceof MapperMethod.ParamMap)) { return; } MapperMethod.ParamMap> paramMap = (MapperMethod.ParamMap>) paramEntity; Object updateEntity = paramMap.containsKey(DatabaseConstants.ET) ? paramMap.get(DatabaseConstants.ET) : paramMap.get(DatabaseConstants.PARAM1); if (Objects.isNull(updateEntity)) { return; } fillProperties(updateEntity, DatabaseConstants.UPDATE_TIME, systemTime); fillProperties(updateEntity, DatabaseConstants.UPDATE_USER_ID, securityUser.getUserId()); fillProperties(updateEntity, DatabaseConstants.UPDATE_USER_NAME, securityUser.getUserName()); } private Object getValue(Object updateEntity, String fieldName) { Field updateUserIdField; try { updateUserIdField = updateEntity.getClass().getDeclaredField(fieldName); updateUserIdField.setAccessible(true); return updateUserIdField.get(updateEntity); } catch (NoSuchFieldException | IllegalAccessException e) { log.error("获取对象值失败: " + fieldName, e); } return null; } /** * 校验字段在数据是否存在,存在的话设置默认值 * * @param paramEntity 操作对象 * @param propertiesKey 数据库的key * @param propertiesValue 默认赋的值 */ private void fillProperties(Object paramEntity, String propertiesKey, Object propertiesValue) { if (!ReflectionUtils.existsField(paramEntity, propertiesKey)) { return; } Object value = getValue(paramEntity, propertiesKey); if (value != null) { return; } try { ReflectionUtils.invokeSetterMethod(paramEntity, propertiesKey, propertiesValue); } catch (Exception e) { log.error("MybatisParamInterceptor fillProperties error,propertiesKey: {}, propertiesValue: {}, failReason: {}", propertiesKey, propertiesValue, e.getMessage()); } } /** * 获取系统登录用户信息 * * @return SecurityUser */ private SecurityUser getSecurityUser() { SecurityUser securityUser = BaseSecurityUtil.getUser(); return Objects.isNull(securityUser) ? new SecurityUser() : securityUser; } } ``` ## 三 MybatisQueryInterceptor 类介绍 MybatisQueryInterceptor 是一个 MyBatis 查询拦截器,主要用于自动添加逻辑删除条件到 SELECT 查询语句中。 基本信息 类名: MybatisQueryInterceptor 注解: @Component: 标识为 Spring 组件 @Intercepts: 定义拦截点,拦截 StatementHandler 的 prepare 方法 @Slf4j: 提供日志记录功能 主要功能 1. 拦截查询语句 拦截 MyBatis 的 SQL 查询语句执行过程 只处理以 SELECT 开头的 SQL 语句 2. 自动添加逻辑删除条件 针对以 t_ 开头的业务表(忽略大小写) 自动在查询语句中添加 delete_flag = 0 条件 避免查询到已被逻辑删除的数据 核心处理逻辑 SQL 语句识别: 通过 StatementHandler 获取原始 SQL 语句 判断是否为 SELECT 语句 表名识别: 解析 SQL 语句,提取 FROM 子句后的表名 只处理表名以 t_ 开头的业务表 条件添加策略: 如果 SQL 中没有 WHERE 子句,则添加 WHERE delete_flag = 0 如果已有 WHERE 子句但不含 delete_flag 条件,则添加 AND delete_flag = 0 如果已包含 delete_flag 条件,则不做处理 特殊处理包含 LIMIT 子句的情况 核心方法 intercept: 拦截入口方法,实现 SQL 语句的解析和修改 plugin: 包装目标对象,返回代理对象 setProperties: 设置插件属性(当前未实现具体功能) 技术特点 使用正则表达式分割 SQL 语句来识别不同部分 通过反射工具 ReflectionUtils 修改 BoundSql 中的 SQL 语句 支持多种 SQL 结构的处理(带/不带 WHERE、LIMIT 等) 具备完善的日志记录功能,便于调试和监控 ```java package com.mdgyl.hussar.sourcing.config; import com.mdgyl.common.util.ReflectionUtils; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.plugin.*; import org.springframework.stereotype.Component; import java.sql.Connection; import java.util.Properties; @Slf4j @Component @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}) }) public class MybatisQueryInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 获取原始的 SQL 语句 StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); String originalSql = statementHandler.getBoundSql().getSql(); if (StringUtils.isEmpty(originalSql) || !originalSql.trim().toLowerCase().startsWith("select")) { return invocation.proceed(); } log.info("拦截器拼接前的sql是: {}", originalSql); // 表名前缀为 t_开头的是业务的表 忽略大小写 String[] split = originalSql.split("(?i)from"); String[] tableNameArr = split[1].split("(?i)where"); if (!tableNameArr[0].trim().startsWith("t_")) { return invocation.proceed(); } // 修改 SQL,添加逻辑删除字段的条件 String[] sqlStrArr = originalSql.split("(?i)where"); StringBuilder stringBuilder = new StringBuilder(originalSql); // 判断数据库是否有删除标识字段,如果有的话,就拼接 if (sqlStrArr.length <= 1) { if (originalSql.toLowerCase().contains("limit")) { stringBuilder = new StringBuilder(originalSql.replaceFirst("(?i)limit", "WHERE delete_flag = 0 limit ")); } else { stringBuilder = new StringBuilder(originalSql + " WHERE delete_flag = 0"); } } else if (!sqlStrArr[1].contains("delete_flag")) { stringBuilder = new StringBuilder(originalSql.replaceFirst("(?i)where", "WHERE delete_flag = 0 AND ")); } log.info("拦截器拼接后的sql是: {}", stringBuilder); String modifiedSql = stringBuilder.toString(); try { ReflectionUtils.setFieldValue(statementHandler.getBoundSql(), "sql", modifiedSql); } catch (Exception e) { log.error("MybatisQueryInterceptor invokeSetterMethod error,modifiedSql: {},failReason: {}", modifiedSql, e.getMessage()); } // 执行拦截的方法 return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // 这里可以用来设置插件的属性 } } ```