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_byupdate_time字段,而这两个字段应该单纯地作为文章本身的更新数据,不应该被更新浏览量的定时任务干扰


治标方案

针对定时任务更新浏览量的任务,进行单独的逻辑处理

  1. 提供是否为更新浏览量操作的标识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;
        }
    }
  2. 在定时任务更新浏览量操作的前后,设置该标识。

    @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);
        }
    }
  3. 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()) {
                ...
            }
        }
    }
  4. 实体类中对应字段的自动填充注解作相应变动

    查看@TableField注解的源码:

    image-20250208103838740 image-20250208104158113

    如果选择了更新时填充字段,由于断言了该字段必有值,即使是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;

治本方案

治标方案在当前描述的场景下,是没有问题的,但如果将来业务逻辑复杂交织起来,很可能无法以上述方式简单地解决。而浏览量作为一个文章是否热门的指标,应该与文章本身的内容进行分离

因此,可以将浏览量单独分表,优化查询,同时能避免 文章的更新字段 在浏览量更新时特殊处理的逻辑