@Auto-Annotation自定义注解——日志记录篇
自定义通用注解连更系列—连载中…
首页介绍:点这里
前言
平时开发中,我们经常需要通过日志或者数据库来记录系统中一些重要的操作,如删除、修改和新增等。但每次在这些方法里手动打印日志或者记录到数据库太过繁琐,并且在代码中看到好多日志打印语句一点都不优雅。
通过自定义注解统一收集日志的方式来实现,则不需要在代码中考虑日志打印的问题,只需要在接口上打一个注解即可。
所需依赖
spring-boot-starter-web
中关联了spring-expression
表达式,可直接引用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.10</version>
</dependency>
操作日志注解集@OperateLogs
考虑到操作日志会存在需要打印多个的情况,所以我采用注解集的方式,支持在接口上打印多个相同注解。
并且支持在注解参数中配置SPEL表达式,动态提取接口参数中的数据。
/** 操作日志注解集
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperateLogs {
OperateLog[] value();
}
/** 操作日志注解
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(OperateLogs.class)
public @interface OperateLog {
/**
* 业务id
*/
String bid();
/**
* 日志标签
*/
String tag();
/**
* 操作类型
*/
OperateTypeEnum operateType();
/**
* 日志消息
*/
String message() default "";
/**
* 操作人员
*/
String operatorId() default "";
/**
* 是否记录结果值
*/
boolean recordResult() default false;
/**
* 结果值
*/
String result() default "";
/**
* 是否在方法执行后记录(默认方法执行前记录)
*/
boolean after() default false;
}
操作日志DTO数据传输对象
/** 操作日志DTO类
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OperateLogDTO {
/**
* 业务id
*/
private String bid;
/**
* 操作类型
*/
private OperateTypeEnum operateType;
/**
* 日志消息
*/
private String message;
/**
* 日志标签
*/
private String tag;
/**
* 操作人员
*/
private String operatorId;
/**
* 是否记录结果值
*/
private Boolean recordResult;
/**
* 结果值
*/
private String result;
/**
* 执行是否成功
*/
private Boolean success = Boolean.FALSE;
/**
* 异常信息
*/
private String exception;
}
日志记录切面类
日志收集的核心在于这个环绕通知的切面类,该通知类主要逻辑在于方法执行前后各自通过SPEL表达式对参数进行解析,获取参数数据,收集起来统一处理。
这里重点强调一个上下文的概念,当参数发生改变,响应信息并为输出时,提供了一个OperateLogContext.setContext("参数名", “参数值”);
的方法可在代码中手动赋值参数到上下文对象,这样在方法执行完后便可获取到最新的参数数据。
/** 应用上下文对象
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
public class OperateLogContext {
private static final ThreadLocal<StandardEvaluationContext> THREAD_LOCAL = new NamedThreadLocal<>("ThreadLocal-StandardEvaluationContext");
public static final String RESULT = "result";
public static StandardEvaluationContext getContext() {
return THREAD_LOCAL.get() == null ? new StandardEvaluationContext() : THREAD_LOCAL.get();
}
public static void setContext(String key, Object value) {
StandardEvaluationContext context = getContext();
context.setVariable(key, value);
THREAD_LOCAL.set(context);
}
public static ThreadLocal<StandardEvaluationContext> getThreadLocal() {
return THREAD_LOCAL;
}
public static void clearContext() {
THREAD_LOCAL.remove();
}
}
/**
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
@Slf4j
@Aspect
@Configuration
public class OperateLogAspect {
private final SpelExpressionParser parser = new SpelExpressionParser();
@Resource
private IOperateLogOutput operateLogOutput;
@Around("@annotation(cn.auto.annotation.OperateLogs) || @annotation(cn.auto.annotation.OperateLog)")
public Object around(ProceedingJoinPoint jp) throws Throwable {
List<OperateLogDTO> operateLogList = new ArrayList<>();
Object result;
OperateLog[] annotations;
MethodSignature signature = (MethodSignature) jp.getSignature();
Method method = signature.getMethod();
try {
//拦截注解
annotations = method.getAnnotationsByType(OperateLog.class);
//方法执行前解析参数
annotationResoleExpression(Boolean.FALSE, jp, operateLogList, null, annotations, signature);
//方法执行
result = jp.proceed();
//方法执行后解析参数
annotationResoleExpression(Boolean.TRUE, jp, operateLogList, result, annotations, signature);
} catch (Throwable throwable) {
//捕获到jp.proceed()方法执行异常,补齐记录方法执行后日志信息(方法未成功执行,后置日志可不收集)
// annotationResoleExpression(jp, operateLogList, result, annotations, signature);
operateLogList.forEach(x -> x.setException(throwable.getMessage()));
throw new ServerException("方法执行异常", throwable);
} finally {
//输出记录日志信息
operateLogOutput.outPut(operateLogList);
}
return result;
}
private void annotationResoleExpression(Boolean isAfter, ProceedingJoinPoint jp, List<OperateLogDTO> operateLogList,
Object result, OperateLog[] annotations, MethodSignature signature) {
String errorStr = "初始化方法执行前后异常内容";
try {
for (OperateLog annotation : annotations) {
if (Boolean.FALSE.equals(isAfter) && !annotation.after()) {
errorStr = "方法执行前解析参数异常";
} else if (Boolean.TRUE.equals(isAfter) && annotation.after()) {
errorStr = "方法执行后解析参数异常";
} else {
continue;
}
setLogContextParam(jp, signature, result);
OperateLogDTO operateLogDTO = resoleExpression(annotation);
operateLogList.add(operateLogDTO);
}
} catch (Exception e) {
log.error(errorStr, e);
}
}
private void setLogContextParam(ProceedingJoinPoint jp, MethodSignature signature, Object result) {
//插入入参
String[] parameterNames = signature.getParameterNames();
Object[] args = jp.getArgs();
for (int i = 0; i < parameterNames.length; i++) {
OperateLogContext.setContext(parameterNames[i], args[i]);
}
//插入结果
if (!ObjectUtils.isEmpty(result)) {
OperateLogContext.setContext(OperateLogContext.RESULT, result);
}
}
private OperateLogDTO resoleExpression(OperateLog annotation) {
StandardEvaluationContext context = OperateLogContext.getContext();
OperateLogDTO operateLogDTO = OperateLogDTO.builder().build();
//解析注解el
String bid = annotation.bid();
if (StringUtils.hasText(bid)) {
Expression expression = parser.parseExpression(bid);
String bidValue = expression.getValue(context, String.class);
operateLogDTO.setBid(bidValue);
}
operateLogDTO.setOperateType(annotation.operateType());
String message = annotation.message();
if (StringUtils.hasText(message)) {
Expression expression = parser.parseExpression(message);
String messageValue = expression.getValue(context, String.class);
operateLogDTO.setMessage(messageValue);
}
String tag = annotation.tag();
if (StringUtils.hasText(tag)) {
Expression expression = parser.parseExpression(tag);
String tagValue = expression.getValue(context, String.class);
operateLogDTO.setTag(tagValue);
}
String operatorId = annotation.operatorId();
if (StringUtils.hasText(operatorId)) {
Expression expression = parser.parseExpression(operatorId);
String operatorIdValue = expression.getValue(context, String.class);
operateLogDTO.setOperatorId(operatorIdValue);
}
if (annotation.recordResult()) {
operateLogDTO.setRecordResult(Boolean.TRUE);
Object resultValue = context.lookupVariable(OperateLogContext.RESULT);
operateLogDTO.setResult(JSON.toJSONString(resultValue));
}
operateLogDTO.setSuccess(Boolean.TRUE);
//清理上下文
OperateLogContext.clearContext();
return operateLogDTO;
}
}
日志输出接口
最终收集到的日志信息可以统一发送给日志输出接口,进行存储或清洗处理
/** 日志输出接口
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
public interface IOperateLogOutput {
/**
* 输出日志信息
* @param operateLogDTOList 日志集
*/
void outPut(List<OperateLogDTO> operateLogDTOList);
}
默认提供一个输出操作日志实现类
- 如想自定义一个输出操作日志实现类,请实现IOperateLogOutput接口,并在类上加上该注解@Primary
- 使用 @Primary 注解:在后一个新写的实现类上添加 @Primary 注解,使其成为首选的实现类。
/**
* 默认输出操作日志实现类
* @Author: 清峰
* @Description: May there be no bug in the world!
*/
@Component
public class OperateLogOutputAction implements IOperateLogOutput{
@Override
public void outPut(List<OperateLogDTO> operateLogDTOList) {
// 自定义输出操作日志实现逻辑...
}
}
标记日志记录接口
@OperateLogs({
@OperateLog(bid = "#user.id",tag = "保存用户信息逻辑",operateType = OperateTypeEnum.UPDATE,message = "'保存用户信息前,名称为:'+#user.name"),
@OperateLog(bid = "#user.id",tag = "保存用户信息逻辑",operateType = OperateTypeEnum.UPDATE,message = "'保存用户信息前,年龄为:'+#user.age",recordResult = true,after = true)
})
@PostMapping("saveUser")
private void saveUser(User user){
System.out.println("保存用户信息逻辑...");
}
总结
通过自定义日志注解的方式,可以通过很优雅的方式对日志信息进行统一收集处理,便捷了开发者的编码效率,同时也可以很方便的统计接口调用情况,异常情况,数据更改情况等。