一、背景

1、后端的核心链路的日志,如 订单的增删改,优惠券的增删改,活动等的操作,都需要记录日志在数据库表中,方便后续审计核查。
2、那这块的日志统一管理起来,采用注解形式来实现呢?

二、技术分析

1、AOP:既然确定了注解形式,那肯定离不开aop 切面拦截
2、SpEL:基于注解解析日志,使用spring 的表达式框架,动态获取上下文来填充日志。
3、java反射:自定义函数的解析与执行

三、实现分析

1、AOP 首先获取注解的信息。封装成一个日志模板,该模板可以支持动态数据库获取信息,也可以支持SPEL 表达式获取上下文信息
2、采用stack 的数据结构,来判断和解析模板。

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
public static BraceResult findBraceResult(String content) {
// 找到第一个"{"
int leftBraceIndex = content.indexOf("{");
int matchedRightBraceIndex = -1;
if(leftBraceIndex<0) {
return null;
}
// stack用于处理第一个"{"右边可能出现的"{"
Stack<Character> stack = new Stack<>();
stack.push('{');
// 从第一个"{"开始遍历,找到其匹配的"}"的index
for(int i=leftBraceIndex+1;i<content.length();i++) {
// 第一个"{"的右边还可能有"{"
if(content.charAt(i) == '{') {
stack.push('{');
}
if(content.charAt(i) == '}') {
// 遇到"}",栈中的"{"就需要弹出一个
stack.pop();
}
if(stack.isEmpty()) {
// 当栈空了,说明我们要找的和第一个"{"匹配的"}"就找到了
matchedRightBraceIndex = i;
break;
}
}
// 没有找到相匹配的"}"
if(matchedRightBraceIndex == -1) {
return null;
}
String betweenBraceContent = content.substring(leftBraceIndex + 1 ,matchedRightBraceIndex);
return new BraceResult(leftBraceIndex,matchedRightBraceIndex,betweenBraceContent);
}

3、

封装自定义函数,后续基于反射,来执行自定义函数,该处的量亮点在于,可以从json结果集中获取信息,也可以使用spel 获取填充信息

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
private IExpression parseExpression(String content) {
BraceResult braceResult = BraceUtils.findBraceResult(content);
if (null != braceResult) {
// 自定义函数
String classAndFunctionName = content.substring(0, braceResult.getLeftBraceIndex());
String className = classAndFunctionName.substring(0, classAndFunctionName.indexOf("."));
String functionName = classAndFunctionName.substring(classAndFunctionName.indexOf(".") +1);
String input = content.substring(braceResult.getLeftBraceIndex() + 1, braceResult.getMatchedRightBraceIndex());
String jsonKey = null;
if(content.length() != braceResult.getMatchedRightBraceIndex() + 1) {
jsonKey = content.substring(braceResult.getMatchedRightBraceIndex() + 2);
}
return new FunctionWithJSONTemplate(className,functionName, new FuncInput(input), jsonKey);
}
else {
// SpEl表达式
return new SpELTemplate(content);
}
}

public class FunctionWithJSONTemplate implements IExpression {

/**
* 自定义类名称
*/
private String className;

/**
* 自定义函数名称
*/
private String functionName;

/**
* 自定义函数入参
*/
private FuncInput input;

/**
* json key
*/
private String jsonKey;

@Override
public String execute(Object result,ProceedingJoinPoint point,
OperateLogExpressionEvaluator expressionEvaluator,
IFunctionService iFunctionService) {

Object value = null;
if(input.isSpEl()) {
//执行SpEL
value = expressionEvaluator.executeObjectExpression(result,point, input.getInput());
}
else {
value = input.getInput();
}
String functionResult = iFunctionService.apply(className,functionName,value);
if(JSONObject.isValidObject(functionResult)){
JSONObject jsonObj = JSON.parseObject(functionResult);
return jsonObj.getString(jsonKey);
}
return functionResult;
}

四、使用注意事项

对于RT要求高的场景

(1)建议不要使用自定义函数,
(2)最后的日志处理,改成异步发kafka消息。

对于想使用自定义函数

(1)有一类场景,如日志模板为 【订单状态从 xxxa 更改为 xxxb】,
那该类日志的方法必须是在事务内,否则方法执行完,就是提交状态,自定义函数获取的是事务提交后的信息

五、详细实现

核心流程

(1)解析操作日志模板和表达式,封装成日志模板
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
private LogTemplateResult parseOperateLog(Object result,ProceedingJoinPoint point, OperateLog operateLog) {

String operator = null;
String bizNo = null;
String bizType = operateLog.bizType();

// 解析业务编码
bizNo = expressionEvaluator.executeStringExpression(result,point, operateLog.bizNo());
if (StringUtils.isEmpty(bizNo)) {
LOGGER.warn("bizNo is empty, please check your bizNo expression");
throw new UnsupportedOperationException("bizNo cannot be empty!!");
}
// 解析日志模板和表达式
LogTemplateResult logTemplateResult = parseLogTemplateResult(operateLog.content());
// 解析操作人
operator = expressionEvaluator.executeStringExpression(result,point, operateLog.operator());

logTemplateResult.setOperateLog(operateLog);
logTemplateResult.setBizNo(bizNo);
logTemplateResult.setOperator(operator);
logTemplateResult.setBizType(bizType);
logTemplateResult.setPoint(point);

return logTemplateResult;
}
(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
private OperateLogInstance executeExpressionAndFillPlaceholder(Object result,LogTemplateResult logTemplateResult) {
String logContent = null;
ProceedingJoinPoint point = logTemplateResult.getPoint();
OperateLog operateLog = logTemplateResult.getOperateLog();

// 如果日志LogContentTemplate里面包含表达式的话
if (logTemplateResult.isHasExpression()) {
String logContentTemplate = logTemplateResult.getContentTemplate();
List<String> expressionResult = new ArrayList<>();
// 执行自定义函数
for (IExpression expression : logTemplateResult.getExpressions()) {
// 保存执行的结果
expressionResult.add(expression.execute(result,point, expressionEvaluator, iFunctionService));
}
// 构造最终的日志content
logContent = String.format(logContentTemplate, expressionResult.toArray(new String[expressionResult.size()]));
} else {
// 普通logContent
logContent = operateLog.content();
}

// 构造操作日志实例
return OperateLogInstance.builder()
.bizNo(logTemplateResult.getBizNo())
.operator(logTemplateResult.getOperator())
.bizType(logTemplateResult.getBizType())
.logContent(logContent)
.build();
}
(3)存储操作日志,需要业务方自行定义
1
2
3
4
5
6
public interface OperateLogStoreService {
/**
* 存储操作日志
*/
void storeOperateLog(OperateLogInstance instance) throws Exception;
}

类图

涉及到的类

UML时序图

时序图1
时序图2