Mybatis Interceptor 参数 Invocation 详解:公共字段填充设计思路与阿里规约
Mybatis 是 Java 开发中非常流行的持久层框架,它的拦截器(Interceptor)机制特别强大,可以让我们在 SQL 执行的各个环节“插手”。今天我们要深入聊聊拦截器里的核心参数 Invocation,通过它实现“公共字段填充”(比如自动填入创建时间、更新时间),并且结合阿里开发规约看看怎么写出更规范的代码。
为了让内容通俗易懂,我会从基础开始,逐步展开上下文,尤其是详细解析 Invocation 里有哪些东西,object 是什么,以及如何处理不同类型的参数(比如实体对象和 Map)。准备好了吗?咱们一步步来!
一、Mybatis Interceptor 的基础知识
1. 拦截器是什么?
想象一下,Mybatis 是个火车站,SQL 是火车,拦截器就是安检员。火车要出发(SQL 执行)时,安检员可以检查行李(参数)、改车票(SQL 语句),甚至拦下火车(阻止执行)。Mybatis 拦截器就是干这个活儿的,它可以在以下四个地方“插手”:
- Executor:执行器,负责执行 SQL(比如增删改查)。
- ParameterHandler:参数处理器,设置 SQL 的参数。
- ResultSetHandler:结果集处理器,把查询结果转成 Java 对象。
- StatementHandler:语句处理器,生成和执行 SQL。
拦截器的核心接口是 org.apache.ibatis.plugin.Interceptor,有三个方法:
intercept(Invocation invocation):拦截逻辑写在这儿。plugin(Object target):决定拦截哪些对象。setProperties(Properties properties):接收外部配置。
今天的主角是 intercept 方法里的 Invocation 参数,它就像安检员的“工具箱”,我们得搞清楚里面装了啥,才能干活儿。
二、Invocation 是什么?里面有哪些东西?
Invocation 是 Mybatis 拦截器里的一个关键对象,定义在 org.apache.ibatis.plugin.Invocation 类中。它就像一个“遥控器”,让我们能操控被拦截的方法。它的结构很简单,但功能强大,包含三个核心字段:
1. Invocation 的三个字段
target:被拦截的对象。比如我们拦截Executor,那target就是Executor的实例。method:被拦截的方法。比如Executor的update方法或query方法。args:方法的参数数组。每个参数是个Object,具体是什么取决于拦截的方法。
可以用代码看看:
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object getTarget() { return target; }
public Method getMethod() { return method; }
public Object[] getArgs() { return args; }
public Object proceed() throws InvocationTargetException, IllegalAccessException {
return method.invoke(target, args); // 执行原始方法
}
}
2. Invocation 的作用
- 获取上下文:通过
target、method和args,你知道“谁在干啥,用啥干”。 - 修改参数:可以直接操作
args里的内容。 - 控制执行:调用
proceed()放行,不调用就拦截。
比如我们拦截 Executor.update,Invocation 就包含:
target:某个Executor实现类(比如SimpleExecutor)。method:update方法。args:update方法的参数。
3. Invocation 的 args 里有什么?
args 是个数组,里面装的是被拦截方法的参数。不同的方法,参数不同。我们今天关注 Executor.update,它的签名是:
int update(MappedStatement ms, Object parameter) throws SQLException;
args[0]:MappedStatement,描述 SQL 的元信息(比如 SQL 类型、语句内容)。args[1]:Object parameter,这就是 SQL 的入参对象,也就是我们常说的object。
三、object 是什么东西?
object 是我们从 invocation.getArgs()[1] 拿到的东西,也就是 Executor.update 的第二个参数 parameter。它具体是什么,取决于你调用 Mapper 接口时传了啥。让我详细拆解一下。
1. object 的来源
假设你有这样的 Mapper 接口:
@Mapper
public interface UserMapper {
@Insert("INSERT INTO user (name) VALUES (#{name})")
int insertUser(User user);
@Update("UPDATE user SET name = #{name} WHERE id = #{id}")
int updateUser(@Param("id") Long id, @Param("name") String name);
}
- 调用
insertUser(user)时,Executor.update被触发,args[1]是User对象。 - 调用
updateUser(1L, "张三")时,args[1]是一个Map。
2. object 的可能类型
情况 1:单个实体对象(Object)
如果 Mapper 方法接收一个参数,比如:
User user = new User();
user.setName("张三");
userMapper.insertUser(user);
这时 object 就是 User 实例,里面存的是:
User{name='张三', createTime=null, updateTime=null}
情况 2:多参数包装成 Map
如果 Mapper 方法有多个参数:
userMapper.updateUser(1L, "张三");
Mybatis 会把参数包装成一个 Map,object 是个 Map<String, Object>,内容可能是:
{
"id": 1L,
"name": "张三"
}
键名来自 @Param 注解,如果没写注解,就是 param1、param2 这样默认命名。
情况 3:无参数
如果方法没参数(比如固定的 SQL),object 是 null。
3. 验证 object 的内容
可以在拦截器里打印:
Object parameter = invocation.getArgs()[1];
System.out.println("parameter: " + parameter);
- 单参数输出:
parameter: User{name='张三', ...}。 - 多参数输出:
parameter: {id=1, name=张三}。
四、公共字段填充的设计思路
1. 为什么要填充公共字段?
很多表都有“创建时间(create_time)”、“更新时间(update_time)”这样的字段,每次手动填太麻烦。我们可以用拦截器自动填充。
2. 用 Invocation 怎么干?
我们拦截 Executor.update(对应 INSERT 和 UPDATE),用 Invocation 拿到 object,然后塞入公共字段。步骤是:
- 判断是 INSERT 还是 UPDATE。
- 检查
object是实体对象还是 Map。 - 填充字段(时间、用户等)。
- 放行执行。
五、代码实现
假设实体类是:
public class User {
private Long id;
private String name;
private Date createTime;
private Date updateTime;
private String createBy;
private String updateBy;
// getter 和 setter 省略
}
拦截器代码:
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import java.util.Date;
import java.util.Map;
import java.util.Properties;
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class AutoFillInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 拿到 Invocation 的内容
Object[] args = invocation.getArgs();
MappedStatement mappedStatement = (MappedStatement) args[0]; // SQL 元信息
Object parameter = args[1]; // 入参对象(object)
// 2. 判断是插入还是更新
String sqlCommandType = mappedStatement.getSqlCommandType().toString();
if ("INSERT".equals(sqlCommandType) || "UPDATE".equals(sqlCommandType)) {
String currentUser = "admin"; // 模拟当前用户
// 3. 处理不同类型的 parameter
if (parameter != null) {
if (parameter instanceof Map) {
// Map 类型参数
Map<String, Object> paramMap = (Map<String, Object>) parameter;
if ("INSERT".equals(sqlCommandType)) {
paramMap.putIfAbsent("createTime", new Date());
paramMap.putIfAbsent("updateTime", new Date());
paramMap.putIfAbsent("createBy", currentUser);
paramMap.putIfAbsent("updateBy", currentUser);
} else if ("UPDATE".equals(sqlCommandType)) {
paramMap.putIfAbsent("updateTime", new Date());
paramMap.putIfAbsent("updateBy", currentUser);
}
} else {
// 实体对象类型(比如 User)
MetaObject metaObject = SystemMetaObject.forObject(parameter);
if ("INSERT".equals(sqlCommandType)) {
setField(metaObject, "createTime", new Date());
setField(metaObject, "updateTime", new Date());
setField(metaObject, "createBy", currentUser);
setField(metaObject, "updateBy", currentUser);
} else if ("UPDATE".equals(sqlCommandType)) {
setField(metaObject, "updateTime", new Date());
setField(metaObject, "updateBy", currentUser);
}
}
}
}
// 4. 放行
return invocation.proceed();
}
// 设置字段值(避免覆盖已有值)
private void setField(MetaObject metaObject, String fieldName, Object value) {
try {
if (metaObject.getValue(fieldName) == null) {
metaObject.setValue(fieldName, value);
}
} catch (Exception e) {
// 字段不存在就忽略
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// 可接收外部配置
}
}
配置
在 Mybatis 配置中加入:
<plugins>
<plugin interceptor="com.example.AutoFillInterceptor"/>
</plugins>
代码说明
Invocation的使用:通过getArgs()拿到MappedStatement和parameter。- 区分类型:
- 如果
parameter是Map,用putIfAbsent添加字段。 - 如果是实体对象,用
MetaObject设置字段(比反射更高效)。
- 如果
- 填充逻辑:插入时填所有字段,更新时只填更新字段,且只在字段为空时填充。
六、结合阿里规约优化
1. 规约:表必须有 id、create_time、update_time
- 实现:拦截器已支持自动填充。
- 优化:可以用注解标记字段,避免硬编码。
2. 规约:避免过多反射
- 优化:用
MetaObject替代反射,提高性能。
3. 规约:业务逻辑放应用层
- 实现:填充逻辑在拦截器,符合要求。
七、总结
通过这次分析,我们搞清楚了:
Invocation是拦截器的“遥控器”,包含target(谁)、method(干啥)、args(用啥)。object是args[1],就是 SQL 的入参对象,可能是实体对象或 Map。- 公共字段填充 通过区分类型处理
object,实现自动化。
加上阿里规约的约束,代码更规范、高效。希望这篇博客让你对 Invocation 和 Mybatis 拦截器有了更深的理解!有问题欢迎讨论。