MyBatis拦截器终极指南:从原理到企业级实战

在本篇文章中,我们将深入了解如何编写一个 MyBatis 拦截器,并通过一个示例来展示如何在执行数据库操作(如插入或更新)时,自动填充某些字段(例如 createdByupdatedBy)信息。本文将详细讲解拦截器的工作原理、代码示例及其在 MyBatis 项目中的应用。

一、为什么需要数据库操作拦截器?

在典型的企业级应用开发中,我们经常会遇到这样的需求:

  1. 需要自动记录数据行的创建时间和修改时间

  2. 需要跟踪记录数据操作人

  3. 需要实现全局的软删除逻辑

  4. 需要对敏感数据进行自动加密

  5. 需要实现SQL执行监控和慢查询统计

传统做法是在每个Mapper方法中手动添加这些逻辑,但这样会导致大量重复代码。MyBatis拦截器(Interceptor)正是为了解决这类问题而生,它可以在SQL执行的各个阶段插入自定义逻辑,实现横切关注点的统一管理。

二、MyBatis拦截器核心原理

如果对于MyBatis核心组件功能还不了解的小伙伴们建议先跳到最后一章节:第五节去了解MyBatis核心组件功能再倒回来继续学习!

2.1 拦截器架构

MyBatis采用责任链模式实现拦截器机制,主要拦截点包括:

拦截接口拦截时机典型应用场景
ExecutorSQL执行前后(增删改查操作)事务管理、分页处理
StatementHandlerSQL语句构建时SQL改写、危险操作拦截
ParameterHandler参数处理时参数加密、参数校验
ResultSetHandler结果集处理时结果解密、数据脱敏

2.2 拦截器生命周期

MyBatis 中的拦截器生命周期较为简单。它的生命周期由 Plugin.wrap() 方法控制,首先会创建代理对象并包装目标对象。拦截器的 intercept 方法在执行目标方法前被调用,拦截器的 plugin 方法用于对目标方法的代理,setProperties 方法用于注入拦截器所需的配置属性。

拦截目标方法调用的顺序:当拦截器装载到 MyBatis 配置中后,每次执行目标方法(如数据库操作的 insertupdate)时,都会经过拦截器链。如果多个拦截器存在,它们会按照配置顺序依次执行。可以通过实现Ordered接口或使用@Order注解控制执行顺序:

@Intercepts(...)
@Order(Ordered.HIGHEST_PRECEDENCE)
public class FirstInterceptor implements Interceptor {}

@Intercepts(...)
@Order(Ordered.LOWEST_PRECEDENCE)
public class LastInterceptor implements Interceptor {}
关键接口解析
  1. 初始化阶段:通过@Intercepts注解声明拦截目标

  2. 代理阶段:通过plugin()方法创建代理对象

  3. 执行阶段:intercept()方法处理拦截逻辑

  4. 配置阶段:通过XML或Java Config注册拦截器

// 典型拦截器声明
@Intercepts({
    @Signature(type = Executor.class, 
              method = "update",
              args = {MappedStatement.class, Object.class})
})
public class CustomInterceptor implements Interceptor {
    // 实现方法...
}

2.3 代理模式与责任链模式

  • 代理模式(Proxy Pattern):MyBatis 拦截器本质上是通过代理模式对目标对象进行包装,通过 Plugin.wrap() 方法将拦截器包装在目标对象上,形成一个代理对象。这个代理对象会拦截对目标方法的调用,执行预定的增强逻辑,再将控制权交给目标方法。
  • 责任链模式(Chain of Responsibility Pattern):MyBatis 的拦截器是按顺序进行链式调用的,多个拦截器会组成一个链,按照声明顺序依次执行,直到所有拦截器都被调用。

三、实战:实现自动化字段填充

3.1 需求分析

实现以下字段的自动填充:

字段名插入时自动填充更新时自动填充数据类型
created_by✔️String
created_time✔️Date
updated_by✔️String
updated_time✔️Date
is_deleted✔️(默认0)Integer

 3.2 完整实现代码解析

package com.wanren.subject.application.interceptor;

import com.wanren.subject.common.util.LoginUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.Executor;

/**
 *@ClassName MybatisInterceptor
 *@Description 填充createBy,createTime等公共字段的拦截器
 *@Author 彭于晏
 *Date 2025/2/14 0:31
 *Version 1.0
 **/
@Component
@Slf4j
//@Signature指明该拦截器需要拦截哪一个接口的哪一个方法,包括如下
// 接口类型:Executor(拦截执行器方法,负责调用StatementHandler操作数据库)
// StatementHandler(//拦截SQL语法构建处理,直接在数据库执行SQL脚本的对象)、
// ParameterHandler(//拦截参数处理)和ResultSetHandler(//拦截结果集处理,ResultSet结果集对象转换成List类型的集合)
//对应接口中的某一个方法的参数,比如Executor中query方法因为重载原因,有多个,args就是指明参数类型,从而确定是具体哪一个方法。
@Intercepts(@Signature(type = Executor.class,method = "update",args = {
    MappedStatement.class,Object.class
}))
public class MybatisInterceptor implements Interceptor {
    //当被拦截的数据库操作(如查询、插入、更新)发生时,intercept 方法会被调用。
    //invocation的三个方法:Object target = invocation.getTarget();//被代理对象
    //Method method = invocation.getMethod();//代理方法
    //Object[] args = invocation.getArgs();//方法参数
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        //MyBatis 中封装 SQL 语句信息的类,表示一个 SQL 映射。MappedStatement 对象包含 SQL 执行所需的所有信息,比如 SQL 命令类型、SQL 语句、参数类型等
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
        Object parameter = invocation.getArgs()[1];//parameter 是一个 Object 类型,通常是实体类对象
        if(parameter == null){
            //执行目标方法(invocation.proceed())
            return invocation.proceed();
        }
        //获取当前登录用户的id
        String loginId = LoginUtil.getLoginId();
        if(StringUtils.isBlank(loginId)){
            return invocation.proceed();
        }
        if(sqlCommandType.INSERT == sqlCommandType || sqlCommandType.UPDATE == sqlCommandType){
            replaceEntityProperty(parameter,loginId,sqlCommandType);
        }
        return invocation.proceed();
    }

    private void replaceEntityProperty(Object parameter, String loginId, SqlCommandType sqlCommandType) {
        if(parameter instanceof Map){
            replaceMap((Map) parameter,loginId,sqlCommandType);
        }else{
            replace(parameter,loginId,sqlCommandType);
        }
    }

    private void replace(Object parameter, String loginId, SqlCommandType sqlCommandType) {
        if(SqlCommandType.INSERT == sqlCommandType)
        {
            dealInsert(parameter,loginId);
        }else{
            dealUpdate(parameter,loginId);
        }
    }

    private void dealUpdate(Object parameter, String loginId) {
        Field[] fields = getAllFields(parameter);
        for (Field field : fields) {
            try {
                field.setAccessible(true);
                //parameter是字段名,field是字段的值
                Object o = field.get(parameter);
                //如果该字段不为null,则跳过
                if (Objects.nonNull(o)) {
                    field.setAccessible(false);
                    continue;
                }
                if ("updateBy".equals(field.getName())) {
                    field.set(parameter, loginId);
                    field.setAccessible(false);
                } else if ("updateTime".equals(field.getName())) {
                    field.set(parameter, new Date());
                    field.setAccessible(false);
                } else {
                    field.setAccessible(false);
                }
            }catch (Exception e){
                log.error("dealUpdate.error:{}", e.getMessage(), e);
            }
        }
    }

    private void dealInsert(Object parameter, String loginId) {
        Field[] fields = getAllFields(parameter);
        for(Field field : fields){
            try{
                //Java 语言的私有字段是不可访问的。如果你想访问私有字段(即在类外部访问 private 字段),
                // 你必须调用 setAccessible(true) 来打破 Java 的访问控制,允许访问这些字段。
                field.setAccessible(true);
                Object o = field.get(parameter);
                if (Objects.nonNull(o)) {
                    field.setAccessible(false);
                    continue;
                }
                if("isDeleted".equals(field.getName())){
                    field.set(parameter,0);
                    field.setAccessible(false);
                }
                else if ("createdBy".equals(field.getName())) {
                    field.set(parameter, loginId);
                    field.setAccessible(false);
                } else if ("createdTime".equals(field.getName())) {
                    field.set(parameter, new Date());
                    field.setAccessible(false);
                }else {
                    field.setAccessible(false);
                }
            }catch (Exception e){
                log.error("dealInsert.error:{}", e.getMessage(), e);
            }
        }
    }

    private Field[] getAllFields(Object parameter) {
        Class<?> clazz = parameter.getClass();
        List<Field> fieldList = new ArrayList<>();
        while (clazz != null){
            fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));
            clazz = clazz.getSuperclass();
        }
        Field[] fields =new Field[fieldList.size()];
        fieldList.toArray(fields);
        return fields;
    }

    private void replaceMap(Map parameter, String loginId, SqlCommandType sqlCommandType) {
        for(Object val : parameter.values()){
            replace(val,loginId,sqlCommandType);
        }
    }

    //插件用于封装目标对象,通过这个方法可以返回目标对象本身,也可以返回一个他的代理,决定
    //是否要进行拦截,从而进一步决定要返回一个怎样的对象
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
    //如果我们拦截器需要用到一些变量参数,而且这个参数是支持可配置的,类似Spring中的@Value("${}")从application.properties文件获取自定义变量属性,这个时候我们就可以使用这个方法。
    //在拦截器插件的setProperties方法中进行。这些自定义属性参数会在项目启动的时候被加载。
    @Override
    public void setProperties(Properties properties) {
    }
}

四、扩展应用场景

4.1 敏感数据加密

public Object intercept(Invocation invocation) {
    Object parameter = invocation.getArgs()[1];
    if (parameter instanceof User) {
        User user = (User) parameter;
        user.setPassword(encrypt(user.getPassword()));
    }
    return invocation.proceed();
}

4.2 SQL执行监控 

public Object intercept(Invocation invocation) {
    long start = System.nanoTime();
    try {
        return invocation.proceed();
    } finally {
        long cost = (System.nanoTime() - start)/1000000;
        if(cost > 1000){
            log.warn("慢查询警告: {}ms - {}", cost, getSql(invocation));
        }
        
        Metrics.counter("sql.total.count").increment();
        Metrics.timer("sql.execute.time").record(cost, MILLISECONDS);
    }
}

4.3 多租户数据隔离

public void replaceEntityProperty(Object parameter, String tenantId) {
    if (parameter instanceof TenantAware) {
        ((TenantAware) parameter).setTenantId(tenantId);
    }
}

 五、MyBatis核心组件功能详解

从MyBatis代码实现的角度来看,MyBatis的核心组成部分涉及到多个组件和接口,它们共同协作完成了数据库操作的管理、映射及优化等工作。以下是MyBatis的核心部件及其功能的详细介绍:

1. Configuration(配置对象)

功能概述
Configuration 是 MyBatis 的全局配置中心,负责管理所有配置信息,包括数据库连接、映射器、类型处理器、插件等。它是 MyBatis 初始化的核心类。

核心职责

  • 加载并解析 MyBatis 配置文件(如 mybatis-config.xml)。

  • 管理 Mapper 文件和接口的配置信息。

  • 注册类型处理器(TypeHandler)和对象工厂(ObjectFactory)。

  • 维护拦截器链(InterceptorChain),用于支持插件扩展。

2. SqlSessionFactory(会话工厂)

功能概述
SqlSessionFactory 是 MyBatis 的核心工厂类,用于创建 SqlSession 实例。它是线程安全的,通常在应用启动时初始化。

核心职责

  • 根据 Configuration 创建 SqlSession

  • 管理数据库连接池和事务工厂。

  • 加载 Mapper 文件和对应的映射语句。

3. SqlSession(会话)

功能概述
SqlSession 是 MyBatis 与数据库交互的核心接口,提供了执行 SQL、管理事务、获取 Mapper 接口等功能。

核心职责

  • 执行 CRUD 操作(selectinsertupdatedelete)。

  • 提供事务管理、批量操作、缓存等功能。

  • 获取 Mapper 接口的代理对象。

4. Executor(执行器)

功能概述
Executor 是 MyBatis 的内部执行器,负责执行 SQL 语句并处理结果。它是 MyBatis 中的核心对象之一,负责查询、插入、更新等数据库操作。

核心职责

  • 调用 StatementHandler 执行 SQL 语句。

  • 管理一级缓存和二级缓存。

  • 触发拦截器链,支持插件扩展。

5. StatementHandler(语句处理器)

功能概述

StatementHandler 负责将 SQL 语句发送到数据库执行,主要完成 SQL 的解析、生成和执行。

核心职责

  • 创建 PreparedStatement 并设置参数。

  • 执行 SQL 并返回结果。

  • 支持一级缓存,避免重复查询。

6. ParameterHandler(参数处理器)

功能概述
ParameterHandler 负责将 Java 对象转换为 JDBC 参数,并设置到 PreparedStatement 中。

核心职责

  • 处理 SQL 参数的映射。

  • 将 Java 对象转换为 JDBC 所需的参数格式。

7. ResultSetHandler(结果集处理器)

功能概述
ResultSetHandler 负责将 JDBC 返回的 ResultSet 转换为 Java 对象集合。

核心职责

  • 解析 ResultSet 结果集。

  • 将数据库查询结果映射为 Java 对象。

8. TypeHandler(类型处理器)

功能概述
TypeHandler 用于 Java 类型和数据库类型之间的转换。负责将 Java 对象的属性值转换为数据库支持的数据类型,或者将数据库查询结果转化为 Java 对象。

核心职责

  • 处理 Java 类型与 JDBC 类型的映射。

  • 支持自定义类型转换。

9. MappedStatement(映射语句)

功能概述
MappedStatement负责封装单个 <select>、<insert>、<update>、<delete> 等 SQL 语句的配置信息。包括 SQL 类型、参数映射、结果映射等。

核心职责

  • 存储 SQL 语句、参数映射、返回值映射等信息。

  • 提供 SQL 执行的上下文信息。

10. SqlSource(SQL 源)

功能概述
SqlSource 负责根据传入的 parameterObject 动态生成 SQL 语句。它通过 BoundSql 对象封装 SQL 和参数,并返回给 StatementHandler 执行。

核心职责

  • 动态生成 SQL 语句。

  • 根据传入的参数对象构建 SQL 语句,并封装到 BoundSql 中。

11. BoundSql(封装的 SQL)

功能概述

BoundSql 负责封装动态生成的 SQL 语句和参数信息。它是 SqlSource 生成 SQL 后的载体,存储了 SQL 语句及其相关参数信息。

核心职责

  • 存储动态生成的 SQL 语句。

  • 提供 SQL 执行所需的参数信息。

"优秀的框架设计应该像电路板一样,允许开发者在不修改核心逻辑的情况下,通过插件机制扩展功能。" —— MyBatis设计哲学

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/970247.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2月第九讲“探秘Transformer系列”

0.1 流程 使用Transformer来进行文本生成其实就是用模型来预测下一个词&#xff0c;完整流程包括多个阶段&#xff0c;如分词、向量化、计算注意力和采样&#xff0c;具体运作流程如下&#xff1a; 分词&#xff08;tokenize&#xff09;。把用户的输入文本&#xff08;此处假…

crewai框架(0.83.0)添加知识源

官方的文档如下 https://docs.crewai.com/concepts/knowledge但是不知道为什么&#xff0c;可能是版本的问题&#xff08;我用的是0.86.0&#xff09;&#xff0c;参考官方文档的配置我会报错&#xff0c;并且也导入不了数据库&#xff0c;也可能用的不是官方API。本文以常用的…

deepseek + embeding模型搭建本地知识库

上一篇文章讲了ollamadeepseek模型的本地化部署&#xff0c;具体能部署哪一款取决于你的土豪程度&#xff1a; 今天的目标是本地安装部署embeding模型&#xff0c;实现LLMembeding模型的rag知识库的本地化部署&#xff0c;包括&#xff1a; embeding模型的本地化部署anyhingL…

2、树莓派5第一次开机三种方式:使用外设 / 使用网线 / 使用wifi

本文整理了树莓派第一次开机方式&#xff0c;供大家参考 方式一&#xff1a;连接鼠标、键盘、显示器外设开机 树莓派自带USB接口及HDMI接口&#xff0c;因此可以通过USB连接鼠标键盘&#xff0c;HDMI接入显示器&#xff0c;再进行电源供电&#xff0c;就可以完成第一次开机 …

案例-02.部门管理-查询

一.查询部门-需求 二.查询部门-思路 API接口文档 三.代码实现 1.controller层&#xff1a;负责与前端进行交互&#xff0c;接收前端所发来的请求 注&#xff1a;Slf4j用于记录日志使用&#xff0c;可以省略private static Logger log LoggerFactory.getLogger(DeptControlle…

小程序包体积优化指南:静态资源条件编译与分包编译技巧

在开发小程序时&#xff0c;可能大家都遇到过包体积超限的情况&#xff0c;这对多平台支持、用户体验和加载速度带来不少困扰。UniApp 提供了一些强大的功能&#xff0c;比如静态资源的条件编译和分包编译&#xff0c;这些功能可以帮助我们减少小程序的包体积&#xff0c;提高加…

12. QT控件:多元素控件

0. 概述 Qt中提供的多元素控件 QListWidget QListView QTableWidget QTableView QTreeWidget QTreeView xxWidget 和 xxView的区别 以QTableWidget 和 QTableView 为例&#xff1a; QTableView 是基于MVC设计的控件&#xff0c;QTableView自身不持有数据。使用QTableView需…

CAS单点登录(第7版)20.用户界面

如有疑问&#xff0c;请看视频&#xff1a;CAS单点登录&#xff08;第7版&#xff09; 用户界面 概述 概述 对 CAS 用户界面 &#xff08;UI&#xff09; 进行品牌化涉及编辑 CSS 样式表以及一小部分相对简单的 HTML 包含文件&#xff0c;也称为视图。&#xff08;可选&…

android 的抓包工具

charles 抓包工具 官网地址 nullCharles Web Debugging Proxy - Official Sitehttps://www.charlesproxy.com/使用手册一定记得看官网 SSL Certificates • Charles Web Debugging Proxy http请求&#xff1a; 1.启动代理&#xff1a; 2.设置设备端口 3.手机连接当前代理 …

关于视频去水印的一点尝试

一. 视频去水印的几种方法 1. 使用ffmpeg delogo滤镜 delogo 滤镜的原理是通过插值算法&#xff0c;用水印周围的像素填充水印的位置。 示例&#xff1a; ffmpeg -i input.mp4 -filter_complex "[0:v]delogox420:y920:w1070:h60" output.mp4 该命令表示通过滤镜…

预测技术在美团弹性伸缩场景的探索与应用

管理企业大规模服务的弹性伸缩场景中&#xff0c;往往会面临着两个挑战&#xff1a;第一个挑战是精准的负载预测&#xff0c;由于应用实例的启动需要一定预热时间&#xff0c;被动响应式伸缩会在一段时间内影响服务质量&#xff1b;第二个挑战是高效的资源分配&#xff0c;即在…

【含开题报告+文档+PPT+源码】基于Spring+Vue的拾光印记婚纱影楼管理系统

开题报告 本论文旨在探讨基于Spring和Vue框架的拾光印记婚纱影楼管理系统的设计与实现。该系统集成了用户注册登录、个人资料修改、婚庆资讯浏览、婚庆套餐查看、婚纱拍摄预约、婚纱浏览与租赁、客片查看以及在线客服等多项功能&#xff0c;为用户提供了一站式的婚纱影楼服务体…

ASP.NET Core 使用 FileStream 将 FileResult 文件发送到浏览器后删除该文件

FileStream 在向浏览器发送文件时节省了服务器内存和资源&#xff0c;但如果需要删除文件怎么办&#xff1f;本文介绍如何在发送文件后删除文件&#xff1b;用 C# 编写。 另请参阅&#xff1a;位图创建和下载 使用FileStream向浏览器发送数据效率更高&#xff0c;因为文件是从…

字节跳动后端二面

&#x1f4cd;1. 数据库的事务性质&#xff0c;InnoDB是如何实现的&#xff1f; 数据库事务具有ACID特性&#xff0c;即原子性、一致性、隔离性和持久性。InnoDB通过以下机制实现这些特性&#xff1a; &#x1f680; 实现细节&#xff1a; 原子性&#xff1a;通过undo log实…

【个人开发】cuda12.6安装vllm安装实践【内含踩坑经验】

1. 背景 vLLM是一个快速且易于使用的LLM推理和服务库。企业级应用比较普遍&#xff0c;尝试安装相关环境&#xff0c;尝试使用。 2. 环境 模块版本python3.10CUDA12.6torch2.5.1xformers0.0.28.post3flash_attn2.7.4vllm0.6.4.post1 2.1 安装flash_attn 具体选择什么版本&…

19.4.9 数据库方式操作Excel

版权声明&#xff1a;本文为博主原创文章&#xff0c;转载请在显著位置标明本文出处以及作者网名&#xff0c;未经作者允许不得用于商业目的。 本节所说的操作Excel操作是讲如何把Excel作为数据库来操作。 通过COM来操作Excel操作&#xff0c;请参看第21.2节 在第19.3.4节【…

2024-2025年主流的开源向量数据库推荐

以下是2024-2025年主流的开源向量数据库推荐&#xff0c;涵盖其核心功能和应用场景&#xff1a; 1. Milvus 特点&#xff1a;专为大规模向量搜索设计&#xff0c;支持万亿级向量数据集的毫秒级搜索&#xff0c;适用于图像搜索、聊天机器人、化学结构搜索等场景。采用无状态架…

vue项目使用vite和vue-router实现history路由模式空白页以及404问题

开发项目的时候&#xff0c;我们一般都会使用路由&#xff0c;但是使用hash路由还是history路由成为了两种选择&#xff0c;因为hash路由在url中带有#号&#xff0c;history没有带#号&#xff0c;看起来更加自然美观。但是hash速度更快而且更通用&#xff0c;history需要配置很…

AI编程01-生成前/后端接口对表-豆包(或Deepseek+WPS的AI

前言: 做过全栈的工程师知道,如果一个APP的项目分别是前端/后端两个团队开发的话,那么原型设计之后,通过接口文档进行开发对接是非常必要的。 传统的方法是,大家一起定义一个接口文档,然后,前端和后端的工程师进行为何,现在AI的时代,是不是通过AI能协助呢,显然可以…