Zhu.Yang

朱阳的个人博客(公众号:think123)

0%

老大喊我用AOP记录下日志

老大喊我记录下API的操作日志,免得前端甩锅,主要记录新增,修改,删除等操作。我想了下就决定用AOP来实现这个功能。

由于使用的是SpringBoot,所以首先应该在依赖中引入AOP包。

1
2
3
4
 <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

一般引入了AOP之后,一般不用做其他特殊配置,也不用加上@EnableAspectJAutoProxy注解。但是它仍有两个属性需要我们注意

1
2
3
4
5
# 相当于@EnableAspectJAutoProxy
spring.aop.auto=true

# 默认使用jdk来实现AOP,如果想要使用CGLIB的话,这里要改成true
spring.aop.proxy-target-class=false

实现日志记录功能

我想要记录某个API对模块进行了什么操作,操作的key,对于修改,删除来说我们记录id,对于新增来说,最开始没有id,我们记录name即可(也可以是其他属性),所以OpLog注解是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface OpLog {

// 模块
String module();
// 类型
String opType();
// 级别,默认一般
String level() default OpLevel.COMMON;
// 需要记录的key
String key();
}

为了让这个功能更加易用,所以我要求其他开发者必须在Controller上加上注解@OpLog,为什么是Controller呢?因为API是在Controller上,我并不关心具体业务,我只要记录对应的操作即可。在Controller层你只需要这要做就行了

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RequestMapping("/user")
public class CpGroupController {

// 注意这里的userParam和@RequestBody的userParam名字必需要要一致
@OpLog(module = "用户管理", opType = "新增", key = "userParam.name")
@PostMapping("/add")
public BaseResponse<Boolean> add(@RequestBody UserParam userParam) {

return ResponseUtil.success(userService.add(userParam));
}
}

接下来就是切面代码的编写了,其中我们要记录访问的url以及必要的操作信息。

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
@Component
@Aspect
@Slf4j
public class OpLogAspect {

@Autowired
private OperateLogService operateLogService;

int paramNameIndex = 0;
int propertyIndexFrom = 1;

@Pointcut("execution(public * com.generalthink.springboot.web.controller..*.*(..))")
public void webLogPointcut() {
}

@Around(value = "webLogPointcut() && @annotation(opLog)", argNames = "joinPoint,opLog")
public Object around(ProceedingJoinPoint joinPoint, OpLog opLog) throws Throwable {

Object objReturn = joinPoint.proceed();

try {
OperateLog operationLog = generateOperateLog(joinPoint, opLog);

operateLogService.save(operationLog);
} catch (Exception e) {
log.error("operateLog record error", e);
}

return objReturn;
}

private OperateLog generateOperateLog(ProceedingJoinPoint joinPoint, OpLog opLog) {

String requestUrl = extractRequestUrl();

Object recordKey = getOpLogRecordKey(joinPoint, opLog);

return OperateLog.builder()
.module(opLog.module())
.opType(opLog.opType())
.level(opLog.level())
.operateTimeUnix(CommonUtils.getNowTimeUnix())
.recordKey(recordKey != null ? recordKey.toString() : null)
.url(requestUrl)
.operator(getCurrentUser())
.build();
}

// 获取当前用户
private String getCurrentUser() {
Subject subject = SecurityUtils.getSubject();
return (String) subject.getPrincipal();
}

// 获取想要记录的key
private Object getOpLogRecordKey(ProceedingJoinPoint joinPoint, OpLog opLog) {
String key = opLog.key();

if(StringUtils.isEmpty(key)) {
return null;
}

String[] keys = key.split("\\.");

//入参 value
Object[] args = joinPoint.getArgs();

CodeSignature codeSignature = (CodeSignature) joinPoint.getSignature();

// 获取Controller方法上的参数名称
String[] paramNames = codeSignature.getParameterNames();

Object paramArg = null;

for (int i = 0; i < paramNames.length; i++) {
String paramName = paramNames[i];
if (paramName.equals(keys[paramNameIndex])) {
paramArg = args[i];
break;
}
}

if(keys.length == 1) {
return paramArg;
}

// 获取参数的值
Object paramValue = null;
if(null != paramArg) {
try {
paramValue = getParamValue(paramArg, keys, propertyIndexFrom);
} catch (IllegalAccessException e) {
log.error("parse field error", e);
}
}

return paramValue;
}

public Object getParamValue(Object param, String[] keys, int idx)
throws IllegalAccessException {
Optional<Field> fieldOptional = getAllFields(new ArrayList<>(), param.getClass())
.stream()
.filter(field -> field.getName().equalsIgnoreCase(keys[idx]))
.findFirst();

if(!fieldOptional.isPresent()) {
return null;
}

// 设置属性可访问,因为bean当中属性一般都是private
Field field = fieldOptional.get();
field.setAccessible(true);


if(idx + 1 == keys.length) {
return field.get(param);
}

return getParamValue(field.get(param), keys, idx + 1);
}

// 递归获取所有Field
public List<Field> getAllFields(List<Field> fieldList, Class<?> type) {

// 获取当前类的所有Field
fieldList.addAll(Arrays.asList(type.getDeclaredFields()));

// 获取父类的所有Field
Class<?> superClass = type.getSuperclass();
if(superClass != null && superClass != Object.class) {
getAllFields(fieldList,superClass);
}

return fieldList;
}

// 获取访问URL
private String extractRequestUrl() {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();

return request.getRequestURL().toString();
}
}

主要就是记录了操作了什么模块,操作内容等等,对于修改和删除操作我们可以记录id,但是对于save操作,我们没有办法记录id,只能记录其他属性,比如说name,就可以记录保存的数据了

1
2
3
4
5
{
name: "zhangsan",
age: 20
}

对于上面的数据就会把”zhangsan”这个值保存下来,后面就可以通过查询日志表知道是谁操作的,修改和删除也是同样的.

欢迎关注我的其它发布渠道