在本篇文章中,我们将深入了解如何编写一个 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();//方法参数
@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 操作(
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设计哲学