1. 前言
在企业级开发中,维护数据的审计字段(create_time, update_time, creator, updater)是必不可少的环节。MyBatis-Plus (MP) 提供了 MetaObjectHandler 接口来实现这一逻辑的自动化。
然而,很多开发者对这一机制存在误解,认为只要实现了 Handler 接口,填充逻辑就会自动生效。事实上,MP 内部存在一套严格的**"短路"触发机制**。
本文将深入剖析这套机制的运行原理,并对比"强制赋值"与"标准填充"的区别,给出生产环境下的最佳实践代码。
2. 核心运行原理:短路机制
MyBatis-Plus 在执行 SQL 之前,并不会无条件地调用 MetaObjectHandler。为了性能优化,它在启动阶段构建元数据时,就已经决定了哪些表需要填充。
2.1 启动阶段:元数据解析
应用启动时,MP 会解析所有的实体类(Entity),生成对应的 TableInfo 对象。在此过程中,框架会遍历实体类的所有字段,检查是否存在 @TableField(fill = ...) 注解。
- isWithInsertFill : 如果检测到任意一个字段包含
INSERT或INSERT_UPDATE策略,该标记置为true。 - isWithUpdateFill : 如果检测到任意一个字段包含
UPDATE或INSERT_UPDATE策略,该标记置为true。
2.2 运行阶段:条件执行
当执行 BaseMapper.insert() 或 BaseMapper.update() 时,MybatisParameterHandler 会执行如下逻辑(伪代码):
java
// MybatisParameterHandler.java
public void processParameter(Object parameter) {
// ...
// 关键判断:
// 1. 全局配置中是否存在 MetaObjectHandler
// 2. 当前表元数据 (TableInfo) 是否标记了需要填充
if (metaObjectHandler != null && tableInfo.isWithInsertFill()) {
metaObjectHandler.insertFill(metaObject);
}
// ...
}
结论 :如果实体类中没有任何一个字段标注了 @TableField(fill = ...),tableInfo.isWithInsertFill() 为 false,insertFill 方法会被直接跳过,根本不会执行。
3. 规范的写法示例
为了确保代码的可维护性和健壮性,我们应当遵循"配置驱动"的原则,即:实体类声明策略,Handler 执行策略。
3.1 实体类配置(必须)
必须在基类或实体类中明确标记哪些字段需要填充。
java
@Data
public class BaseDO {
/**
* 创建时间
* 策略:仅在插入时填充
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
* 策略:插入和更新时均填充
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 创建人
* 策略:仅在插入时填充
*/
@TableField(fill = FieldFill.INSERT)
private String creator;
/**
* 更新人
* 策略:插入和更新时均填充
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updater;
}
3.2 处理器实现(推荐)
推荐使用 strictInsertFill 和 strictUpdateFill 方法。
优点 :MP 会在内部再次校验字段是否存在、是否标记了对应的 fill 策略以及属性类型是否匹配,比直接 Set 值更安全。
java
@Component
@Slf4j
public class DefaultDBFieldHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 1. 填充创建时间 (仅当字段为空时填充)
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
// 2. 填充更新时间
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
// 3. 填充当前用户信息
Long userId = SecurityFrameworkUtils.getLoginUserId();
if (userId != null) {
String userIdStr = userId.toString();
this.strictInsertFill(metaObject, "creator", String.class, userIdStr);
this.strictInsertFill(metaObject, "updater", String.class, userIdStr);
}
}
@Override
public void updateFill(MetaObject metaObject) {
// 1. 强制更新 updateTime
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
// 2. 更新修改人
Long userId = SecurityFrameworkUtils.getLoginUserId();
if (userId != null) {
this.strictUpdateFill(metaObject, "updater", String.class, userId.toString());
}
}
}
4. 常见误区:关于"强转赋值"
在实际开发中,有些开发者会使用如下的"强转写法":
java
// 不推荐的写法
if (metaObject.getOriginalObject() instanceof BaseDO) {
BaseDO baseDO = (BaseDO) metaObject.getOriginalObject();
baseDO.setCreateTime(LocalDateTime.now()); // 绕过 MP API,直接操作 Java 对象
}
这种写法为什么也能生效?
只要实体类中至少有一个字段 标记了 @TableField(fill = INSERT),MP 就会打开执行 insertFill 的大门。一旦进入该方法,由于上述代码是直接操作 Java 对象(setter),它不经过 MP 的字段元数据检查,因此无论 createTime 字段本身有没有注解,都会被赋值。
为什么不推荐?
- 逻辑不一致 :如果
createTime忘记加注解,但通过强转代码却被填充了,这会导致代码行为与注解声明不符,增加维护难度。 - 耦合度过高 :强依赖
BaseDO类。如果存在不继承BaseDO但也需要填充的表,该逻辑失效。 - 违背设计初衷 :绕过了 MP 的
strict(严格) 检查机制,丧失了框架提供的类型安全和策略校验功能。