一、背景:

后台系统配置越来越多的出现需要进行日志记录的功能,且当前已有日志记录不可复用,需要统一日志记录格式,提高日志记录开发效率。

二、预期效果展示:

新建动作:

修改动作:

删除动作:

三、数据存储:

注:可以选择其他存储方式,这里只简单举个例子

`biz_id` bigint(20) NOT NULL DEFAULT 0 COMMENT '业务id',`biz_type` tinyint(4) NOT NULL DEFAULT 0 COMMENT '业务类型',`operator_id` varchar(128) NOT NULL DEFAULT '' COMMENT '操作人',`operate_content` text COMMENT '操作内容',`change_before` text COMMENT '修改前',`change_after` text COMMENT '修改后',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间'

四、原理简述:

日志构建关注两个对象,一个是修改前,修改后:

修改前:null + 修改后:X = 新建

修改前:Y + 修改后:X = 更新

修改前:Y + 修改后:null = 删除

修改内容判断依据传入的两个对象,对两个对象的每个属性进行逐一对比,如果发生变化则是需要进行日志记录字段;关注的属性使用注解进行标注。

五、具体实现:

注解

@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)@Inherited@Documentedpublic @interface LogField {    String name() default "";    String valueFun() default "";    boolean spliceValue() default true;}

name: name值表示该字段如果被修改,应在日志中记录的字段名;默认取字段名

valueFun: 表示获取改变字段内容的获取方法;默认取字段值,若valueFun方法不存在,则取默认值

spliceValue: 日志是否需要拼接变更内容,默认拼接

注解处理:

@Service@Slf4jpublic class OperateLogService {    @Resource    private CommonOperateLogService commonOperateLogService;    enum ActionEnum{        ADD("新建"),        UPDATE("修改"),        DELETE("删除");        ActionEnum(String desc) {            this.desc = desc;        }        public String desc;    }    private int insertLog(CommonOperatorLog commonOperatorLog){        String result = commonOperateLogService.insertLog(JSON.toJSONString(commonOperatorLog));        Response response = JSON.parseObject(result, Response.class);        return Objects.isNull(response) || ApiResponse.Status.fail.equals(response.getStatus()) ? 0 : (int) response.getContent();    }    public PageOutput queryList(Long bizId, Integer bizType, Integer pageNum, Integer pageSize){        String result = commonOperateLogService.queryLog(bizId, bizType, pageNum, pageSize);        PageOutput pageOutput = JSON.parseObject(result, new TypeReference<PageOutput>() {});        return pageOutput;    }    public  void saveLog(String operatorId,Long bizId, Integer bizType, T target, T original){        if(StringUtils.isBlank(operatorId) || (Objects.isNull(target) && Objects.isNull(original))){            throw new IllegalArgumentException();        }        if(Objects.nonNull(target) && Objects.nonNull(original) && !target.getClass().isAssignableFrom(original.getClass())){            throw new IllegalArgumentException();        }        ActionEnum action = getAction(target, original);        List<Triple> changeInfos = getChangeInfoList(target, original);        List changeInfoList = new ArrayList();        if(CollectionUtils.isEmpty(changeInfos) && !ActionEnum.UPDATE.equals(action)){            changeInfoList.add(0, action.desc);        }else if (CollectionUtils.isEmpty(changeInfos)){            return;        }else {            changeInfoList = changeInfos.stream().map(i -> i.getRight().spliceValue() ?                            action.desc + StringUtils.joinWith("为:", i.getLeft(), i.getMiddle()) :                            action.desc + StringUtils.join("了", i.getLeft()))                    .collect(Collectors.toList());        }        String operateContext = StringUtils.join(changeInfoList, "\n");        operateContext = operateContext.replaceAll("\"","")                .replaceAll("\\[","").replaceAll("\\]","");        CommonOperatorLog operatorLog = new CommonOperatorLog();        operatorLog.setBizId(bizId);        operatorLog.setBizType(bizType);        operatorLog.setOperateContent(operateContext);        operatorLog.setOperatorId(operatorId);        operatorLog.setChangeBefore(JSON.toJSONString(original));        operatorLog.setChangeAfter(JSON.toJSONString(target));        this.insertLog(operatorLog);    }    private ActionEnum getAction(Object target, Object original){        ActionEnum action = ActionEnum.ADD;        if(Objects.nonNull(target) && Objects.nonNull(original)){            action = ActionEnum.UPDATE;        }else if(Objects.nonNull(target)){            action = ActionEnum.ADD;        }else if (Objects.nonNull(original)){            action = ActionEnum.DELETE;        }        return action;    }    private List<Triple> getChangeInfoList(T target, T original){        if(Objects.isNull(target) || Objects.isNull(original)){            return new ArrayList();        }                List<Pair> targetFields = allFields(target);        List<Pair> originalFields = allFields(original);        if(targetFields.size() != originalFields.size()){            //理论上不可能执行到这            throw new IllegalArgumentException();        }        List<Triple> result = new ArrayList();        for (int i = 0; i < targetFields.size(); i++) {            Pair targetField = targetFields.get(i);            Pair originalField = originalFields.get(i);            ReflectionUtils.makeAccessible(targetField.getKey());            ReflectionUtils.makeAccessible(originalField.getKey());            Object targetValue = ReflectionUtils.getField(targetField.getKey(), targetField.getValue());            Object originalValue = ReflectionUtils.getField(originalField.getKey(), originalField.getValue());               if(targetValue != originalValue && (Objects.isNull(targetValue) ||                        (!targetValue.equals(originalValue) &&                        compareTo(Pair.of(targetField.getKey(), targetValue), Pair.of(originalField.getKey(), originalValue)) &&                        !JSON.toJSONString(targetValue).equals(JSON.toJSONString(originalValue))))){                result.add(Triple.of(getFieldName(targetField.getKey()), getFieldValue(targetField.getKey(), targetField.getValue()), targetField.getKey().getAnnotation(LogField.class)));            }        }        return result;    }    private boolean compareTo(Pair targetField, Pair originalField){        Field field = targetField.getKey();        Object targetValue = targetField.getValue();        Object originalValue = originalField.getValue();        boolean canCompare = Arrays.stream(field.getType().getInterfaces()).anyMatch(i -> Comparable.class.getName().equals(i.getName()));        if(canCompare && Objects.nonNull(targetValue) && Objects.nonNull(originalValue)){            Method compareTo = ReflectionUtils.findMethod(field.getType(), "compareTo", field.getType());            if(Objects.isNull(compareTo)){                return true;            }            Object compared = ReflectionUtils.invokeMethod(compareTo, targetValue, originalValue);            return (int)compared != 0 ;        }        return true;    }    private  List<Pair> allFields(T obj){        List<Triple> targetField = findField(obj);        List<Triple> allField = Lists.newArrayList(targetField);        List<Triple> needRemove = new ArrayList();        for (int i = 0; i < allField.size(); i++) {            Triple fieldObjectDes = allField.get(i);            if(!fieldObjectDes.getRight()){                ReflectionUtils.makeAccessible(fieldObjectDes.getLeft());                Object fieldV = ReflectionUtils.getField(fieldObjectDes.getLeft(), fieldObjectDes.getMiddle());                List<Triple> fieldList = findField(fieldV);                if(CollectionUtils.isNotEmpty(fieldList)){                    allField.addAll(fieldList);                    needRemove.add(fieldObjectDes);                }            }        }        if(CollectionUtils.isNotEmpty(needRemove)){            allField.removeAll(needRemove);        }        return allField.stream().map(i->Pair.of(i.getLeft(), i.getMiddle())).collect(Collectors.toList());    }    private  List<Triple> findField(T obj){        Class objClass = obj.getClass();        Field[] declaredFields = objClass.getDeclaredFields();        List allFields = Lists.newArrayList(declaredFields);        if(Objects.nonNull(objClass.getSuperclass())){            Field[] superClassFields = objClass.getSuperclass().getDeclaredFields();            allFields.addAll(Arrays.asList(superClassFields));        }        List<Triple> result = new ArrayList();        for (Field declaredField : allFields) {            LogField annotation = declaredField.getAnnotation(LogField.class);            if(Objects.nonNull(annotation)){                result.add(Triple.of(declaredField, obj, declaredField.getType().getPackage().getName().startsWith("java")));            }        }        return result;    }    private String getFieldName(Field field){        LogField annotation = field.getAnnotation(LogField.class);        String name = annotation.name();        if(StringUtils.isBlank(name)){            name = field.getName();        }        return name;    }    private  String getFieldValue(Field field, T targetObj){        LogField annotation = field.getAnnotation(LogField.class);        if(!annotation.spliceValue()){            return "";        }        String valueFun = annotation.valueFun();        if(StringUtils.isBlank(valueFun)){            Object fieldValue = ReflectionUtils.getField(field, targetObj);            return getStrValue(fieldValue);        }else {            Method valueMethod = ReflectionUtils.findMethod(targetObj.getClass(), valueFun);            if(Objects.isNull(valueMethod)){                Object fieldValue = ReflectionUtils.getField(field, targetObj);                return getStrValue(fieldValue);            }else {                ReflectionUtils.makeAccessible(valueMethod);                Object invokeMethodRes = ReflectionUtils.invokeMethod(valueMethod, targetObj);                return getStrValue(invokeMethodRes);            }        }    }    private String getStrValue(Object fieldValue){        List emptyStr = ImmutableList.of("\"\"", "{}","[]");        String value = Objects.isNull(fieldValue) ? "无" : JSON.toJSONString(fieldValue);        return emptyStr.contains(value) ? "无" : value;    }}

六、使用示例:

1、使用的日志记录对象(这个对象只为日志服务)

public class SubsidyRateLog {    @LogField(name = "补贴率名称")    private String name;    @LogField(name = "适用城市", valueFun = "getCityNames")    private List cityIds;    private List cityNames;}

name是直接展示字段,所以修改值即name本身的值;cityIds 是我们关心比较字段,当它值不一样时进行 字段value 值获取,这个值是展示在前端的,所以可以根据需要进行格式定义,默认是将取到的值进行toJSON;当前例子中获取的是getCityNames方法返回的值;

2、无专用日志对象(大多数时候我们有自己的实体对象,但不包含具体日志描述字段),需要进行继承

public class SubsidyRate {    @LogField(name = "补贴率名称")    private String name;    @LogField(name = "适用城市", valueFun = "getCityNames")    private List cityIds;}@Datapublic class SubsidyRateLog extends SubsidyRate{        private List cityNames;}

此方式适用于兼容现有对象,而不去破坏现有对象的完整性

3、对象包含子对象(比较复杂的大对象,如Task中的券信息)

public class SubsidyRateLog {    @LogField(name = "补贴率名称")    private String name;    @LogField    private Address address;}public class Address {    @LogField(name = "省份")    private String province;    @LogField(name = "城市")    private String city;}

此情况下会将所有信息平铺,如果 Address 中 没有_LogField_ 注解,那么会直接使用将获取address值,如果存在注解,那么将忽略address本身,只关注注解字段。

作者:京东零售祁伟