Free Your Hands: 6 SpringBoot Techniques for Automatic Common Field Filling
The article analyzes the repetitive maintenance of common fields like createTime and updateUser in SpringBoot CRUD services, presents six production‑tested strategies—including MyBatis‑Plus meta‑object handling, AOP with custom annotations, multi‑data‑source adaptation, distributed ID generation, caching, batch optimization, and audit logging—detailing implementations, pitfalls, and best‑practice recommendations that can cut code by 90% and boost development efficiency.
When building CRUD services with SpringBoot, every entity often repeats fields such as createTime, updateTime, createUser, and updateUser. Manually setting these fields in each service method is inefficient, error‑prone, and hard to maintain.
Pain Points
High code duplication – every service method must set the same fields.
High maintenance cost – changing a field requires modifications in many places.
Easy to miss updates, especially for the update operation.
Basic Solution: MyBatis‑Plus Automatic Filling
Define a MetaObjectHandler implementation that fills fields on insert and update:
@Slf4j
@Component
public class AutoFillHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "createUser", String.class, getCurrentUser());
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
this.strictUpdateFill(metaObject, "updateUser", String.class, getCurrentUser());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
this.strictUpdateFill(metaObject, "updateUser", String.class, getCurrentUser());
}
private String getCurrentUser() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.map(Authentication::getName)
.orElse("system");
}
}Annotate the base entity class:
@Data
public class BaseEntity {
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private String createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private String updateUser;
}
public class Order extends BaseEntity {
// business fields ...
}Advanced Scheme: AOP Unified Handling
Create a custom annotation and enum to indicate the operation type:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AutoFill {
OperationType value();
}
public enum OperationType {
INSERT,
UPDATE
}Implement an aspect that intercepts methods annotated with @AutoFill and fills the fields:
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
@Autowired
private ObjectMapper objectMapper;
@Around("@annotation(autoFill)")
public Object around(ProceedingJoinPoint pjp, AutoFill autoFill) throws Throwable {
Object[] args = pjp.getArgs();
for (Object arg : args) {
if (arg instanceof BaseEntity) {
fillFields((BaseEntity) arg, autoFill.value());
}
}
return pjp.proceed(args);
}
private void fillFields(BaseEntity entity, OperationType type) {
String currentUser = getCurrentUser();
LocalDateTime now = LocalDateTime.now();
if (type == OperationType.INSERT) {
entity.setCreateTime(now);
entity.setCreateUser(currentUser);
}
entity.setUpdateTime(now);
entity.setUpdateUser(currentUser);
}
private String getCurrentUser() {
return Optional.ofNullable(RequestContextHolder.getRequestAttributes())
.map(attrs -> (ServletRequestAttributes) attrs)
.map(ServletRequestAttributes::getRequest)
.map(req -> req.getHeader("X-User-Id"))
.orElse("system");
}
}Production‑Level Best Practices
Multi‑data‑source adaptation:
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
}
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MultiDataSourceAutoFillHandler();
}
}
public class MultiDataSourceAutoFillHandler extends MetaObjectHandler {
// Dynamically handle based on the current data source
}Distributed ID generation (Snowflake) integrated into the auto‑fill handler:
public class SnowflakeIdGenerator {
// implementation omitted for brevity
}
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "id", String.class, idGenerator.nextId());
// other fields as before
}Common Pitfalls and Guardrails
Null‑pointer protection when retrieving the current user:
private String safeGetUser() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.map(Authentication::getPrincipal)
.map(principal -> {
if (principal instanceof UserDetails) {
return ((UserDetails) principal).getUsername();
}
return principal.toString();
})
.orElse("system");
}Prevent field overwrite on update by configuring @TableField strategy:
@TableField(fill = FieldFill.INSERT, updateStrategy = FieldStrategy.NEVER)
private String createUser;Performance Optimizations
Cache the current user in a ThreadLocal to avoid repeated header reads:
public class UserContextHolder {
private static final ThreadLocal<String> userHolder = new ThreadLocal<>();
public static void setUser(String user) { userHolder.set(user); }
public static String getUser() { return userHolder.get(); }
public static void clear() { userHolder.remove(); }
}
public class UserInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
UserContextHolder.setUser(request.getHeader("X-User-Id"));
return true;
}
}Batch insert optimization by pre‑computing common fields:
@Transactional
public void batchInsert(List<Order> orders) {
String user = getCurrentUser();
LocalDateTime now = LocalDateTime.now();
orders.forEach(order -> {
order.setCreateTime(now);
order.setCreateUser(user);
order.setUpdateTime(now);
order.setUpdateUser(user);
});
orderMapper.batchInsert(orders);
}Monitoring and Auditing
JPA auditing annotations on the base entity:
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@CreatedBy
private String createUser;
@LastModifiedBy
private String updateUser;
@CreatedDate
private LocalDateTime createTime;
@LastModifiedDate
private LocalDateTime updateTime;
}Operation log aspect that records who performed which operation:
@Aspect
@Component
public class OperationLogAspect {
@AfterReturning("@annotation(autoFill)")
public void logOperation(AutoFill autoFill) {
LogEntry log = new LogEntry();
log.setOperator(getCurrentUser());
log.setOperationType(autoFill.value().name());
logService.save(log);
}
}Conclusion and Future Outlook
By combining the six strategies in production, the team achieved a 90% reduction in code related to common field maintenance, a 75% drop in related bugs, and a 40% increase in new‑feature development speed. Looking ahead, the evolution of Spring Data JPA and its integration with reactive programming may enable fully non‑blocking automatic field filling across the entire request chain.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Programmer XiaoFu
xiaofucode.com – a programmer learning guide driven by the pursuit of profit
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
