排查使用MyBatis-Plus自动填充字段时,未标记@TableField(fill = FieldFill.INSERT)却被插入填充值问题

背景

问题起因是项目组一位伙伴,反映插入某张业务表时主键id不是雪花算法,而是看起来像是自增主键的值,且数据库已经产生了那种类似自增主键的值,导致插入数据时主键id冲突,程序异常无法插入数据。

排查过程

1、检查那张表的主键配置未发现问题,是采取的mp自动生成的雪花算法(隐藏项目业务相关信息)

@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("test")
@EqualsAndHashCode(callSuper = true)
public class TestEntity extends Model<TestEntity> {
    /**
     * 主键
     */
    @TableId(type = IdType.ASSIGN_ID)
    private Long deptId;

    /**
     * 创建人
     */
    @TableField(fill = FieldFill.INSERT)
    private String createBy;
        
    /**
     * 创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;
}

后来突然联想到最近有其他人,添加了数据权限相关功能时,修改框架了mp的自动填充处理器,增加了默认填充字段deptId(当前登录人的部门编号)。


import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.pig4cloud.pig.common.core.constant.CommonConstants;
import com.pig4cloud.pig.common.security.util.SecurityUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.ClassUtils;

import java.nio.charset.Charset;
import java.time.LocalDateTime;
import java.util.Optional;

/**
 * MybatisPlus 自动填充配置
 *
 * @author L.cm
 */
@Slf4j
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        log.debug("mybatis plus start insert fill ....");
        LocalDateTime now = LocalDateTime.now();

        // 审计字段自动填充,覆盖用户输入
        fillValIfNullByName("createTime", now, metaObject, true);
        fillValIfNullByName("updateTime", now, metaObject, true);
        fillValIfNullByName("createBy", getUserName(), metaObject, true);
        fillValIfNullByName("updateBy", getUserName(), metaObject, true);

        // 删除标记自动填充
        fillValIfNullByName("delFlag", CommonConstants.STATUS_NORMAL, metaObject, true);

        // 当前登录人的部门编号
        if (ObjUtil.isNotNull(SecurityUtils.getUser())) {
            fillValIfNullByName("deptId", SecurityUtils.getUser().getDeptId(), metaObject, true);
        }
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.debug("mybatis plus start update fill ....");
        fillValIfNullByName("updateTime", LocalDateTime.now(), metaObject, true);
        fillValIfNullByName("updateBy", getUserName(), metaObject, true);
    }

    /**
     * 填充值,先判断是否有手动设置,优先手动设置的值,例如:job必须手动设置
     *
     * @param fieldName  属性名
     * @param fieldVal   属性值
     * @param metaObject MetaObject
     * @param isCover    是否覆盖原有值,避免更新操作手动入参
     */
    private static void fillValIfNullByName(String fieldName, Object fieldVal, MetaObject metaObject, boolean isCover) {
        // 0. 如果填充值为空
        if (fieldVal == null) {
            return;
        }
        // 1. 没有 get 方法
        if (!metaObject.hasSetter(fieldName)) {
            return;
        }
        // 2. 如果用户有手动设置的值
        Object userSetValue = metaObject.getValue(fieldName);
        String setValueStr = StrUtil.str(userSetValue, Charset.defaultCharset());
        if (StrUtil.isNotBlank(setValueStr) && !isCover) {
            return;
        }
        // 3. field 类型相同时设置
        Class<?> getterType = metaObject.getGetterType(fieldName);
        if (ClassUtils.isAssignableValue(getterType, fieldVal)) {
            metaObject.setValue(fieldName, fieldVal);
        }
    }

    /**
     * 获取 spring security 当前的用户名
     *
     * @return 当前用户名
     */
    private String getUserName() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // 匿名接口直接返回
        if (authentication instanceof AnonymousAuthenticationToken) {
            return null;
        }

        if (Optional.ofNullable(authentication).isPresent()) {
            return authentication.getName();
        }

        return null;
    }

}

猜测是mp的自动填充给主键值覆盖了,但是问题点在于我们并没有给主键deptId字段加自动填充注解@TableField(fill = FieldFill.INSERT),调试代码和查阅MyBatis-Plus官网,发现不是MyBatis-Plus的问题(官网中明确说明字段必须声明@TableField注解才会填充默认值)。而是开源框架中的自动填充器逻辑问题,仔细查看填充器的代码,发现不光是主键,其他有值的参数,即使未被标记注解,值也会被自动填充。
在这里插入图片描述

MyBatis-Plus填充逻辑的源码

    default MetaObjectHandler strictFill(boolean insertFill, TableInfo tableInfo, MetaObject metaObject, List<StrictFill<?, ?>> strictFills) {
        if (insertFill && tableInfo.isWithInsertFill() || !insertFill && tableInfo.isWithUpdateFill()) {
            strictFills.forEach((i) -> {
                String fieldName = i.getFieldName();
                Class<?> fieldType = i.getFieldType();
                tableInfo.getFieldList().stream().filter((j) -> {
                    return j.getProperty().equals(fieldName) && fieldType.equals(j.getPropertyType()) && (insertFill && j.isWithInsertFill() || !insertFill && j.isWithUpdateFill());
                }).findFirst().ifPresent((j) -> {
                    this.strictFillStrategy(metaObject, fieldName, i.getFieldVal());
                });
            });
        }

        return this;
    }

解决方案

至此发现问题,修改开源框架中的自动填充器的逻辑。并且优化了部分代码逻辑,同时修复了框架中的自动填充器的其他问题【1、优化:值覆盖与否,不再判断指定属性是否有值,因为该判断没有实际作用;2、fix:字段未设置@TableField(fill = FieldFill.INSERT)注解也会被填充默认值问题;3、fix:自动填充时未根据@TableField属性fill判断是新增时填充还是修改时填充问题】

package com.lxdz.kyj.common.data.mybatis;

import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.pig4cloud.pig.common.core.constant.CommonConstants;
import com.pig4cloud.pig.common.security.util.SecurityUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.ClassUtils;

import java.nio.charset.Charset;
import java.time.LocalDateTime;
import java.util.Optional;

/**
 * MybatisPlus 自动填充配置
 *
 * @author L.cm
 */
@Slf4j
public class MybatisPlusMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        log.debug("mybatis plus start insert fill ....");
        LocalDateTime now = LocalDateTime.now();

        // 审计字段自动填充
        fillValIfNullByName("createTime", now, metaObject, true, true);
        fillValIfNullByName("updateTime", now, metaObject, false, true);
        fillValIfNullByName("createBy", getUserName(), metaObject, false, true);
        fillValIfNullByName("updateBy", getUserName(), metaObject, true, true);
        // 删除标记自动填充
        fillValIfNullByName("delFlag", CommonConstants.STATUS_NORMAL, metaObject, false, true);
        // 当前登录人的部门编号
        if (ObjUtil.isNotNull(SecurityUtils.getUser())) {
            fillValIfNullByName("deptId", SecurityUtils.getUser().getDeptId(), metaObject, true);
        }
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.debug("mybatis plus start update fill ....");
        fillValIfNullByName("updateTime", LocalDateTime.now(), metaObject, true, false);
        fillValIfNullByName("updateBy", getUserName(), metaObject, true,false);
    }

    /**
     * 填充值,先判断是否有手动设置,优先手动设置的值,例如:job必须手动设置
     *
     * @param fieldName  属性名
     * @param fieldVal   属性值
     * @param metaObject MetaObject
     * @param isCover    是否覆盖原有值,避免更新操作手动入参
     */
    private void fillValIfNullByName(String fieldName, Object fieldVal, MetaObject metaObject, boolean isCover, boolean insertFill) {
        // 0. 如果填充值为空
        if (fieldVal == null) {
            return;
        }

        // 1. 没有 get 方法
        if (!metaObject.hasSetter(fieldName)) {
            return;
        }
        // 2. 如果用户有手动设置的值
        if (!isCover) {
            return;
        }
        // 3. field 类型相同时设置
        Class<?> getterType = metaObject.getGetterType(fieldName);
        if (ClassUtils.isAssignableValue(getterType, fieldVal)) {
            TableInfo tableInfo = findTableInfo(metaObject);
            if (tableInfo.getFieldList().stream().anyMatch((i) -> i.getProperty().equals(fieldName)
                    && getterType.equals(i.getPropertyType())
                    && (insertFill && i.isWithInsertFill() || !insertFill && i.isWithUpdateFill()))) {
                metaObject.setValue(fieldName, fieldVal);
            }
        }
    }

    /**
     * 获取 spring security 当前的用户名
     *
     * @return 当前用户名
     */
    private String getUserName() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (Optional.ofNullable(authentication).isPresent()) {
            return authentication.getName();
        }
        return null;
    }
}

验证

验证后已把修复的代码提交到开源框架,希望开源框架的大神们能给通过

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值