@Auto-Annotation自定义注解——日志记录篇

@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("保存用户信息逻辑...");
    }

总结

​ 通过自定义日志注解的方式,可以通过很优雅的方式对日志信息进行统一收集处理,便捷了开发者的编码效率,同时也可以很方便的统计接口调用情况,异常情况,数据更改情况等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值