SpringBoot开发日志注解记录字段变更内容

正在公司摸鱼的我突然接到了大佬给我的一个人任务,开发一个日志注解,来记录当方法中的每一个参数的名字,并记录每次的参数修改的值信息。接到任务的我瑟瑟发抖。

1. 数据库选择MongoDb


因为MongoDb具有以下特点

  • MongoDb为文档型数据库。操作起来比较简单和容易。
  • Mongo支持丰富的查询表达式。查询指令使用JSON形式的标记,可轻易查询文档中内嵌的对象及数组。
  • MongoDb采用Bson(类json的一种二进制形式的存储格式)存储数据,跟新和查询的速度很快
    当然了MongoDb还有很多优点我就不一一赘述了,大家可以去官网看看文档。

2. 开发实体类保存到数据库中


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
typescript复制代码@Data
public class SysLogEntity implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;

// 用户名
private String username;

// 用户操作
private String operation;

// 请求方法
private String method;

// 请求参数
private String params;

// 执行时长(毫秒)
private Long time;

// IP地址
private String ip;

// 创建时间
private Date createDate;

//所有参数名称 以逗号分割
private String parametersName;

//修改内容
private String modifyContent;

//操作类型
private String operationType;

//key值
private String logKey;

}
  • 在数据库中保存的数据格式大体是这样的

3. 思考问题


当时我在做这一个功能的时候在想,你无法确定方法的入参到底是什么类型,有可能为实体类型,有可能为Map类型,有可能为String类型,如果参数为实体类型的需要去进行反射获取其中的所有字段,这是一个很耗时的操作,那么我为什么不拿出来在SpringBoot初始化的时候来做这个事情呢,于是就有了下面的操作。

  • 首先创建一个实体类用来保存当前方法中的参数名,参数类型,字段值,和当前字段值对应的具体位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ruby复制代码@Data
public class MethodParametersInfo {
/**
* 参数名称
*/
String parameterName;
/**
* 参数类型
*/
Class<?> classType;
/**
*字段
*/
Field[] fields;
/**
* 字段
*/
Integer position;
}
  • 开发日志注解,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
less复制代码@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModifyLog {
/**
* 方法描述
* @return
*/
String value() default "";

/**
* 方法类型
* @return
*/
LogTypeEnum type();
/**
* @return 是否需要默认的改动比较
*/
boolean needDefaultCompare() default false;

/**
* key值可以根据spel表达式来填写
* @return
*/
String key();

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
arduino复制代码public enum LogTypeEnum {
/**
* 保存
*/
Save("save"),
/**
* 修改
*/
Update("update"),
/**
* 删除
*/
Delete("delete"),
/**
* 保存或修改
*/
SaveOrUpdate("saveOrUpdate");

LogTypeEnum(String key){
this.key = key;
}

public String getKey() {
return key;
}


private final String key;


}
  • 将实体类需要保存的字段进行细分,于是便又开发了一个注解,用来确定实体类中具体需要把那些字段信息保存到日志中
1
2
3
4
5
6
7
8
9
10
less复制代码@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Documented
public @interface DataName {
/**
* @return 字段名称
*/
String name() default "";

}
  • 在Springboot进行初始化的时候把方法中的参数进行缓存,若参数为实体类,进行反射获取其中所有的字段属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
ini复制代码@Component
public class ModifyLogInitialization {

@Autowired
private RequestMappingHandlerMapping mapping;

public static Map<String,Map<String, MethodParametersInfo>> modifyLogMap = new HashMap<>();


/**
* @Author: lin
* @Description: 初始化Controller层上带有 @ModifyLog 注解的方法 缓存到map中
* @DateTime: 2020/12/25 15:52
* @Params: [event]
* @Return void
*/
@EventListener
public void initializationMethod(WebServerInitializedEvent event){


//获取所有方法
Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();

handlerMethods.forEach((k,v) -> {
//判断Controller层方法上注解
if(v.getMethodAnnotation(ModifyLog.class)!=null){
Class<?> beanType = v.getBeanType();
//获取方法对象
Method method = v.getMethod();
//方法参数
MethodParameter[] parameters = v.getMethodParameters();
//参数名作为key 缓存参数信息
HashMap<String, MethodParametersInfo> methodMap = new HashMap<>();
//类路径加方法名作为key
String methodKey = beanType.getName()+"."+method.getName()+"()";
modifyLogMap.put(methodKey,methodMap);

int i = 0;
//循环方法参数
for (MethodParameter parameter : parameters) {
MethodParametersInfo info = new MethodParametersInfo();
//参数位置
info.setPosition(i);
//参数名称
String parameterName = parameter.getParameter().getName();
info.setParameterName(parameterName);
//参数类型
Class<?> parameterType = parameter.getParameterType();
info.setClassType(parameterType);

//获取所有字段
Field[] fields = parameterType.getDeclaredFields();
if(!parameterType.isAssignableFrom(String.class) & !parameterType.isAssignableFrom(Map.class)){
info.setFields(fields);
}
//加入到Map中
methodMap.put(parameterName,info);
i++;
}
}

});
}

}

用监听的方式监听WebServerInitializedEvent在启动的时候做缓存,RequestMappingHandlerMapping可以获取所有Controllec层标注@RequestMapping的方法

4. 用Aop来保存参数内容


  • 可以用spel表达式加入到注解中,这样就可以在aop中去解析获取关键值,可以参考 juejin.cn/post/684490… 篇文章
  • 为了免去使用if和else来判断参数类型我使用了适配器模式,并把每一个参数类的解析抽出取来,这样当你添加不同参数类型的解析的时候可以避免代码的侵入性
  • 下面是一个类型判断的接口,可以判断当前参数的类型,并且去调用当前参数类型的解析器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typescript复制代码public interface TypeAdapter {
/**
* @Author: lin
* @Description: 判断支持类型
* @DateTime: 2021/1/4 9:16
* @Params: [classType]
* @Return boolean
*/
boolean supprot(Class<?> classType);

/**
* @Author: lin
* @Description: 获取文本
* @DateTime: 2021/1/4 9:16
* @Params: [sb, k, v, oldObjectList, newObjectList]
* @Return void
*/
void getContent(StringBuilder sb, String k, MethodParametersInfo v, List<Object> oldObjectList, List<Object> newObjectList);
  • 这是map的类型判断并且实现map类型的参数解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typescript复制代码@Component
public class ClassTypeAdapter implements TypeAdapter {

@Autowired
ContentParse classParse;

@Override
public boolean supprot(Class<?> classType) {
return !classType.isAssignableFrom(String.class) && !classType.isAssignableFrom(Map.class);
}

@Override
public void getContent(StringBuilder sb, String k, MethodParametersInfo v, List<Object> oldObjectList, List<Object> newObjectList) {
classParse.getDifferentContent(sb,k,v,oldObjectList,newObjectList);
}
}
  • 下面是一个参数类型的解析接口
1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码public interface ContentParse {

/**
* @Author: lin
* @Description: 根据字段不同类型进行解析
* @DateTime: 2020/12/28 15:13
* @Params: [sb, k, v]
* @Return void
*/
void getDifferentContent(StringBuilder sb, String k, MethodParametersInfo v, List<Object> oldObjectList,List<Object> newObjectList);

}
  • 参数类型解析接口的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
scss复制代码@Component("classParse")
public class ClassParse implements ContentParse {
@Override
public void getDifferentContent(StringBuilder sb, String k, MethodParametersInfo v, List<Object> oldObjectList, List<Object> newObjectList) {
//获取所有字段
Field[] fields = v.getFields();
//根据字段名称跟字段映射成Map
Map<String, List<Field>> fieldMap = Arrays.stream(fields).collect(Collectors.groupingBy(Field::getName));
//记录位置
Integer position = v.getPosition();
//取出当前位置的 参数对应的值
Object oldObject = oldObjectList.get(position);
Object newObject = newObjectList.get(position);
//转换为Map
Map<String, Object> oldMap = JSONUtil.parseObj(oldObject);
Map<String, Object> newMap = JSONUtil.parseObj(newObject);
oldMap.forEach((oldK,oldV) -> {
Object newV = newMap.get(oldK);
if(!newV.equals(oldV)){
//值不相等,根据当前字段名(k值)取出当前字段
List<Field> fieldList = fieldMap.get(oldK);
Field field = fieldList.get(0);
//有没有DataName注解
if(field.isAnnotationPresent(DataName.class)){
sb.append("[参数: ").append(k)
.append("中属性").append(field.getName()).append("]从[")
.append(oldV).append("]改为了[").append(newV).append("];");
}
}
});
}
}
  • 之后我们就可以写一个Util类用来获取数据库中根据当前类路径跟方法名所对应的参数值,并跟当前传的新值作比较,判断那个参数的值有修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
typescript复制代码@Component
public class ModifyLogUtil {

/**
* @Author: lin
* @Description: 获取参数名称保存到数据库中以逗号分割
* @DateTime: 2020/12/24 14:46
* @Params: [methodKey]
* @Return java.lang.String
*/

@Autowired
private List<TypeAdapter> typeAdapterList;

public String getParametersName(String methodKey){
// 类路径跟方法名获取参数信息
Map<String, Map<String, MethodParametersInfo>> modifyLogMap = ModifyLogInitialization.modifyLogMap;
//参数信息 key是参数名 value参数信息
Map<String, MethodParametersInfo> parameterMap = modifyLogMap.get(methodKey);

StringBuilder sb = new StringBuilder();

if(parameterMap != null){
parameterMap.forEach((k,v) ->{
sb.append(k);
sb.append(",");
});
sb.deleteCharAt(sb.length()-1);
}

return sb.toString();
}

/**
* @Author: lin
* @Description: 根据参数名称获取参数信息进行比对
* @DateTime: 2020/12/24 15:05
* @Params: [methodKey, parameter]
* @Return java.lang.String
*/
public String getContentName(String methodKey, String params, ProceedingJoinPoint joinPoint){
//解析旧数据
List<Object> oldObjectList = parseOldObject(params);
//解析新数据
List<Object> newObjectList = parseNewObject(joinPoint);

// 类路径跟方法名获取参数信息
Map<String, Map<String, MethodParametersInfo>> modifyLogMap = ModifyLogInitialization.modifyLogMap;
//参数信息 key是参数名 value参数信息
Map<String, MethodParametersInfo> parameterMap = modifyLogMap.get(methodKey);

//记录
StringBuilder sb = new StringBuilder();
parameterMap.forEach((k,v) ->{
Class<?> classType = v.getClassType();

typeAdapterList.stream().filter(typeAdapter -> typeAdapter.supprot(classType)).findFirst()
.get().getContent(sb,k,v,oldObjectList,newObjectList);
});
log.info("参数改变的值为: {}",sb.toString());
return sb.toString();
}
/**
* @Author: lin
* @Description: 解析旧参数
* @DateTime: 2020/12/25 14:02
* @Params: [params]
* @Return java.util.List<java.lang.Object>
*/
public List<Object> parseOldObject(String params){
JSONArray array = JSONUtil.parseArray(params);
return new ArrayList<>(array);
}

/**
* @Author: lin
* @Description: 解析新参数
* @DateTime: 2020/12/25 14:02
* @Params: [joinPoint]
* @Return java.util.List<java.lang.Object>
*/
public List<Object> parseNewObject(ProceedingJoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
return new ArrayList<>(Arrays.asList(args));
}
}
  • 接下来就是在aop中使用这个util工具
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
scss复制代码@Slf4j
@Aspect
@Component
public class SysLogAspect {

@Autowired
private KeyResolver keyResolver;

@Autowired
private ModifyLogUtil modifyLogUtil;

@Autowired
MongoTemplate mongoTemplate;




@Around("@annotation(modifyLog)")
public Object modifyLogAround(ProceedingJoinPoint point,ModifyLog modifyLog) throws Throwable {
long beginTime = System.currentTimeMillis();
// 执行方法
Object result = point.proceed();
// 执行时长(毫秒)
long time = System.currentTimeMillis() - beginTime;

saveUpdateSysLog(point,time,modifyLog);

return result;
}


/**
* @Author: lin
* @Description: 当方法为Update类型的时候要获取其中不同项
* @DateTime: 2020/12/22 15:51
* @Params: [joinPoint, time]
* @Return void
*/
private void saveUpdateSysLog(ProceedingJoinPoint joinPoint, long time,ModifyLog modifyLog){
//日志实体类
SysLogEntity currentSysLogEntity = new SysLogEntity();
currentSysLogEntity.setId(SnowflakeUtil.snowflakeId());

MethodSignature signature = (MethodSignature) joinPoint.getSignature();
//获取key值
String key = keyResolver.resolver(modifyLog, joinPoint);
currentSysLogEntity.setLogKey(key);
log.info("key值{}",key);

//日志类型
String logType = modifyLog.type().getKey();
// 注解上的描述
currentSysLogEntity.setOperation(modifyLog.value());
//操作类型
currentSysLogEntity.setOperationType(logType);
//设置实体类字段
commonMethod(joinPoint, time, signature, currentSysLogEntity);
//根据类名加方法名去缓存中查找
String parametersName = modifyLogUtil.getParametersName(currentSysLogEntity.getMethod());
//方法中参数名称
currentSysLogEntity.setParametersName(parametersName);
//当前类型为save则保存
if(Constant.MODIFY_LOG_SAVE.equals(logType)){
mongoTemplate.insert(currentSysLogEntity);
return;
}
//当前类型为删除记录删除的字段值
if(Constant.MODIFY_LOG_DELETE.equals(logType)){
Object[] args = joinPoint.getArgs();
String deleteParam = JSONUtil.toJsonStr(args);
currentSysLogEntity.setModifyContent("当前删除的参数信息是:" + deleteParam);
mongoTemplate.insert(currentSysLogEntity);
return;
}
//当前类型为保存或修改
if(Constant.MODIFY_LOG_SAVE_OR_UPDATE.equals(logType)){

SysLogEntity oldSysLogEntity = getEntity(currentSysLogEntity);
//没有查询到就插入信息
if(oldSysLogEntity == null){
mongoTemplate.insert(currentSysLogEntity);
return;
}
//有信息则判断是否需要插入修改字段的信息
if(modifyLog.needDefaultCompare()){
String content = modifyLogContent(joinPoint,oldSysLogEntity);
//设置修改字段属性
currentSysLogEntity.setModifyContent(content);
}
mongoTemplate.insert(currentSysLogEntity);
}
}


/**
* @Author: lin
* @Description: 设置实体类属性公用方法
* @DateTime: 2020/12/22 16:07
* @Params: [joinPoint, time, signature, sysLogEntity]
* @Return void
*/
private void commonMethod(ProceedingJoinPoint joinPoint, long time, MethodSignature signature, SysLogEntity sysLogEntity) {
//请求的类名
String className = joinPoint.getTarget().getClass().getName();
//方法名
String methodName = signature.getName();
sysLogEntity.setMethod(className + "." + methodName + "()");

//请求的参数
Object[] args = joinPoint.getArgs();

String argsStr = JSONUtil.toJsonStr(args);

//保存参数
sysLogEntity.setParams(argsStr);
// 获取request
HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
// 设置IP地址
sysLogEntity.setIp(IPUtils.getIpAddr(request));

//用户名
String username = ((SysUserEntity) SecurityUtils.getSubject().getPrincipal()).getUsername();
sysLogEntity.setUsername(username);
sysLogEntity.setTime(time);
sysLogEntity.setCreateDate(new Date());
}

/**
* @Author: lin
* @Description: 是update操作则进行比对判断是那些 字段值有改变
* @DateTime: 2020/12/24 14:55
* @Params: []
* @Return void
*/
private String modifyLogContent(ProceedingJoinPoint joinPoint,SysLogEntity oldSysLogEntity){

//实际参数
String params = oldSysLogEntity.getParams();
//取出修改的参数属性
String contentName = modifyLogUtil.getContentName(oldSysLogEntity.getMethod(), params, joinPoint);

return contentName;
}

private SysLogEntity getEntity(SysLogEntity sysLogEntity){
Query query = new Query();
Criteria criteria = new Criteria();
criteria.and("logKey").is(sysLogEntity.getLogKey());
criteria.and("method").is(sysLogEntity.getMethod());
query.addCriteria(criteria);
//根据日期排序
query.with(Sort.by(Sort.Order.desc("createDate")));
query.limit(1);
SysLogEntity one = mongoTemplate.findOne(query, SysLogEntity.class);
return one;

}

}

5. 小结


  • 作为一个刚入门半年的菜鸟我还是又很多地方需要学习,这个方法肯定还能在进一步的优化,希望各位大佬能给指点一下。希望大家共同进步。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%