refactor(ops): 重构业务日志注解和切面
- 更新 BusinessLog 注解以支持新的事件日志模型 - 更新 BusinessLogAspect 切面使用 EventLogRecorder Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
package com.viewsh.module.ops.infrastructure.log.annotation;
|
||||
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.EventLevel;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.LogScope;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.LogType;
|
||||
|
||||
@@ -13,11 +15,20 @@ import java.lang.annotation.*;
|
||||
* 使用示例:
|
||||
* <pre>
|
||||
* {@code
|
||||
* // 基础用法(使用 LogType/LogScope)
|
||||
* @BusinessLog(type = LogType.DISPATCH, scope = LogScope.ORDER,
|
||||
* description = "自动派单", includeParams = true, includeResult = true)
|
||||
* description = "自动派单", includeParams = true)
|
||||
* public DispatchResult dispatch(OrderDispatchContext context) {
|
||||
* // ...
|
||||
* }
|
||||
*
|
||||
* // 新用法(直接指定 EventDomain 和 Module)
|
||||
* @BusinessLog(module = "clean", domain = EventDomain.DEVICE,
|
||||
* eventType = "TTS_SENT", description = "语音播报",
|
||||
* deviceId = "#deviceId", personId = "#context.cleanerId")
|
||||
* public void broadcast(String text, Long deviceId) {
|
||||
* // ...
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
@@ -28,15 +39,19 @@ import java.lang.annotation.*;
|
||||
@Documented
|
||||
public @interface BusinessLog {
|
||||
|
||||
/**
|
||||
* 日志类型
|
||||
*/
|
||||
LogType type();
|
||||
// ==================== 旧版字段(保持兼容) ====================
|
||||
|
||||
/**
|
||||
* 日志作用域
|
||||
* 日志类型(旧版,保持兼容)
|
||||
* <p>
|
||||
* 当指定了 domain 和 eventType 时,此字段可忽略
|
||||
*/
|
||||
LogScope scope();
|
||||
LogType type() default LogType.SYSTEM;
|
||||
|
||||
/**
|
||||
* 日志作用域(旧版,保持兼容)
|
||||
*/
|
||||
LogScope scope() default LogScope.SYSTEM;
|
||||
|
||||
/**
|
||||
* 操作描述
|
||||
@@ -84,4 +99,70 @@ public @interface BusinessLog {
|
||||
* 自定义标签
|
||||
*/
|
||||
String[] tags() default {};
|
||||
|
||||
// ==================== 新版字段(支持 EventLogRecorder) ====================
|
||||
|
||||
/**
|
||||
* 模块标识(新版)
|
||||
* <p>
|
||||
* 如:clean, repair, patrol
|
||||
* <p>
|
||||
* 当指定此字段时,将使用 EventLogRecorder 记录日志
|
||||
*/
|
||||
String module() default "";
|
||||
|
||||
/**
|
||||
* 事件域(新版)
|
||||
* <p>
|
||||
* 当指定此字段时,将使用 EventLogRecorder 记录日志
|
||||
*/
|
||||
EventDomain domain() default EventDomain.SYSTEM;
|
||||
|
||||
/**
|
||||
* 事件类型(新版)
|
||||
* <p>
|
||||
* 当指定此字段时,将使用 EventLogRecorder 记录日志
|
||||
*/
|
||||
String eventType() default "";
|
||||
|
||||
/**
|
||||
* 日志级别(新版)
|
||||
*/
|
||||
EventLevel level() default EventLevel.INFO;
|
||||
|
||||
/**
|
||||
* 设备ID提取表达式(SpEL)
|
||||
* <p>
|
||||
* 示例:"#deviceId", "#context.deviceId", "#request.deviceId"
|
||||
*/
|
||||
String deviceId() default "";
|
||||
|
||||
/**
|
||||
* 人员ID提取表达式(SpEL)
|
||||
* <p>
|
||||
* 示例:"#cleanerId", "#context.userId", "#request.assigneeId"
|
||||
*/
|
||||
String personId() default "";
|
||||
|
||||
/**
|
||||
* 业务实体ID提取表达式(SpEL)
|
||||
* <p>
|
||||
* 示例:"#orderId", "#context.orderId"
|
||||
*/
|
||||
String targetId() default "";
|
||||
|
||||
/**
|
||||
* 业务实体类型
|
||||
* <p>
|
||||
* 如:order, area, task
|
||||
*/
|
||||
String targetType() default "";
|
||||
|
||||
/**
|
||||
* 简要摘要模板(用于列表展示)
|
||||
* <p>
|
||||
* 示例:"播报: {text}", "到岗: {areaName}"
|
||||
*/
|
||||
String summary() default "";
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.viewsh.module.ops.infrastructure.log.aspect;
|
||||
|
||||
import com.viewsh.module.ops.infrastructure.log.annotation.BusinessLog;
|
||||
import com.viewsh.module.ops.infrastructure.log.context.BusinessLogContext;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.EventDomain;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.EventLevel;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.LogScope;
|
||||
import com.viewsh.module.ops.infrastructure.log.enumeration.LogType;
|
||||
import com.viewsh.module.ops.infrastructure.log.publisher.BusinessLogPublisher;
|
||||
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecord;
|
||||
import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
@@ -19,8 +21,6 @@ import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 业务日志切面
|
||||
@@ -28,12 +28,12 @@ import java.util.List;
|
||||
* 职责:
|
||||
* 1. 拦截 @BusinessLog 注解的方法
|
||||
* 2. 提取方法参数和结果
|
||||
* 3. 发布业务日志
|
||||
* 3. 统一通过 EventLogRecorder 记录日志
|
||||
* <p>
|
||||
* 设计说明:
|
||||
* - 使用环绕通知
|
||||
* - 支持 SpEL 表达式提取参数和结果
|
||||
* - 支持异步日志发布
|
||||
* - 所有日志异步记录,不阻塞主业务流程
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@@ -43,69 +43,67 @@ import java.util.List;
|
||||
public class BusinessLogAspect {
|
||||
|
||||
@Resource
|
||||
private BusinessLogPublisher businessLogPublisher;
|
||||
private EventLogRecorder eventLogRecorder;
|
||||
|
||||
private final ExpressionParser parser = new SpelExpressionParser();
|
||||
|
||||
@Around("@annotation(businessLog)")
|
||||
public Object around(ProceedingJoinPoint joinPoint, BusinessLog businessLog) throws Throwable {
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 构建日志上下文
|
||||
BusinessLogContext context = buildContext(joinPoint, businessLog);
|
||||
|
||||
Object result = null;
|
||||
Throwable throwable = null;
|
||||
|
||||
try {
|
||||
// 执行目标方法
|
||||
result = joinPoint.proceed();
|
||||
|
||||
// 提取结果信息
|
||||
if (businessLog.includeResult() || !businessLog.result().isEmpty()) {
|
||||
extractResultInfo(context, result, businessLog);
|
||||
}
|
||||
|
||||
// 发布成功日志
|
||||
if (businessLog.async()) {
|
||||
businessLogPublisher.publishSuccess(context);
|
||||
} else {
|
||||
businessLogPublisher.publishSuccess(context);
|
||||
}
|
||||
// 发布日志
|
||||
publishLog(joinPoint, businessLog, result, null);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
throwable = e;
|
||||
|
||||
// 发布失败日志
|
||||
if (businessLog.async()) {
|
||||
businessLogPublisher.publishError(context, e);
|
||||
} else {
|
||||
businessLogPublisher.publishError(context, e);
|
||||
}
|
||||
|
||||
publishLog(joinPoint, businessLog, null, e);
|
||||
throw e;
|
||||
|
||||
} finally {
|
||||
// 记录执行时长
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
context.putExtra("duration", duration);
|
||||
|
||||
// 记录参数信息
|
||||
if (businessLog.includeParams()) {
|
||||
extractParamsInfo(context, joinPoint, businessLog);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建日志上下文
|
||||
* 发布日志
|
||||
*/
|
||||
private BusinessLogContext buildContext(ProceedingJoinPoint joinPoint, BusinessLog businessLog) {
|
||||
LogType type = businessLog.type();
|
||||
LogScope scope = businessLog.scope();
|
||||
private void publishLog(ProceedingJoinPoint joinPoint, BusinessLog businessLog, Object result, Throwable throwable) {
|
||||
try {
|
||||
EventLogRecord record = buildEventLogRecord(joinPoint, businessLog);
|
||||
|
||||
// 设置日志级别
|
||||
if (throwable != null) {
|
||||
record.setLevel(EventLevel.ERROR);
|
||||
} else {
|
||||
record.setLevel(businessLog.level());
|
||||
}
|
||||
|
||||
// 提取关联ID和扩展信息
|
||||
EvaluationContext evalContext = createEvaluationContext(joinPoint, result);
|
||||
extractIds(record, businessLog, evalContext);
|
||||
extractExtraInfo(record, businessLog, evalContext, throwable);
|
||||
|
||||
// 处理摘要模板
|
||||
if (!businessLog.summary().isEmpty()) {
|
||||
String summary = processSummaryTemplate(businessLog.summary(), evalContext);
|
||||
record.setSummary(summary);
|
||||
}
|
||||
|
||||
// 异步记录(record 方法已默认异步)
|
||||
eventLogRecorder.record(record);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("[BusinessLogAspect] 记录日志失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 EventLogRecord
|
||||
*/
|
||||
private EventLogRecord buildEventLogRecord(ProceedingJoinPoint joinPoint, BusinessLog businessLog) {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
String className = method.getDeclaringClass().getSimpleName();
|
||||
@@ -116,98 +114,170 @@ public class BusinessLogAspect {
|
||||
description = className + "." + methodName;
|
||||
}
|
||||
|
||||
BusinessLogContext context = BusinessLogContext.builder()
|
||||
.type(type)
|
||||
.scope(scope)
|
||||
.description(description)
|
||||
// 判断使用新版还是旧版注解
|
||||
String module = !businessLog.module().isEmpty() ? businessLog.module() : "ops";
|
||||
EventDomain domain = businessLog.domain() != EventDomain.SYSTEM ? businessLog.domain() : mapLogTypeToEventDomain(businessLog.type());
|
||||
String eventType = !businessLog.eventType().isEmpty() ? businessLog.eventType() :
|
||||
(businessLog.type() != null ? businessLog.type().getCode() : "UNKNOWN");
|
||||
|
||||
EventLogRecord record = EventLogRecord.builder()
|
||||
.module(module)
|
||||
.domain(domain)
|
||||
.eventType(eventType)
|
||||
.message(description)
|
||||
.build();
|
||||
|
||||
// 添加标签
|
||||
for (String tag : businessLog.tags()) {
|
||||
context.putExtra("tag:" + tag, true);
|
||||
// 添加扩展信息
|
||||
if (businessLog.tags().length > 0) {
|
||||
for (String tag : businessLog.tags()) {
|
||||
record.putPayload("tag:" + tag, true);
|
||||
}
|
||||
}
|
||||
record.putPayload("className", className);
|
||||
record.putPayload("methodName", methodName);
|
||||
record.putPayload("scope", businessLog.scope().getCode());
|
||||
|
||||
// 添加方法信息
|
||||
context.putExtra("className", className);
|
||||
context.putExtra("methodName", methodName);
|
||||
|
||||
return context;
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取参数信息
|
||||
* 提取设备、人员、业务实体ID
|
||||
*/
|
||||
private void extractParamsInfo(BusinessLogContext context, ProceedingJoinPoint joinPoint,
|
||||
BusinessLog businessLog) {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
String[] parameterNames = signature.getParameterNames();
|
||||
Object[] args = joinPoint.getArgs();
|
||||
|
||||
if (parameterNames == null || parameterNames.length == 0) {
|
||||
return;
|
||||
private void extractIds(EventLogRecord record, BusinessLog businessLog, EvaluationContext evalContext) {
|
||||
// 提取设备ID
|
||||
if (!businessLog.deviceId().isEmpty()) {
|
||||
extractId(record, businessLog.deviceId(), evalContext, "deviceId", true);
|
||||
}
|
||||
|
||||
// 提取指定参数
|
||||
// 提取人员ID
|
||||
if (!businessLog.personId().isEmpty()) {
|
||||
extractId(record, businessLog.personId(), evalContext, "personId", true);
|
||||
}
|
||||
|
||||
// 提取业务实体ID
|
||||
if (!businessLog.targetId().isEmpty()) {
|
||||
extractId(record, businessLog.targetId(), evalContext, "targetId", true);
|
||||
}
|
||||
|
||||
// 设置业务实体类型
|
||||
if (!businessLog.targetType().isEmpty()) {
|
||||
record.setTargetType(businessLog.targetType());
|
||||
} else if (businessLog.scope() == LogScope.ORDER) {
|
||||
record.setTargetType("order");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取扩展信息
|
||||
*/
|
||||
private void extractExtraInfo(EventLogRecord record, BusinessLog businessLog,
|
||||
EvaluationContext evalContext, Throwable throwable) {
|
||||
// 提取结果信息
|
||||
if (businessLog.includeResult() && !businessLog.result().isEmpty()) {
|
||||
try {
|
||||
Expression expression = parser.parseExpression(businessLog.result());
|
||||
Object value = expression.getValue(evalContext);
|
||||
if (value != null) {
|
||||
String key = extractKey(businessLog.result());
|
||||
record.putPayload(key, value);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Failed to extract result: {}", businessLog.result());
|
||||
}
|
||||
}
|
||||
|
||||
// 提取自定义参数
|
||||
if (businessLog.params().length > 0) {
|
||||
EvaluationContext evalContext = createEvaluationContext(joinPoint);
|
||||
for (String param : businessLog.params()) {
|
||||
try {
|
||||
Expression expression = parser.parseExpression(param);
|
||||
Object value = expression.getValue(evalContext);
|
||||
String key = extractKey(param);
|
||||
context.putExtra(key, value);
|
||||
if (value != null) {
|
||||
String key = extractKey(param);
|
||||
record.putPayload(key, value);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to extract param: {}", param, e);
|
||||
log.debug("Failed to extract param: {}", param);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 提取所有参数
|
||||
for (int i = 0; i < Math.min(parameterNames.length, args.length); i++) {
|
||||
context.putExtra("param:" + parameterNames[i], args[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加错误信息
|
||||
if (throwable != null) {
|
||||
record.putPayload("errorMessage", throwable.getMessage());
|
||||
record.putPayload("errorClass", throwable.getClass().getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取结果信息
|
||||
* 提取ID值
|
||||
*/
|
||||
private void extractResultInfo(BusinessLogContext context, Object result, BusinessLog businessLog) {
|
||||
if (!businessLog.result().isEmpty()) {
|
||||
// 使用 SpEL 表达式提取
|
||||
EvaluationContext evalContext = new StandardEvaluationContext(result);
|
||||
try {
|
||||
Expression expression = parser.parseExpression(businessLog.result());
|
||||
Object value = expression.getValue(evalContext);
|
||||
String key = extractKey(businessLog.result());
|
||||
context.putExtra(key, value);
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to extract result: {}", businessLog.result(), e);
|
||||
}
|
||||
} else {
|
||||
// 存储完整结果
|
||||
context.putExtra("result", result);
|
||||
}
|
||||
|
||||
// 尝试提取 success 字段
|
||||
if (result != null) {
|
||||
try {
|
||||
EvaluationContext evalContext = new StandardEvaluationContext(result);
|
||||
Expression expression = parser.parseExpression("success");
|
||||
Object success = expression.getValue(evalContext);
|
||||
if (success instanceof Boolean) {
|
||||
context.setSuccess((Boolean) success);
|
||||
private void extractId(EventLogRecord record, String expression, EvaluationContext evalContext,
|
||||
String fieldName, boolean setField) {
|
||||
try {
|
||||
Expression expr = parser.parseExpression(expression);
|
||||
Object value = expr.getValue(evalContext);
|
||||
if (value instanceof Long) {
|
||||
if (setField && "deviceId".equals(fieldName)) {
|
||||
record.setDeviceId((Long) value);
|
||||
} else if (setField && "personId".equals(fieldName)) {
|
||||
record.setPersonId((Long) value);
|
||||
} else if (setField && "targetId".equals(fieldName)) {
|
||||
record.setTargetId((Long) value);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// 没有 success 字段,默认为成功
|
||||
context.setSuccess(true);
|
||||
} else if (value instanceof Integer) {
|
||||
long longValue = ((Integer) value).longValue();
|
||||
if (setField && "deviceId".equals(fieldName)) {
|
||||
record.setDeviceId(longValue);
|
||||
} else if (setField && "personId".equals(fieldName)) {
|
||||
record.setPersonId(longValue);
|
||||
} else if (setField && "targetId".equals(fieldName)) {
|
||||
record.setTargetId(longValue);
|
||||
}
|
||||
} else if (value != null) {
|
||||
record.putPayload(fieldName, value);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Failed to extract {}: {}", fieldName, expression);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理摘要模板
|
||||
* <p>
|
||||
* 支持 {paramName} 占位符替换
|
||||
*/
|
||||
private String processSummaryTemplate(String template, EvaluationContext evalContext) {
|
||||
try {
|
||||
String result = template;
|
||||
int start = template.indexOf('{');
|
||||
while (start >= 0) {
|
||||
int end = template.indexOf('}', start);
|
||||
if (end < 0) break;
|
||||
|
||||
String paramName = template.substring(start + 1, end);
|
||||
try {
|
||||
Expression expression = parser.parseExpression("#" + paramName);
|
||||
Object value = expression.getValue(evalContext);
|
||||
if (value != null) {
|
||||
result = result.replace("{" + paramName + "}", String.valueOf(value));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 保持原样
|
||||
}
|
||||
|
||||
start = template.indexOf('{', end + 1);
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
return template;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 SpEL 评估上下文
|
||||
*/
|
||||
private EvaluationContext createEvaluationContext(ProceedingJoinPoint joinPoint) {
|
||||
private EvaluationContext createEvaluationContext(ProceedingJoinPoint joinPoint, Object result) {
|
||||
StandardEvaluationContext context = new StandardEvaluationContext();
|
||||
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
@@ -220,15 +290,41 @@ public class BusinessLogAspect {
|
||||
}
|
||||
}
|
||||
|
||||
// 添加 result 变量(用于结果提取)
|
||||
if (result != null) {
|
||||
context.setVariable("result", result);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 LogType 映射到 EventDomain
|
||||
*/
|
||||
private EventDomain mapLogTypeToEventDomain(LogType logType) {
|
||||
if (logType == null) {
|
||||
return EventDomain.SYSTEM;
|
||||
}
|
||||
|
||||
switch (logType) {
|
||||
case DISPATCH:
|
||||
return EventDomain.DISPATCH;
|
||||
case DEVICE:
|
||||
return EventDomain.DEVICE;
|
||||
case NOTIFICATION:
|
||||
return EventDomain.DEVICE;
|
||||
case CLEANER:
|
||||
case SYSTEM:
|
||||
case QUEUE:
|
||||
case LIFECYCLE:
|
||||
case TRANSITION:
|
||||
default:
|
||||
return EventDomain.SYSTEM;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从表达式中提取键名
|
||||
* <p>
|
||||
* 示例:
|
||||
* - "#context.orderId" -> "orderId"
|
||||
* - "#result.assigneeId" -> "assigneeId"
|
||||
*/
|
||||
private String extractKey(String expression) {
|
||||
if (expression.contains(".")) {
|
||||
|
||||
Reference in New Issue
Block a user