【!!!亲测可用】利用方法注解和字段属性注解完成变更日志记录(结合切面记录日志,可直接使用)

2 篇文章 0 订阅
1 篇文章 0 订阅


前言

工作中,特别是涉及到数据的记录变更时,常常会有记录变更字段日志的需求。
本文介绍了通过日志切面和自定义注解以及利用ObjectDifferBuilder来进行数据比对,实现数据变更记录日志功能开发。

@UpdateRecord

1. 自定义注解类解析

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface UpdateRecord {
    /**
     * 表名称
     */
    String tableName() default "";

    /**
     * 业务类型
     */
    String bizType() default "";

    /**
     * 日志绑定的业务标识
     */
    String bizId() default "";

    /**
     * dao bean name
     */
    String beanName();
}

2. 注解使用示例

@UpdateRecord(tableName = "deal_meeting", beanName = "dealCompanyValuationService", bizType = "项目估值", bizId = "#dealCompanyValuation.dealId")
    public int save(DealCompanyValuation dealCompanyValuation) {
        //是否是新建
        boolean isAdd = StringUtils.isBlank(dealCompanyValuation.getId());

        // 保存主表
        int count = super.save(dealCompanyValuation);

        // TODO 写自己对应得业务逻辑

        return count;
    }

3. bizId属性写法解析

业务ID这个可以取参数对象里的值。例如:#dealMeeting.dealId

这里使用的技术是Spring SpEL一个表达式工具。


@RecordField

1. 自定义注解类解析

@Retention(RetentionPolicy.RUNTIME)
public @interface RecordField {
    /**
     * 记录字段类型 -> (3中所讲的注解枚举类)
     * @return
     */
    RecordFieldType type() default RecordFieldType.DEFAULT;

    /**
     * 记录字段名称
     * @return
     */
    String name() default "";

    /**
     * 字典TYPE
     * type = DICT时填写
     * @return
     */
    String dictType() default "";

    /**
     * 日期格式化
     * type = DATE时填写
     * @return
     */
    String dateFormat() default "yyyy-MM-dd";
    
    /** 
     * ========== 这里可以继续延申,加上其他的格式化属性,如金额单位转换啊,部门转化啊等等。
     */
}

2. 注解使用示例

/** 主体类型 */
    @Excel(name = "主体类型")
    @ApiModelProperty(value = "主体类型",position=30)
    @RecordField(name = "主体类型", type = RecordFieldType.DICT, dictType = "deal_investment_type")
    private String investType;

RecordFieldType enum枚举类

1. 枚举类内容

public enum RecordFieldType {
    DEFAULT(-1),
    DICT(1),
    USER(2),

    DATE(3);

    private final int value;

    private RecordFieldType(int value) {
        this.value = value;
    }

    public int value() {
        return this.value;
    }
}

默认是-1,对应type不转化。DICT、USER、DATE分别转化为字典、人员、日期。


SysUpdateRecord 日志记录表

1. 表结构sql

DROP TABLE IF EXISTS `sys_update_record`;
CREATE TABLE `sys_update_record`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
  `table_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '表名',
  `key_id` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改主键ID',
  `biz_id` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改记录的ID',
  `biz_type` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改类型',
  `record_name` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '记录字段名称',
  `value_before` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '更改之前的值',
  `value_after` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '更改之后的值',
  `update_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改人',
  `update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2566 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '修改历史记录' ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;


综合使用

1. 切面类开发,逻辑层编写。

@Aspect
@Component
public class UpdateRecordAspect {

    @Autowired
    private SysUpdateRecordService sysUpdateRecordService;

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final SpelExpressionParser parser = new SpelExpressionParser();

    private final DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();

    @Around("@annotation(com.anxin.sys.record.annotation.UpdateRecord)")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        Method method = getMethod(pjp);
        UpdateRecord annotation = method.getAnnotation(UpdateRecord.class);
        BaseEntity<?> entity = (BaseEntity<?>) pjp.getArgs()[0];
        String id = entity.getId();
        Object bean = SpringBeanUtils.getBean(annotation.beanName());
        Object data = null;
        // 获取修改前的对象
        if (bean instanceof BaseDao) {
            BaseDao<?> baseDao = (BaseDao<?>) bean;
            data = baseDao.getById(id);
        } else if (bean instanceof BaseService) {
            BaseService<?, ?> baseService = (BaseService<?, ?>) bean;
            data = baseService.get(id);
        }
//        String bizId = parseExpression(annotation, pjp);
        String bizId = "";
        List<SysUpdateRecord> diff = diff(data, pjp.getArgs()[0], id, annotation, bizId);
        Object result = pjp.proceed();
        sysUpdateRecordService.batchInsert(diff);
        return result;
    }

    protected Method getMethod(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        MethodSignature ms = (MethodSignature) signature;
        return ms.getMethod();
    }

    /**
     * 比较并记录变更
     * @param source 修改前对象
     * @param target 修改后对象
     * @param id 对象id
     * @param annotation updateRecord
     * @param bizId 业务id
     * @return 变更的记录
     */
    public List<SysUpdateRecord> diff(Object source, Object target, String id, UpdateRecord annotation, String bizId) {
        if (source == null || target == null) {
            try {
                Class<?> clazz = source == null ? target.getClass() : source.getClass();
                source = source == null ? clazz.getDeclaredConstructor().newInstance() : source;
                target = target == null ? clazz.getDeclaredConstructor().newInstance() : target;
            } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        }
        DiffNode diff = ObjectDifferBuilder.buildDefault().compare(source, target);
        List<SysUpdateRecord> result = Lists.newArrayList();
        Object finalSource = source;
        Object finalTarget = target;
        // 存储子表字段修改前的值和修改后的值
        // key 字段上@RecordField注解的name值
        // value 有两个值 索引0 修改前的值 索引1 修改后的值
        Map<String, String[]> childrenFieldVal = Maps.newHashMap();
        diff.visit((node, visit) -> {
            SysUpdateRecord updateRecord = diffField(node, finalSource, finalTarget, childrenFieldVal);
            if (updateRecord != null) {
                updateRecord.setTableName(annotation.tableName());
                updateRecord.setBizType(annotation.bizType());
                updateRecord.setKeyId(id);
                updateRecord.setBizId(bizId);
                updateRecord.preUpdate();
                result.add(updateRecord);
            }
        });
        childrenFieldVal.forEach((key, val) -> {
            if (!Objects.equals(val[0], val[1])) {
                SysUpdateRecord updateRecord = new SysUpdateRecord(val[0], val[1], key);
                updateRecord.setTableName(annotation.tableName());
                updateRecord.setBizType(annotation.bizType());
                updateRecord.setKeyId(id);
                updateRecord.setBizId(bizId);
                updateRecord.preUpdate();
                result.add(updateRecord);
            }
        });
        return result;
    }

    SysUpdateRecord diffField(DiffNode node, Object source, Object target, Map<String, String[]> childrenFieldVal) {
        if (node.isRootNode()) {
            return null;
        }
        RecordField recordField = node.getFieldAnnotation(RecordField.class);
        if (recordField == null || node.getValueTypeInfo() != null) {
            return null;
        }
        boolean valueIsCollection = valueIsCollection(node, source, target);
        DiffNode.State state = node.getState();
        String name = recordField.name();
        switch (state) {
            case CHANGED:
                if (!valueIsCollection) {
                    String[] values = convert(recordField, node.canonicalGet(source), node.canonicalGet(target));
                    return new SysUpdateRecord(values[0], values[1], recordField.name());
                }
                break;
            case ADDED:
                String[] beforeVal = childrenFieldVal.getOrDefault(name, new String[2]);
                String[] beforeConvert = convert(recordField, node.canonicalGet(source), null);
                beforeVal[0] = beforeConvert[0];
                childrenFieldVal.put(name, beforeVal);
                break;
            case REMOVED:
                String[] afterVal = childrenFieldVal.getOrDefault(name, new String[2]);
                String[] afterConvert = convert(recordField, null, node.canonicalGet(target));
                afterVal[1] = afterConvert[1];
                childrenFieldVal.put(name, afterVal);
                break;
            default:
                logger.warn("diff log not support");
                break;
        }
        return null;
    }

    String[] convert(RecordField recordField, Object before, Object after) {
        try {
            switch (recordField.type()) {
                case USER:
                    String beforeUserNickName = null;
                    String afterUserNickName = null;
                    if (before != null && StringUtils.isNotBlank(before.toString())){
                        SysUser beforeUser = UserUtils.getUser(String.valueOf(before));
                        if (beforeUser != null){
                            beforeUserNickName = beforeUser.getNickName();
                        }
                    }

                    if (after != null && StringUtils.isNotBlank(after.toString())){
                        SysUser afterUser = UserUtils.getUser(String.valueOf(after));
                        if (afterUser != null){
                            afterUserNickName = afterUser.getNickName();
                        }
                    }
                    return new String[] {
                            beforeUserNickName,
                            afterUserNickName
                    };
                case DICT:
                    return new String[] {
                            DictUtils.getDictLabel(String.valueOf(before), recordField.dictType(), ""),
                            DictUtils.getDictLabel(String.valueOf(after), recordField.dictType(), "")
                    };
                case DATE:
                    String[] result = new String[2];
                    if (before != null) {
                        result[0] = DateUtil.format(DateUtil.parse(String.valueOf(before)), recordField.dateFormat());
                    }
                    if (after != null) {
                        result[1] = DateUtil.format(DateUtil.parse(String.valueOf(after)), recordField.dateFormat());
                    }
                    return result;
                default:
                    return new String[] {
                            String.valueOf(before),
                            String.valueOf(after)
                    };
            }
        }catch (Exception e){
            logger.error("日志记录报错",e);
        }

        return new String[]{
                String.valueOf(before),
                String.valueOf(after)
        };
    }

    private boolean valueIsCollection(DiffNode node, Object sourceObject, Object targetObject) {
        if (sourceObject != null) {
            Object sourceValue = node.canonicalGet(sourceObject);
            if (sourceValue == null) {
                if (targetObject != null) {
                    return node.canonicalGet(targetObject) instanceof Collection;
                }
            }
            return sourceValue instanceof Collection;
        }
        return false;
    }
}

doAround解析

  1. 通过@round注解监听对应包下对应注解的活动变化;
  2. 利用ProceedingJoinPoint参数通过getMethod()方法获取到对应的方法;
  3. 根据方法对象获取到对应加在方法上的注解SpringBeanUtils.getBean(UpdateRecord.Class);
  4. 转成我们对应继承的基础父类BaseEntity,并取出对应的id数据;
  5. 根据切面方法参数获取到对应的目标方法参数,我们第一个参数就是我们的业务对象;
  6. 获取到对应bean是属于dao层还是service层
  7. 利用instanceOf根据不同的结构进行不同的数据处理,最终得到我们的传入对象;
  8. 编写方法: 利用最终得到的数据源对象,如今传入的对象,id,注解annotation,业务id来进行最后的逻辑判断处理;
  9. 最后返回List集合的日志记录数据,然后进行日志插入数据库操作;
  10. 最终完成日志记录;

diff方法解析,一切变更判断的逻辑处理汇集地(详细看代码中注解解析)

ObjectDifferBuilder类的使用详解,详见https://blog.csdn.net/feeltouch/article/details/86683119
List<SysUpdateRecord> diff = diff(data, pjp.getArgs()[0], id, annotation, bizId);

// diff方法,传入source原始对象  target新对象,id,注解对象,业务id对象
public List<SysUpdateRecord> diff(Object source, Object target, String id, UpdateRecord annotation, String bizId) {
    // 构造数据处理,两个是否其中一个为空,则进行数据构造
    // 利用反射来获取到对应的新实例方法,保证任何一个都不能为null,防止后续报错。
        if (source == null || target == null) {
            try {
                // 源数据如果为空,clazz赋值新数据target类,否则clazz及时源数据类
                Class<?> clazz = source == null ? target.getClass() : source.getClass();
                // 如果源数据为空,clazz构造新对象给源数据,否则直接赋值
                source = source == null ? clazz.getDeclaredConstructor().newInstance() : source;
                // 同上
                target = target == null ? clazz.getDeclaredConstructor().newInstance() : target;
            } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        }
    // 利用java对象比较器ObjectDifferBuilder构造出对象调用compare对象来进行字段结构进行判断
        DiffNode diff = ObjectDifferBuilder.buildDefault().compare(source, target);
    // 构造list来记录生成的日志信息
        List<SysUpdateRecord> result = Lists.newArrayList();
    // 接收处理后的源对象和新对象(此时已经都不为空)
        Object finalSource = source;
        Object finalTarget = target;
        // 存储子表字段修改前的值和修改后的值
        // key 字段上@RecordField注解的name值
        // value 有两个值 索引0 修改前的值 索引1 修改后的值
        Map<String, String[]> childrenFieldVal = Maps.newHashMap();
    // 遍历节点进行判断处理
        diff.visit((node, visit) -> {
            // 根据传入的对象和节点以及返回的唯一map结构传入,构造出对应节点的变化记录信息
            SysUpdateRecord updateRecord = diffField(node, finalSource, finalTarget, childrenFieldVal);
            // update的属性值 前后都有值  走这里保存
            if (updateRecord != null) {
                updateRecord.setTableName(annotation.tableName());
                updateRecord.setBizType(annotation.bizType());
                updateRecord.setKeyId(id);
                updateRecord.setBizId(bizId);
                updateRecord.preUpdate();
                result.add(updateRecord);
            }
        });
    	// 之前字段值为空 || 现在字段值为空  走这里保存
        childrenFieldVal.forEach((key, val) -> {
            if (!Objects.equals(val[0], val[1])) {
                SysUpdateRecord updateRecord = new SysUpdateRecord(val[0], val[1], key);
                updateRecord.setTableName(annotation.tableName());
                updateRecord.setBizType(annotation.bizType());
                updateRecord.setKeyId(id);
                updateRecord.setBizId(bizId);
                updateRecord.preUpdate();
                result.add(updateRecord);
            }
        });
        return result;
    }

diffField方法解析
// 传入节点 源对象(非空)现对象 构造的最终全属性返回的更改变化数据map集
SysUpdateRecord diffField(DiffNode node, Object source, Object target, Map<String, String[]> childrenFieldVal) {
    // 如果是根节点 则直接返回空
        if (node.isRootNode()) {
            return null;
        }
    	// 获取每个node节点上对应的注解对象
        RecordField recordField = node.getFieldAnnotation(RecordField.class);
    	// 注解对象为空或者node节点获取的值类型对象不为空,则返回null
        if (recordField == null || node.getValueTypeInfo() != null) {
            return null;
        }
    	// 判断节点类型是否是子列表集合,例如:List<User> userList;
        boolean valueIsCollection = valueIsCollection(node, source, target);
    	// 获取节点的操作状态
        DiffNode.State state = node.getState();
    	// 获取对应节点注解标注的名称
        String name = recordField.name();
        switch (state) {
            // changed 被改变(之前这个属性有,现在改变成为另一个了)
            case CHANGED:
                // 是否是子列表
                if (!valueIsCollection) {
                    // 转换属性,比如字典值为sys_dict_sex 1 ->男
                    String[] values = convert(recordField, node.canonicalGet(source), node.canonicalGet(target));
                    // 将返回的数据字符串数组拆分成新老数据全参构造到SysUpdateRecord对象中
                    return new SysUpdateRecord(values[0], values[1], recordField.name());
                }
                break;
            // addEd 新增(之前这个属性有值,现在没有)
            case ADDED:
                // 之前这个属性有值,构造这个属性的数组结构为字符数组总量为2的空字符串数组
                String[] beforeVal = childrenFieldVal.getOrDefault(name, new String[2]);
                // 转化之前的对象对应的这个属性值
                String[] beforeConvert = convert(recordField, node.canonicalGet(source), null);
                // 将这个属性值赋值给字符串数组第一个位置,也就是旧数据
                beforeVal[0] = beforeConvert[0];
                // 放入这个最终的map中
                childrenFieldVal.put(name, beforeVal);
                break;
             // removed 移除(这个是现在有值,以前的被移除了)
            case REMOVED:
                // 同上,这个是现在有值,以前的被移除了
                String[] afterVal = childrenFieldVal.getOrDefault(name, new String[2]);
                String[] afterConvert = convert(recordField, null, node.canonicalGet(target));
                afterVal[1] = afterConvert[1];
                childrenFieldVal.put(name, afterVal);
                break;
            default:
                logger.warn("diff log not support");
                break;
        }
        return null;
    }
	// 转化的方法,返回转化后的new String[]{}
    String[] convert(RecordField recordField, Object before, Object after) {
        try {
            switch (recordField.type()) {
                case USER:
                    String beforeUserNickName = null;
                    String afterUserNickName = null;
                    if (before != null && StringUtils.isNotBlank(before.toString())){
                        SysUser beforeUser = UserUtils.getUser(String.valueOf(before));
                        if (beforeUser != null){
                            beforeUserNickName = beforeUser.getNickName();
                        }
                    }

                    if (after != null && StringUtils.isNotBlank(after.toString())){
                        SysUser afterUser = UserUtils.getUser(String.valueOf(after));
                        if (afterUser != null){
                            afterUserNickName = afterUser.getNickName();
                        }
                    }
                    return new String[] {
                            beforeUserNickName,
                            afterUserNickName
                    };
                case DICT:
                    return new String[] {
                            DictUtils.getDictLabel(String.valueOf(before), recordField.dictType(), ""),
                            DictUtils.getDictLabel(String.valueOf(after), recordField.dictType(), "")
                    };
                case DATE:
                    String[] result = new String[2];
                    if (before != null) {
                        result[0] = DateUtil.format(DateUtil.parse(String.valueOf(before)), recordField.dateFormat());
                    }
                    if (after != null) {
                        result[1] = DateUtil.format(DateUtil.parse(String.valueOf(after)), recordField.dateFormat());
                    }
                    return result;
                default:
                    return new String[] {
                            String.valueOf(before),
                            String.valueOf(after)
                    };
            }
        }catch (Exception e){
            logger.error("日志记录报错",e);
        }

        return new String[]{
                String.valueOf(before),
                String.valueOf(after)
        };
    }
	// 判断是该节点是否是子列表
    private boolean valueIsCollection(DiffNode node, Object sourceObject, Object targetObject) {
        if (sourceObject != null) {
            Object sourceValue = node.canonicalGet(sourceObject);
            if (sourceValue == null) {
                if (targetObject != null) {
                    return node.canonicalGet(targetObject) instanceof Collection;
                }
            }
            return sourceValue instanceof Collection;
        }
        return false;
    }

效果演示

1. 对象属性变化

2. 子列表(后期优化)


总结

以上就是今天要讲的内容,后续会对子列表进行优化。发表出一篇集版本控制、数据记录、日志处理于一体的文章,当然,该文章已符合大多数人的需求。
  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

〆﹏destiny 筑梦)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值