一、背景
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时序图

