MyBatis拦截器终极指南:从原理到企业级实战
在本篇文章中,我们将深入了解如何编写一个 MyBatis 拦截器,并通过一个示例来展示如何在执行数据库操作(如插入或更新)时,自动填充某些字段(例如 createdBy 和 updatedBy)信息。本文将详细讲解拦截器的工作原理、代码示例及其在 MyBatis 项目中的应用。
一、为什么需要数据库操作拦截器?
在典型的企业级应用开发中,我们经常会遇到这样的需求:
-
需要自动记录数据行的创建时间和修改时间
-
需要跟踪记录数据操作人
-
需要实现全局的软删除逻辑
-
需要对敏感数据进行自动加密
-
需要实现SQL执行监控和慢查询统计
传统做法是在每个Mapper方法中手动添加这些逻辑,但这样会导致大量重复代码。MyBatis拦截器(Interceptor)正是为了解决这类问题而生,它可以在SQL执行的各个阶段插入自定义逻辑,实现横切关注点的统一管理。
二、MyBatis拦截器核心原理
如果对于MyBatis核心组件功能还不了解的小伙伴们建议先跳到最后一章节:第五节去了解MyBatis核心组件功能再倒回来继续学习!
2.1 拦截器架构
MyBatis采用责任链模式实现拦截器机制,主要拦截点包括:
| 拦截接口 | 拦截时机 | 典型应用场景 |
|---|---|---|
| Executor | SQL执行前后(增删改查操作) | 事务管理、分页处理 |
| StatementHandler | SQL语句构建时 | SQL改写、危险操作拦截 |
| ParameterHandler | 参数处理时 | 参数加密、参数校验 |
| ResultSetHandler | 结果集处理时 | 结果解密、数据脱敏 |
2.2 拦截器生命周期
MyBatis 中的拦截器生命周期较为简单。它的生命周期由 Plugin.wrap() 方法控制,首先会创建代理对象并包装目标对象。拦截器的 intercept 方法在执行目标方法前被调用,拦截器的 plugin 方法用于对目标方法的代理,setProperties 方法用于注入拦截器所需的配置属性。
拦截目标方法调用的顺序:当拦截器装载到 MyBatis 配置中后,每次执行目标方法(如数据库操作的 insert、update)时,都会经过拦截器链。如果多个拦截器存在,它们会按照配置顺序依次执行。可以通过实现Ordered接口或使用@Order注解控制执行顺序:
@Intercepts(...)
@Order(Ordered.HIGHEST_PRECEDENCE)
public class FirstInterceptor implements Interceptor {}@Intercepts(...)
@Order(Ordered.LOWEST_PRECEDENCE)
public class LastInterceptor implements Interceptor {}
关键接口解析
-
初始化阶段:通过@Intercepts注解声明拦截目标
-
代理阶段:通过plugin()方法创建代理对象
-
执行阶段:intercept()方法处理拦截逻辑
-
配置阶段:通过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();//方法参数@Overridepublic 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();}//获取当前登录用户的idString 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);}}//插件用于封装目标对象,通过这个方法可以返回目标对象本身,也可以返回一个他的代理,决定//是否要进行拦截,从而进一步决定要返回一个怎样的对象@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}//如果我们拦截器需要用到一些变量参数,而且这个参数是支持可配置的,类似Spring中的@Value("${}")从application.properties文件获取自定义变量属性,这个时候我们就可以使用这个方法。//在拦截器插件的setProperties方法中进行。这些自定义属性参数会在项目启动的时候被加载。@Overridepublic 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 操作(
select、insert、update、delete)。 -
提供事务管理、批量操作、缓存等功能。
-
获取 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设计哲学
相关文章:
MyBatis拦截器终极指南:从原理到企业级实战
在本篇文章中,我们将深入了解如何编写一个 MyBatis 拦截器,并通过一个示例来展示如何在执行数据库操作(如插入或更新)时,自动填充某些字段(例如 createdBy 和 updatedBy)信息。本文将详细讲解拦…...
Pythong 解决Pycharm 运行太慢
Pythong 解决Pycharm 运行太慢 官方给Pycharm自身占用的最大内存设低估了限制,我的Pycharm刚开始默认是256mb。 首先找到自己的Pycharm安装目录 根据合适自己的改 保存,重启Pycharm...
双ESP8266-01S通讯UDP配置
第一台ESP8266(发送命令需要勾---发送新行) ATCWMODE3 ATCWSAP_DEF"CAR_wifi_Master","12345678",5,3 //设置本地wifi名称以及密码 ATCIPSTA_DEF"192.168.4.1" //设置本地IP ATCIFSR …...
Molecular Communication(分子通信)与 Molecular Semantic Communication(分子语义通信)
1. 引言 随着传统无线通信在极端环境(如微观生物体内、海洋深处)中的局限性凸显,分子通信(Molecular Communication, MC)成为一种新型通信范式。分子通信通过分子作为信息载体,在纳米尺度上传输信息&#…...
Cookie:网页浏览背后的“小秘密”
在现代互联网的世界里,Cookie 是一个几乎无处不在的概念。它不仅影响着我们的网页浏览体验,还在背后默默地支持着许多网站的功能和服务。本文将带你全面了解 Cookie 的原理、作用、安全性以及如何管理它们。 一、什么是 Cookie? Cookie 是一…...
日语学习-日语知识点小记-构建基础-JLPT-N4N5阶段(6):動詞ない形について句型
日语学习-日语知识点小记-构建基础-JLPT-N4&N5阶段(6):動詞ない形について句型 1、前言(1)情况说明(2)工程师的信仰2、知识点(1)~動詞な形 +なければなりません(2)~動詞な形 + なくてもいいです(3)に まで までに :区別3、单词(1)日语单词…...
华纳云:如何从服务器日志中发现僵尸进程?
在 CentOS 系统中,僵尸进程通常指那些已经完成执行但仍然在进程表中存在的进程。它们没有实际的执行,但仍然占用系统资源,通常会出现在父进程没有及时回收子进程的状态下。虽然僵尸进程本身不消耗 CPU 或内存资源,但它们会占用进程…...
fastadmin 接口请求提示跨域
问题描述 小程序项目,内嵌h5页面,在h5页面调用后端php接口,提示跨域。网上查找解决方案如下: 1,设置header // 在入口文件index.php直接写入直接写入 header("Access-Control-Allow-Origin:*"); header(&q…...
NHANES指标推荐:DDA!
文章题目:Association of dietary decanoic acid intake with diabetes or prediabetes: an analysis from NHANES 2005-2016 DOI:10.3389/fnut.2024.1483045 中文标题:饮食中癸酸摄入量与糖尿病或糖尿病前期的关系:2005-2016 年 …...
用大模型学大模型04-模型与网络
目前已经学完深度学习的数学基础,开始学习各种 模型和网络阶段,给出一个从简单到入门的,层层递进的学习路线。并给出学习每种模型需要的前置知识。增加注意力机制,bert, 大模型,gpt, transformer, MOE等流行…...
PostgreSQL 数据库压力测试指南
一、为什么需要压力测试? 数据库需要进行压力测试的原因主要包括以下几个方面: 性能评估:通过压力测试,可以了解数据库在高负载情况下的性能表现,包括响应时间、吞吐量和资源利用率等。这有助于确定系统的性能瓶颈。 …...
oppo,康冠科技25届春招内推
oppo,康冠科技25届春招内推 ①康冠科技25届春招 【职位】算法、软件、硬件、技术,结构设计,供应链,产品,职能,商务 【一键内推】https://sourl.cn/2Mm9Lk 【内推码】EVBM88 ②OPPO 2025届春招内推 招聘岗位…...
元学习之孪生网络Siamese Network
简介:元学习是一种思想,一般以神经网络作为特征嵌入的工具,实现对数据特征的提取,然后通过构造某种指标以引导优化器对模型参数进行优化。而最小化距离是最常见的学习目标,这就是熟知的度量学习,度量学习里…...
Python----PyQt开发(PyQt高级:组件大小,界面位置,按钮,文本显示,文本输入,字体大小)
一、大小 setMinimumSize(width, height) 描述: 设置控件的最小尺寸。控件不会被缩小到比这个尺寸更小的大小。 参数: width: 最小宽度(以像素为单位)。 height: 最小高度(以像素为单位)。 button.setMinimumSize(100, …...
qt + opengl 给立方体增加阴影
在前几篇文章里面学会了通过opengl实现一个立方体,那么这篇我们来学习光照。 风氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。下面这张图展示了这些光照分量看起来的样子: 1 环境光照(Ambient …...
vue2老版本 npm install 安装失败_安装卡主
vue2老版本 npm install 安装失败_安装卡主 特别说明:vue2老版本安装慢、运行慢,建议升级vue3element plus vite 解决方案1: 第一步、修改npm 镜像为国内镜像 使用淘宝镜像: npm config set registry https://registry.npmmir…...
20250213编译飞凌的OK3588-C_Linux5.10.209+Qt5.15.10_用户资料_R1
20250213编译飞凌的OK3588-C_Linux5.10.209Qt5.15.10_用户资料_R1 2025/2/13 11:43 缘起:飞凌发布了高版本内核的适配OK3588-C的Buildroot的SDK:OK3588-C_Linux5.10.209Qt5.15.10_用户资料_R1。 但是编译异常了。 于是按照百度升级libc6,可以…...
中望CAD c#二次开发 ——VS环境配置
新建类库项目:下一步 下一步 下一步: 或直接: 改为: <Project Sdk"Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>NET48</TargetFramework> <LangVersion>pr…...
【Elasticsearch】词干提取(Stemming)
词干提取是将一个词还原为其词根形式的过程。这确保了在搜索过程中,一个词的不同变体能够匹配到彼此。 例如,walking(行走)和walked(走过)可以被还原到同一个词根walk(走)。一旦被还…...
Rander压力测试监测,更改服务端资源node
测试策略 压力测试, 目前是本地VM的资源不够,导致压力瓶颈,目前本地的VM,CPU是6个,可以增加到8个,服务端目前资源利用率没有达到最高点 we are now using 3 nodes with 3 pods, therefore, we need the …...
Go语言实现十大排序算法超细节图片讲解
基础排序 冒泡排序 将序列中的元素进行两两比较,将大的元素移动到序列的末尾。 平均时间复杂度是O(n^2),最坏时间复杂度是O(n^2),最好时间复杂度是O(n),排序结果具有稳定性,空间复杂度是O(1)。 这里所说的稳定性是针对…...
linux-centos等测试环境做压力测试
一, jmeter环境安装 1, jdk下载安装 因为jmeter使用Java写的测试工具,应用需要jdk环境支持. yum -y install java-1.8.0-openjdk* 注意:【jdk和*通配符之间不能敲空格,有空格不能匹配所有的子文件】【安装过程中需要等待它执行完成,不要中…...
Django中实现可靠的定时任务调度系统
在许多Web应用程序中,定时任务是一个常见的需求。无论是数据同步、报告生成还是定期维护,都需要一个可靠的调度系统。本文将介绍如何在Django项目中实现一个强大的定时任务调度系统,该系统不仅能够按时执行任务,还能处理分布式环境中的并发问题。 1. 概述 我们将使用以下…...
【鸿蒙Next】写入沙箱的日志文件如何查看
demo案例:https://gitee.com/pengyoucongcode/TxtEdit 文章参考:https://blog.csdn.net/qq_42896653/article/details/144782468...
网页五子棋——通用模块
目录 项目创建 通用功能模块 错误码 自定义异常类 CommonResult jackson 加密工具 项目创建 使用 idea 创建 SpringBoot 项目,并引入相关依赖: 配置 MyBatis: 编辑 application.yml: spring:datasource: # 数据库连接配…...
第6章 6.2使用ASP.NET Core 开发WebAPI ASP.NET Core Web API
6.2.1 Web API项目的搭建 进入VS,【创建新项目】,选择【ASP.NET Core Web API】模板,【下一步】,编辑项目名称及项目位置,【下一步】,选择框架,其他选项默认即可,【创建】。 进入项…...
[MFC] 使用控件
介绍如何使用控件,以及如何获取控件中的数值 check Box 添加点击事件,即选中和取消选中触发的事件 第一种方式是按照如下方式第二种方式是直接双击点击进去 void CMFCApplication1Dlg::OnBnClickedCheckSun() {// TODO: 在此添加控件通知处理程序代…...
景联文科技:以精准标注赋能AI未来,打造高质量数据基石
在人工智能蓬勃发展的时代,数据已成为驱动技术革新的核心燃料,而高质量的数据标注则是让AI模型从“感知”走向“认知”的关键桥梁。作为深耕数据服务领域的创新者,景联文科技始终以“精准、高效、安全”为核心理念,为全球AI企业提…...
2月14(信息差)
🌍杭州:全球数贸港核心区建设方案拟出台 争取国家支持杭州在网络游戏管理给予更多权限 🎄Kimi深夜炸场:满血版多模态o1级推理模型!OpenAI外全球首次!Jim Fan:同天两款国产o1绝对不是巧合&#x…...
UE_C++ —— Metadata Specifiers
声明UClasses、UFunctions、UProperties、UEnums和UInterfaces时使用的元数据关键词,表示其与引擎和关卡编辑器诸多方面的互动方式; 当声明classe、interfaces、structs、enums、enum values、functions、or propertie时,可添加 Metadata Spe…...
