1. 为什么我们需要自动填充公共字段?
在SpringBoot项目中,几乎每个实体类都会有一些公共字段,比如创建时间(create_time)、更新时间(update_time)、创建人(create_by)、更新人(update_by)等。这些字段在每个业务表中几乎都会出现,如果每次都手动设置这些值,不仅代码冗余,而且容易遗漏。
我曾经接手过一个老项目,发现开发者在每个Service方法中都手动设置这些字段,不仅代码重复率高,而且有些地方忘记设置update_time,导致数据不一致。更糟糕的是,当需要修改字段填充逻辑时(比如从获取当前用户ID改为获取部门ID),需要在几十个地方修改,维护成本极高。
2. MyBatis-Plus的自动填充方案
2.1 基于注解的字段填充
MyBatis-Plus提供了@TableField注解的fill属性,可以非常方便地实现自动填充:
java复制public class BaseEntity {
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
}
然后需要实现MetaObjectHandler接口:
java复制@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "createBy", Long.class, getCurrentUserId());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
this.strictUpdateFill(metaObject, "updateBy", Long.class, getCurrentUserId());
}
private Long getCurrentUserId() {
// 从安全上下文获取当前用户ID
// 实际项目中可能从JWT或Session中获取
return 1L; // 示例值
}
}
注意:strictInsertFill和strictUpdateFill方法在3.3.0及以上版本中推荐使用,它们会检查字段是否存在以及类型是否匹配,避免运行时错误。
2.2 自定义SQL注入器
对于更复杂的填充逻辑,比如需要根据不同的表采用不同的填充策略,可以使用MyBatis-Plus的SQL注入器:
java复制public class MySqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.DEFAULT));
return methodList;
}
}
然后在配置类中注册:
java复制@Configuration
public class MybatisPlusConfig {
@Bean
public MySqlInjector mySqlInjector() {
return new MySqlInjector();
}
}
3. 基于AOP的自动填充方案
3.1 Controller层AOP填充
对于不经过MyBatis-Plus的操作,或者需要在前端传入对象时就填充字段的情况,可以使用AOP:
java复制@Aspect
@Component
public class AutoFillAspect {
@Before("execution(* com..controller.*.*(..)) && args(entity,..)")
public void beforeControllerMethod(JoinPoint joinPoint, Object entity) {
if (entity instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) entity;
if (baseEntity.getId() == null) {
baseEntity.setCreateTime(LocalDateTime.now());
baseEntity.setCreateBy(getCurrentUserId());
}
baseEntity.setUpdateTime(LocalDateTime.now());
baseEntity.setUpdateBy(getCurrentUserId());
}
}
private Long getCurrentUserId() {
// 获取当前用户ID的逻辑
return 1L;
}
}
3.2 Service层AOP填充
更常见的做法是在Service层进行填充,这样可以覆盖更多场景:
java复制@Aspect
@Component
public class ServiceAutoFillAspect {
@Around("execution(* com..service..*.save*(..)) || execution(* com..service..*.update*(..))")
public Object aroundServiceMethod(ProceedingJoinPoint joinPoint) throws Throwable {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg instanceof BaseEntity) {
fillBaseEntity((BaseEntity) arg,
joinPoint.getSignature().getName().startsWith("save"));
}
}
return joinPoint.proceed(args);
}
private void fillBaseEntity(BaseEntity entity, boolean isCreate) {
if (isCreate) {
entity.setCreateTime(LocalDateTime.now());
entity.setCreateBy(getCurrentUserId());
}
entity.setUpdateTime(LocalDateTime.now());
entity.setUpdateBy(getCurrentUserId());
}
}
4. 基于JWT的自动用户信息填充
在前后端分离项目中,用户信息通常通过JWT传递。我们可以结合JWT解析来自动填充用户相关字段:
java复制@Component
public class JwtAutoFillHandler {
@Autowired
private JwtParser jwtParser;
public void fillUserInfo(BaseEntity entity, HttpServletRequest request) {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
Claims claims = jwtParser.parseClaimsJws(token.substring(7)).getBody();
Long userId = claims.get("userId", Long.class);
if (entity.getId() == null) {
entity.setCreateBy(userId);
}
entity.setUpdateBy(userId);
}
}
}
然后在Controller中注入使用:
java复制@RestController
@RequestMapping("/api")
public class MyController {
@Autowired
private JwtAutoFillHandler jwtAutoFillHandler;
@PostMapping("/save")
public Result save(@RequestBody MyEntity entity, HttpServletRequest request) {
jwtAutoFillHandler.fillUserInfo(entity, request);
// 其他业务逻辑
}
}
5. 基于Hibernate事件的自动填充
如果你使用Hibernate作为JPA实现,可以利用其事件监听机制:
java复制@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public abstract class BaseEntity {
@CreatedDate
private LocalDateTime createTime;
@LastModifiedDate
private LocalDateTime updateTime;
@CreatedBy
private Long createBy;
@LastModifiedBy
private Long updateBy;
}
然后在配置类中启用审计功能:
java复制@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorAware")
public class JpaConfig {
@Bean
public AuditorAware<Long> auditorAware() {
return () -> Optional.of(getCurrentUserId());
}
private Long getCurrentUserId() {
// 获取当前用户ID的逻辑
return 1L;
}
}
6. 基于自定义注解的灵活填充方案
对于需要更灵活控制的场景,可以创建自定义注解:
java复制@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface AutoFill {
FillType value() default FillType.CURRENT_TIME;
enum FillType {
CURRENT_TIME,
CURRENT_USER_ID,
CURRENT_USERNAME
}
}
然后通过反射工具类实现填充:
java复制public class AutoFillUtils {
public static void fillFields(Object target) {
Field[] fields = target.getClass().getDeclaredFields();
for (Field field : fields) {
AutoFill autoFill = field.getAnnotation(AutoFill.class);
if (autoFill != null) {
try {
field.setAccessible(true);
switch (autoFill.value()) {
case CURRENT_TIME:
field.set(target, LocalDateTime.now());
break;
case CURRENT_USER_ID:
field.set(target, getCurrentUserId());
break;
case CURRENT_USERNAME:
field.set(target, getCurrentUsername());
break;
}
} catch (IllegalAccessException e) {
// 处理异常
}
}
}
}
}
7. 实战中的经验与坑点
7.1 多数据源下的自动填充
在多数据源项目中,MyBatis-Plus的MetaObjectHandler需要在每个数据源配置中单独指定:
java复制@Configuration
@MapperScan(basePackages = "com.primary.mapper", sqlSessionFactoryRef = "primarySqlSessionFactory")
public class PrimaryDataSourceConfig {
@Bean
public MyMetaObjectHandler primaryMetaObjectHandler() {
return new MyMetaObjectHandler();
}
@Bean
public GlobalConfig globalConfig() {
GlobalConfig globalConfig = new GlobalConfig();
globalConfig.setMetaObjectHandler(primaryMetaObjectHandler());
return globalConfig;
}
}
7.2 批量操作时的性能优化
对于批量插入/更新操作,避免在循环中触发自动填充逻辑,应该使用MyBatis-Plus的批量操作方法:
java复制@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Transactional
public void batchInsert(List<User> users) {
// 手动填充公共字段
Long currentUserId = getCurrentUserId();
LocalDateTime now = LocalDateTime.now();
users.forEach(user -> {
user.setCreateTime(now);
user.setCreateBy(currentUserId);
user.setUpdateTime(now);
user.setUpdateBy(currentUserId);
});
// 使用批量插入
userMapper.insertBatchSomeColumn(users);
}
}
7.3 测试环境下的处理
在单元测试中,自动填充可能会干扰测试逻辑。可以通过以下方式处理:
java复制@SpringBootTest
@TestExecutionListeners(listeners = {
DependencyInjectionTestExecutionListener.class,
TransactionalTestExecutionListener.class
}, mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS)
@AutoConfigureMockMvc
public class UserControllerTest {
@MockBean
private MyMetaObjectHandler metaObjectHandler; // 模拟自动填充处理器
@Test
public void testCreateUser() throws Exception {
// 测试逻辑
}
}
7.4 字段填充的优先级问题
当多种填充方式同时存在时(如AOP和MyBatis-Plus),需要明确优先级。通常建议:
- 注解方式(如@TableField)优先级最低
- AOP方式次之
- 手动设置的值优先级最高
可以通过在填充前检查字段是否已有值来控制:
java复制public void fillBaseEntity(BaseEntity entity, boolean isCreate) {
if (isCreate && entity.getCreateTime() == null) {
entity.setCreateTime(LocalDateTime.now());
}
if (entity.getUpdateTime() == null) {
entity.setUpdateTime(LocalDateTime.now());
}
// 其他字段类似
}
8. 如何选择合适的自动填充方案
根据项目特点选择最合适的方案:
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| MyBatis-Plus注解 | 纯MyBatis-Plus项目 | 简单易用,与MP深度集成 | 仅适用于MP操作 |
| AOP | 需要统一处理Controller或Service层 | 适用范围广,不依赖ORM | 需要定义明确的切点 |
| Hibernate事件 | JPA/Hibernate项目 | 标准JPA方式,与Hibernate深度集成 | 仅适用于JPA |
| JWT解析 | 前后端分离项目 | 直接利用现有认证信息 | 依赖特定的认证方式 |
| 自定义注解 | 需要高度灵活控制 | 可定制性强 | 实现复杂度高 |
在实际项目中,我通常会组合使用多种方式。比如主要使用MyBatis-Plus的自动填充,对于特殊场景再辅以AOP处理。关键是要保持一致性,避免同一个字段在不同地方以不同方式填充,导致维护困难。
