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

}
相关推荐
摇滚侠4 小时前
Mybatis 入门到项目实战 搭建 MyBatis 框架 01-14
java·tomcat·mybatis
老马聊技术4 小时前
AI对话功能之SpringBoot整合Vue3
vue.js·人工智能·spring boot·后端
武子康4 小时前
调查研究-174 什么是“红丸主义“:它为什么吸引人,又为什么容易把人带偏?
后端
神奇小汤圆4 小时前
白嫖DeepSeek V4 Pro!免费无限用,还能接入Claude-Code
后端
码不停蹄的玄黓4 小时前
SpringBoot 全局异常处理器实现
java·spring boot·后端
JS菌5 小时前
Skills 动态加载系统:让 AI Agent 按需获取领域知识
前端·人工智能·后端
神奇小汤圆5 小时前
Vector Graph RAG 开源!一套向量数据库同时搞定语义检索+RAG多跳
后端
小高学习java5 小时前
事务的边界问题,如何判断数据回滚时机。
java·数据库·后端
何极光5 小时前
Maven安装与配置
java·maven
Ting.~5 小时前
在java中接入百度地图
java·开发语言·dubbo