ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot3+MyBatis Plus自动填充:从注解到MetaObjectHandler完整实践

SpringBoot3+MyBatis Plus自动填充:从注解到MetaObjectHandler完整实践 SpringBoot3项目里用MyBatis Plus做开发几乎每个表都要带created_time、updated_time这类字段。以前的做法是每次写insert或update语句的时候手动set这些值一张两张表还能忍表一多、字段一多漏set的情况经常出现而且代码冗余得很厉害。MyBatis Plus提供了自动填充机制用注解加上一个处理器就能统一解决这个痛点。这篇文章把自动填充从原理到落地完整梳理一遍包括SpringBoot3环境下的依赖选型、实体类注解怎么写、MetaObjectHandler处理器怎么实现以及我实际开发中踩过的坑和排查经验。适合正在用SpringBoot3 MyBatis Plus做项目想摆脱重复劳动、把基础字段维护统一收口的人。1. 自动填充到底解决了什么问题1.1 没有自动填充时的开发痛点先还原一下没有自动填充的日常工作流。假设你有一张用户表包含username、phone、status以及created_time、updated_time、created_by、updated_by这几个通用字段。每次写新增逻辑代码大概是这样的User user new User(); user.setUsername(张三); user.setPhone(13800138000); user.setStatus(1); user.setCreatedTime(LocalDateTime.now()); user.setUpdatedTime(LocalDateTime.now()); user.setCreatedBy(admin); user.setUpdatedBy(admin); userMapper.insert(user);问题很快就暴露出来了每个新增接口都要重复写4行甚至更多的set代码纯纯的体力活。修改操作同样要set updated_time和updated_by很容易漏掉其中一个。团队多人协作时每个人对哪些字段要写、什么时候写的理解不一致有的set了updated_time忘了updated_by有的直接忘掉update_time数据完整性和一致性全靠自觉。后期如果想统一调整字段填充逻辑比如从LocalDateTime换成长整型时间戳或者操作人从固定字符串换成用户ID所有涉及的地方都要改一遍。这些字段本质上和业务关系不大属于系统层面的公共字段却让每个开发者在每个接口里重复处理。自动填充就是把这些公共字段的赋值逻辑从业务代码中剥离出来统一交给框架处理。1.2 自动填充的核心价值MyBatis Plus自动填充的核心思路是在实体类字段上用注解声明这个字段需要自动填充然后在自定义的处理器里写好填充规则当执行insert或update操作时框架自动调用处理器完成赋值。带来的直接收益有三点业务代码大幅减负。新增、修改接口里不再需要手动set这些公共字段只需要关注业务字段本身。填充逻辑统一收口。所有自动填充字段的赋值规则集中在处理器类里一眼就能看完修改也只需要动一个地方。规范落地有保障。新同学加入项目只要实体类字段标注了填充注解就不太可能漏掉公共字段从机制层面规避低级错误。当然有人可能说数据库的DEFAULT值也能解决一部分问题比如默认当前时间。但DEFAULT只能处理insert场景update场景没法自动更新字段值而且对于操作人这类业务上下文相关的字段数据库默认值根本无能为力。所以自动填充和数据库默认值是互补关系不是替代关系。2. 环境准备SpringBoot3 MyBatis Plus 依赖搭配2.1 SpringBoot3带来的版本变化SpringBoot3相比SpringBoot2有两个核心变化直接影响到依赖选型基础JDK版本升级到Java 17javax命名空间迁移到了jakarta。官方很多第三方库都针对SpringBoot3推出了独立适配版本MyBatis Plus也不例外。所以SpringBoot3项目里不能直接使用传统的mybatis-plus-boot-starter而要引入SpringBoot3专用starterdependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency版本选择上MyBatis Plus 3.5.x系列对SpringBoot3支持得比较完整。3.5.3、3.5.4、3.5.5这几个版本我都是实际用过的3.5.3开始对SpringBoot3的稳定性已经不错新项目建议直接用3.5.5以上的版本。如果你还在用3.4.x的旧版本那不能直接升级到SpringBoot3必须连同MyBatis Plus版本一起升级。这里有个容易踩的坑有些教程或者老项目里还能搜到mybatis-plus-boot-starter配合SpringBoot3的用法这种组合启动了会直接报错提示类找不到或者Bean加载失败。出现这类问题第一步先检查starter坐标是不是用成了老版本。2.2 其他必要依赖除了MyBatis Plus本身项目里通常还需要数据库驱动。这里以MySQL为例dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependencySpringBoot3的spring-boot-starter-parent中已经管理了mysql-connector-j的版本所以不需要手动指定版本号。额外提醒一句如果你的项目引入了mybatis-plus的代码生成器mybatis-plus-generator要注意生成器也有独立的版本要求建议与主starter版本保持一致避免出现方法签名不兼容的问题。2.3 基础配置依赖引入后在application.yml里做基础的MyBatis Plus配置spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/your_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case开启后数据库下划线字段能自动映射到实体类的驼峰属性这和自动填充配合使用基本是标配。log-impl配置成StdOutImpl是为了开发阶段能看到SQL输出方便验证自动填充是否生效生产环境建议去掉。3. 底层原理拆解注解、枚举和处理器3.1 FieldFill枚举与TableField注解自动填充的声明入口是MyBatis Plus的TableField注解中的fill属性它接收一个FieldFill枚举值。枚举一共定义了四种策略枚举值含义使用场景DEFAULT默认值不进行自动填充普通业务字段INSERT插入时填充创建时间、创建人UPDATE更新时填充更新时间、更新人INSERT_UPDATE插入和更新时都填充某些last_updated_time等字段实体类里的用法是这样的Data TableName(sys_user) public class User { TableId(type IdType.ASSIGN_ID) private Long id; private String username; private String phone; private Integer status; TableField(fill FieldFill.INSERT) private LocalDateTime createdAt; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt; TableField(fill FieldFill.INSERT) private String createdBy; TableField(fill FieldFill.INSERT_UPDATE) private String updatedBy; }注意几个细节TableId的IdType.ASSIGN_ID表示主键由MyBatis Plus的雪花算法自动生成不需要数据库自增这和自动填充是两套机制不要混淆。如果数据库表字段用的是created_at这种带下划线的名称实体类属性用createdAt打开map-underscore-to-camel-case后会自动映射注解里的fill只负责填充行为不影响命名映射。字段类型推荐用LocalDateTime替代老的Date,时间处理上更顺手而且MyBatis Plus对LocalDateTime的支持很成熟。3.2 MetaObjectHandler真正干活的接口注解只是声明我要填充真正执行填充逻辑的是MetaObjectHandler接口。这个接口提供了几个方法核心是public interface MetaObjectHandler { void insertFill(MetaObject metaObject); void updateFill(MetaObject metaObject); // 还有一些默认方法如 strictInsertFill、strictUpdateFill }实现这个接口后MyBatis Plus在执行insert或update时会在SQL真正执行前回调对应的方法传入一个包含实体字段信息的MetaObject对象我们在方法里给需要填充的字段set值即可。框架自带方法中最常用的是strictInsertFill和strictUpdateFill这两个名字带strict的方法。strict的含义是严格匹配它会检查实体类中对应字段是否标了fill注解同时还会判断字段当前有没有值如果字段已经有值strict方法默认不会覆盖前提是字段标记了注解且非空判断策略允许。如果字段是null才会执行填充。这个已有值不覆盖的行为很重要它让你在特殊场景下可以手动指定某些字段的值比如后台手工修正一条数据时想自定义创建时间直接在业务代码里set好填充处理器不会覆盖你的值。3.3 填充的执行链路理解自动填充在整个MyBatis Plus执行流程中的位置对排查问题很有帮助。大致链路是这样的调用userMapper.insert(user)MyBatis Plus经过它的SQL注入器生成INSERT语句。在SQL执行前MyBatis Plus会检查实体类字段中有没有fill属性不是DEFAULT的字段。如果有调用自定义MetaObjectHandler的insertFill或updateFill方法。处理器给字段赋值后再拼装成完整的INSERT或UPDATE SQL执行。所以自动填充严格来说是MyBatis Plus层在ORM映射阶段做的手脚它靠的是MyBatis Plus内置的MybatisPlusMetaObjectHandler机制底层依赖MyBatis的MetaObject反射工具。这也是为什么要用metaObject.getValue(...)和metaObject.setValue(...)这组API来读写字段值。4. 完整实操从零实现自动填充4.1 自定义填充处理器核心代码就是实现MetaObjectHandler接口。我的项目里通常写成这样Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createdAt, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updatedAt, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, createdBy, String.class, getCurrentUser()); this.strictInsertFill(metaObject, updatedBy, String.class, getCurrentUser()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updatedAt, LocalDateTime.class, LocalDateTime.now()); this.strictUpdateFill(metaObject, updatedBy, String.class, getCurrentUser()); } private String getCurrentUser() { // 从登录上下文获取操作人信息 // 这里做简化处理实际项目从SecurityContext或ThreadLocal中获取 return system; } }这个处理器里有几个要点类上必须标注Component或者用其他方式注册为Spring Bean否则MyBatis Plus扫描不到。strictInsertFill和strictUpdateFill的参数含义分别是MetaObject、字段名、字段类型、填充值。字段名要和实体类属性名一致不是数据库列名。字段类型要传对。实体类里createdAt是LocalDateTime这里就传LocalDateTime.class传错类型会直接抛异常这是新手最容易翻车的地方。填充值是LocalDateTime.now()每次执行都取当前时间所以同一条记录insert和update的字段值天然就是正确的业务时间。4.2 处理器的代码优化上面是基础写法实际项目里我还会做两点优化。第一把时间字段的填充单独抽方法因为insertFill和updateFill都会用到避免重复private void fillTime(MetaObject metaObject) { LocalDateTime now LocalDateTime.now(); this.strictInsertFill(metaObject, createdAt, LocalDateTime.class, now); this.strictInsertFill(metaObject, updatedAt, LocalDateTime.class, now); }第二操作人字段从上下文中获取。如果你用了Spring Security可以在getCurrentUser里从SecurityContextHolder解析当前登录用户名如果项目用的自己写的登录态一般就是一个ThreadLocal工具类。这块要注意的是处理器是Spring管理的单例Bean不要在里面存放用户相关的状态所有状态都通过方法参数传递否则多线程并发下会串数据。4.3 实战验证插入和更新写一个简单的测试接口来验证填充是否生效RestController RequestMapping(/user) public class UserController { Resource private UserMapper userMapper; PostMapping public User add(RequestBody User user) { userMapper.insert(user); return user; } PutMapping public User update(RequestBody User user) { userMapper.updateById(user); return user; } }调用新增接口时控制台会输出类似这样的SQLINSERT INTO sys_user ( id, username, phone, status, created_at, updated_at, created_by, updated_by ) VALUES ( 1789123456789011234, 张三, 13800138000, 1, 2025-01-15 10:23:45, 2025-01-15 10:23:45, system, system )可以看到created_at、updated_at、created_by、updated_by都被自动填入了值业务代码里完全不需要手动set。更新接口的SQL对应如下UPDATE sys_user SET phone13900139000, updated_at2025-01-15 11:30:00, updated_bysystem WHERE id1789123456789011234这里有个隐藏细节如果更新时实体类里没有传入任何需要更新的业务字段MyBatis Plus生成的UPDATE语句可能只包含自动填充字段这也是正常的说明填充机制独立工作不受业务字段影响。4.4 手动set值的情况刚才提到strict方法默认不覆盖已有值这里实际操作验证一下。假设我想新增一条用户并手动指定创建时间为2020年1月1日User user new User(); user.setUsername(李四); user.setCreatedAt(LocalDateTime.of(2020, 1, 1, 0, 0, 0)); userMapper.insert(user);最终插入数据库的created_at会是2020-01-01 00:00:00而updated_at、created_by、updated_by仍然是自动填充值。这就是strictInsertFill和strictUpdateFill的设计意图优先使用业务代码里已经set好的值字段为null时才走自动填充。注意这里的不覆盖依赖的是strict方法内部对字段值的判断。如果你用的是setFieldValByName方法它会无条件覆盖已有值两者行为差异很大建议统一使用strict方法。5. 功能联动与进阶场景5.1 逻辑删除与自动填充的配合如果你的表逻辑删除了MyBatis Plus的TableLogic注解和自动填充各管各的互不干扰。逻辑删除执行的是UPDATE语句把deleted字段从0改成1它不会触发updateFill对createdAt等字段的填充所以不用担心删除操作会更新时间戳。不过有个值得注意的边界场景某些业务要求删除时记录删除人、删除时间这时候可以考虑在逻辑删除字段旁边再设计deleted_by、deleted_at字段然后通过自定义拦截器或者在业务层更新时手动set而不是依赖自动填充因为MetaObjectHandler只管insert和update的通用场景对逻辑删除这种特殊UPDATE不会单独处理。5.2 多数据源场景下的处理有些项目配置了多数据源比如主库和读库分离。自动填充处理器本身和数据源无关它工作在MyBatis Plus的ORM层不管数据源是哪个insert时都会回调MetaObjectHandler所以多数据源项目不需要额外适配。真正需要留意的是多数据源中的多套Entity如果两个数据源的库表字段命名习惯不同比如一套用created_at一套用create_time实体类属性名不同处理器里的字段名也要分别处理。这种情况我通常给每个数据源单独写MetaObjectHandler或者在一个Handler里根据实体类型判断要填哪些字段。5.3 字段类型和时间策略的扩展默认用LocalDateTime填充没有问题但如果团队规范要求统一存Long型时间戳同样可以支持this.strictInsertFill(metaObject, createdAt, Long.class, System.currentTimeMillis()); this.strictInsertFill(metaObject, updatedAt, Long.class, System.currentTimeMillis());实体类对应字段类型改成Long即可。需要注意的是字段类型变化后MyBatis Plus的自动映射和查询结果类型也要跟着变一般改实体类字段类型就能解决xml里如果手写了ResultMap要同步修改。另外如果项目需要兼容多时区建议填充时使用带时区的时间类型或者统一存储UTC时间展示层再做时区转换。这个就看具体业务了原理上都是改填充值和字段类型处理器逻辑不需要大动。6. 常见问题与排查技巧实录6.1 填充没有生效怎么回事排查自动填充不生效建议按照下面的顺序逐步确认排查点操作方式可能原因依赖版本检查mybatis-plus-spring-boot3-starter版本用了老版本starter导致处理器没被扫描处理器注册确认类上是否有Component注解没有注册为Spring Bean注解配置检查实体类fill属性是否设置fill FieldFill.DEFAULT或没写不会触发字段名匹配核对处理器里字段名与实体类属性名手误写错字段名会静默跳过SQL日志开启log-impl观察实际SQL排除其他拦截器把SQL提前改掉最常见的还是第二个和第三个。尤其是一个项目里如果复制了多个实体类某个实体的字段漏加fill注解那它对应的表自然就不走自动填充。这类问题靠代码review不一定能发现我的习惯是写完后直接看控制台SQL确认INSERT语句里有没有自动填充字段一步到位。6.2 strictInsertFill 抛“找不到属性”异常再一种高频问题是运行时报类似这样的错误strictInsertFill 无法找到属性 createdAt in class com.example.demo.entity.User出现这个异常基本可以断定实体类里根本没有createdAt这个属性或者属性名和处理器里传的字符串不一致。比如实体类写的是gmtCreate处理器里写createdAt虽然实体类字段标了fill注解但strict方法按字段名去找的时候找不到自然就抛异常。解决方式就是统一命名或者改用strictInsertFill(metaObject, gmtCreate, ...)。这块没有太多技巧可言就是细心比对。如果实体类很多我倾向于抽一个公共父类Data public class BaseEntity { TableField(fill FieldFill.INSERT) private LocalDateTime createdAt; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updatedAt; TableField(fill FieldFill.INSERT) private String createdBy; TableField(fill FieldFill.INSERT_UPDATE) private String updatedBy; }所有实体类继承BaseEntity字段名天然统一处理器里不用为每个实体单独适配省心很多。这是一个非常实用的小优化。6.3 新老版本行为差异MyBatis Plus 3.4.x和3.5.x在自动填充这块行为基本一致但SpringBoot3下如果用了3.5.x需要注意它内部对MetaObject处理的细节有微调。比如某些情况下旧版本用getFieldValByName来检查字段值新版本建议用strict系列方法因为strict系列对实体字段的注解状态做了额外校验行为更可控。如果你的处理器里自定义逻辑比较多升级MyBatis Plus大版本后一定要回归测试插入和更新两个场景特别是在更新时实体里部分字段为null的场景。严格来说updateFill只对显式传入的实体生效如果你用UpdateWrapper的方式更新比如LambdaUpdateWrapperUser wrapper new LambdaUpdateWrapper(); wrapper.eq(User::getId, 1L).set(User::getPhone, 13900000000); userMapper.update(null, wrapper);这种情况下自动填充不会生效因为update时传入的第一个参数是nullMyBatis Plus没有实体对象可供MetaObjectHandler操作。这是很多人在实际项目中踩过的坑用Wrapper方式更新发现updated_at没变以为自动填充坏了。解决方案有两个一是尽量用实体对象updateById的方式更新二是确认必须用Wrapper时在Wrapper上手动set时间字段。我在项目中通常是业务优先用updateById复杂条件才用Wrapper并且约定好Wrapper更新的场景必须手动维护时间字段。6.4 高并发下的时间一致性最后一个经验点关于多线程场景。LocalDateTime.now()每次调用都会获取当前系统时间高并发下同一毫秒内多条记录的时间可能会有细微差别这个对大多数业务没影响。但如果你的系统对时间一致性有要求比如要求同一批量操作内的记录时间戳完全一致可以在批量更新时统一获取一次时间然后业务代码里手动set不走自动填充。反过来讲用自动填充反而是最省心的做法它不涉及分布式锁、不涉及事务边界就是简单地在SQL执行前把当前时间塞进去。相比某些方案里在数据库端用CURRENT_TIMESTAMPMyBatis Plus的方式还多了一个好处——同一个事务里应用侧拿到的填充值和数据库最终存储值一致可以在业务代码里直接使用实体对象的createdAt字段不用再查一次库。根据我自己的使用体验自动填充配合公共父类BaseEntity是我在SpringBoot3项目里最满意的编码习惯之一。它把最无聊的时间戳和操作人赋值从业务代码里彻底抹掉了代码整洁度提升明显也少了很多因为漏set字段引起的线上数据问题。如果你还没有在项目里用起来建议从下一个新模块开始尝试把createdAt、updatedAt、createdBy、updatedBy这些基础字段交给处理器统一管理让业务的每行代码都只关心业务本身你会发现省下来的时间比想象中多得多。
返回列表