Springboot中全局myBaits插件配置

一、mybatis拦截器配置

类基本信息

类名: DataSourceConfig

包路径: com.mdgyl.hussar.sourcing.config

注解: 使用了 @Configuration,表明这是一个Spring配置类

主要功能

  1. 数据源配置

通过 @Resource 注入了外部配置的 DataSource 数据源

在 SqlSessionFactoryBeanCustomizer 中将数据源设置到 customizeFactoryBean 上

  1. 自定义拦截器配置

注入了两个自定义拦截器:

MybatisUpdateInterceptor: 更新操作拦截器

MybatisQueryInterceptor: 查询操作拦截器

将这两个拦截器注册到 SqlSessionFactoryBean 中,形成拦截器链

  1. 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` 注解配置拦截点

  1. 自动填充字段

针对不同的数据库操作自动填充相应字段:

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 语句

  1. 自动添加逻辑删除条件

针对以 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) {
        // 这里可以用来设置插件的属性
    }

}
相关推荐
无限的鲜花3 小时前
反射(原创推荐)
java·开发语言
IT二叔3 小时前
Java项目部署-03-teamcity-cicd-docker镜像流水线方式部署
java·ci/cd·持续部署
一路向北he3 小时前
字节钢铁军团--“提供情境,而非控制”
java·开发语言·前端
码事漫谈3 小时前
别写Prompt了,现在流行给AI“写循环”
后端
超级数据查看器4 小时前
超级数据查看器 v10.0 发布
java·大数据·数据库·sqlite·安卓
Kyrie_Li4 小时前
Spring Boot Kafka 生产级配置全解析:从入门到精通
spring boot·后端·kafka
Coder_Shenshen5 小时前
西门子S7CommPlus协议鉴权算法原理与流程详解
网络·后端·算法
折哥的程序人生 · 物流技术专研5 小时前
《Java 100 天进阶之路》第50篇:阻塞队列与并发容器(2026版)
java·面试题·java进阶·blockingqueue·并发容器·集合源码·java100天进阶
ai_coder_ai5 小时前
编写自动化脚本,在自己后端服务中使用Open Api进行设备相关操作
java·运维·自动化
yuhaiqiang6 小时前
随手 vibecoding 的浏览器插件已经 6000 多次下载,聊聊他的产品设计
前端·后端·面试