⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 juejin.cn/post/7394094789270372403 「青石路」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注微信公众号:【芋道源码】有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言都将得到认真回复。甚至不知道如何读源码也可以请教噢。
  4. 新的源码解析文章实时收到通知。每周更新一篇左右。
  5. 认真的源码交流微信群。

一、背景

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

二、预期效果展示

新建动作:

修改动作:

删除动作:

三、数据存储

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

`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
@Documented
public @interface LogField {

String name() default "";

String valueFun() default "";

boolean spliceValue() default true;
}
  • name: name值表示该字段如果被修改,应在日志中记录的字段名;默认取字段名
  • valueFun: 表示获取改变字段内容的获取方法;默认取字段值,若valueFun方法不存在,则取默认值
  • spliceValue: 日志是否需要拼接变更内容,默认拼接

注解处理:

@Service
@Slf4j
public 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<CommonOperatorLog> 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<CommonOperatorLog>>() {});
return pageOutput;
}

public <T> 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<String, String, LogField>> changeInfos = getChangeInfoList(target, original);
List<String> 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<T> List<Triple<String, String, LogField>> getChangeInfoList(T target, T original){
if(Objects.isNull(target) || Objects.isNull(original)){
return new ArrayList<>();
}
List<Pair<Field, Object>> targetFields = allFields(target);
List<Pair<Field, Object>> originalFields = allFields(original);
if(targetFields.size() != originalFields.size()){
//理论上不可能执行到这
throw new IllegalArgumentException();
}

List<Triple<String, String, LogField>> result = new ArrayList<>();
for (int i = 0; i < targetFields.size(); i++) {
Pair<Field, Object> targetField = targetFields.get(i);
Pair<Field, Object> 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<Field, Object> targetField, Pair<Field, Object> 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 <T> List<Pair<Field, Object>> allFields(T obj){
List<Triple<Field, Object, Boolean>> targetField = findField(obj);
List<Triple<Field, Object, Boolean>> allField = Lists.newArrayList(targetField);
List<Triple<Field, Object, Boolean>> needRemove = new ArrayList<>();
for (int i = 0; i < allField.size(); i++) {
Triple<Field, Object, Boolean> fieldObjectDes = allField.get(i);
if(!fieldObjectDes.getRight()){
ReflectionUtils.makeAccessible(fieldObjectDes.getLeft());
Object fieldV = ReflectionUtils.getField(fieldObjectDes.getLeft(), fieldObjectDes.getMiddle());
List<Triple<Field, Object, Boolean>> 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 <T> List<Triple<Field, Object, Boolean>> findField(T obj){
Class<?> objClass = obj.getClass();
Field[] declaredFields = objClass.getDeclaredFields();
List<Field> allFields = Lists.newArrayList(declaredFields);
if(Objects.nonNull(objClass.getSuperclass())){
Field[] superClassFields = objClass.getSuperclass().getDeclaredFields();
allFields.addAll(Arrays.asList(superClassFields));
}
List<Triple<Field, Object, Boolean>> 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 <T> 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<String> 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<Integer> cityIds;

private List<String> cityNames;
}

name是直接展示字段,所以修改值即name本身的值;

cityIds 是我们关心比较字段,当它值不一样时进行 字段value 值获取,这个值是展示在前端的,所以可以根据需要进行格式定义,默认是将取到的值进行toJSON;

当前例子中获取的是getCityNames方法返回的值;

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

public class SubsidyRate {

@LogField(name = "补贴率名称")
private String name;

@LogField(name = "适用城市", valueFun = "getCityNames")
private List<Integer> cityIds;
}

@Data
public class SubsidyRateLog extends SubsidyRate{

private List<String> 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本身,只关注注解字段。

文章目录
  1. 1. 一、背景
  2. 2. 二、预期效果展示
  3. 3. 三、数据存储
  4. 4. 四、原理简述
  5. 5. 五、具体实现
  6. 6. 六、使用示例