背景
问题起因是项目组一位伙伴,反映插入某张业务表时主键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;
}
}
验证
验证后已把修复的代码提交到开源框架,希望开源框架的大神们能给通过