MyBatis-Plus结合SpringSecurity自动填充需要写入数据库的字段
场景
数据库中每张表都有create_by、create_time、update_by、update_time这四个字段,希望在插入、更新数据库操作时,能自动填充这些字段。
SpringSecurity上下文中存有用户信息
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Resource
private RedisTemplate<Object, Object> redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 获取请求头中的token
String authorization = request.getHeader("Authorization"); // 从请求头中获取Authorization字段
if (authorization != null && authorization.startsWith("Bearer ")) {
String token = authorization.substring(7); // 截取出token
String userId = null;
try {
// 解析获取userid
userId = JwtUtils.resolveToken(token);
} catch (JWTDecodeException | TokenExpiredException jwtException) {
log.error("解析token失败:{}", jwtException.getMessage(), jwtException);
response.getWriter()
.write(
ResponseResult.failed(HttpStatus.UNAUTHORIZED.value(),
HttpStatus.UNAUTHORIZED.getReasonPhrase()).toJson()
);
return;
}
// 从redis中获取用户信息
LoginUser loginUser = (LoginUser) redisTemplate.opsForValue().get("login:user:id:" + userId);
// 获取不到用户信息
if (loginUser == null) {
log.error("redis中没有用户信息");
response.getWriter()
.write(
ResponseResult.failed(HttpStatus.UNAUTHORIZED.value(),
HttpStatus.UNAUTHORIZED.getReasonPhrase()).toJson()
);
return;
}
// 设置认证信息到SecurityContextHolder中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(
loginUser, null, loginUser.getAuthorities());
// 设置当前http请求相关的详细信息到SecurityContextHolder中
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
// 放行请求到下一个过滤器
filterChain.doFilter(request, response);
}
}
实现MetaObjectHandler
接口
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 插入数据时,自动填充 create_by、create_time、update_by、update_time 字段
Long userId = getUserId();
LocalDateTime dateTime = LocalDateTime.now();
this.strictInsertFill(metaObject, "createBy", Long.class, userId);
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, dateTime);
this.strictInsertFill(metaObject, "updateBy", Long.class, userId);
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, dateTime);
}
/**
* 从spring security的上下文中 获取当前登录用户的ID
* @return 当前登录用户的ID
*/
private static Long getUserId() {
LoginUser loginUser = (LoginUser) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return loginUser.getUser().getId();
}
@Override
public void updateFill(MetaObject metaObject) {
// 更新数据时,自动填充 update_by、update_time 字段
Long userId = getUserId();
LocalDateTime dateTime = LocalDateTime.now();
this.strictUpdateFill(metaObject, "updateBy", Long.class, userId);
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, dateTime);
}
}
实体类添加自动填充的注解
@TableField(fill = FieldFill.INSERT)
private Long createBy;
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateBy;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
注意:@TableField
的注解和MyMetaObjectHandler
是相辅相成的。MyMetaObjectHandler
作用于有@TableField
的字段,这不意味着没有MyMetaObjectHandler
,@TableField
就不会生效,@TableField
本身有着最简单的自动填充策略,MyMetaObjectHandler
是对其增强的自定义处理。
特别场景下,自动填充存在问题
文章表中有浏览量字段view_count
,通过定时任务写表进行更新。
在MyBatis-Plus的自动填充机制下,更新浏览量的操作也会变更update_by
和update_time
字段,而这两个字段应该单纯地作为文章本身的更新数据,不应该被更新浏览量的定时任务干扰。
治标方案
针对定时任务更新浏览量的任务,进行单独的逻辑处理。
提供是否为更新浏览量操作的标识
isUpdateViewCount
/** * 更新浏览量时的配置类。 * 由于文章的浏览量在文章表中,在MyMetaObjectHandler的自动填充机制下, * 更新浏览量的操作会变更updateBy和updateTime字段,而这两个字段应该单纯地作为文章本身的更新数据, * 不能被更新浏览量的定时任务干扰 * * @author Hunter * @since 2025/2/7 */ @Component public class UpdateViewCountConfig { private boolean isUpdateViewCount = false; public boolean isUpdateViewCount() { return isUpdateViewCount; } public void setIsUpdateViewCount(boolean isUpdateViewCount) { this.isUpdateViewCount = isUpdateViewCount; } }
在定时任务更新浏览量操作的前后,设置该标识。
@EnableScheduling // 开启定时任务 @Component @Slf4j public class UpdateViewCountCronJob { ... @Resource private UpdateViewCountConfig updateViewCountConfig; /** * 每5分钟更新一次浏览量 * 从redis写回到数据库 */ @Scheduled(cron = "0 */5 * * * ?") public void updateViewCount() { log.info("========== 更新浏览量 =========="); // 从redis读出浏览量,需要先指定泛型,如果直接调用到entries只能得到Object类型 ... // 设置 是否为更新浏览量操作 的标识 updateViewCountConfig.setIsUpdateViewCount(true); // 更新到数据库 ... // 重置标识 updateViewCountConfig.setIsUpdateViewCount(false); } }
在
MyMetaObjectHandler
中针对该标识添加判别逻辑。@Component @Slf4j public class MyMetaObjectHandler implements MetaObjectHandler { @Resource private UpdateViewCountConfig updateViewCountConfig; @Override public void updateFill(MetaObject metaObject) { // 如果不是更新文章浏览量,就自动填充 update_by、update_time 字段 if (!updateViewCountConfig.isUpdateViewCount()) { ... } } }
实体类中对应字段的自动填充注解作相应变动。
查看
@TableField
注解的源码:如果选择了更新时填充字段,由于断言了该字段必有值,即使是null也会写表。因此,让fill属性不参与更新操作时的处理,而应该交给
updateStrategy
,设置在not null的时候,才写入表中。@TableField(fill = FieldFill.INSERT, updateStrategy = FieldStrategy.NOT_NULL) private Long updateBy; @TableField(fill = FieldFill.INSERT, updateStrategy = FieldStrategy.NOT_NULL) private LocalDateTime updateTime;
治本方案
治标方案在当前描述的场景下,是没有问题的,但如果将来业务逻辑复杂交织起来,很可能无法以上述方式简单地解决。而浏览量作为一个文章是否热门的指标,应该与文章本身的内容进行分离。
因此,可以将浏览量单独分表,优化查询,同时能避免 文章的更新字段 在浏览量更新时特殊处理的逻辑。