SpringBoot使用自定义注解、AOP实现API接口日志记录

2023-12-14 00:06:05

? ? ? ? 在后端服务中,对接口的每次请求入参、方法、出参、执行耗时等信息进行日志打印或记录,是很有必要的。有了日志记录,在后端服务出现问题的时候,可以快速定位问题所在,或者可以通过日志异常快速提醒相关人员进行服务维护。

自定义注解

? ? ? ? Java注解是从JDK5开始引入的,通过它提供的元注解我们可以自定义符合自己使用的自定义注解。

主要元注解说明

  • @Retention(RetentionPolicy.RUNTIME): 这个元注解用于指定注解的保留策略,即注解在何时生效。RetentionPolicy.RUNTIME?表示该注解将在运行时保留,这意味着它可以通过反射在运行时被访问和解析。
  • @Target({ElementType.METHOD}): 这个元注解用于指定注解的目标元素,即可以在哪些地方使用这个注解。ElementType.METHOD?表示该注解只能用于方法上。这意味着您只能在方法上使用这个特定的注解。
  • @Documented: 这个元注解用于指定被注解的元素是否会出现在生成的Java文档中。如果一个注解使用了?@Documented,那么在生成文档时,被注解的元素及其注解信息会被包含在文档中。这可以帮助文档生成工具(如 JavaDoc)在生成文档时展示关于注解的信息。

AOP

? ? ? ? AOP是一种编程范式,它提供了一种能力,让开发者能够模块化跨多个对象的横切关注点(例如日志、事务管理等)

aspectj 注解说明

在配置 AOP 切面之前,我们需要了解下?aspectj?相关注解的作用:

  • @Aspect:声明该类为一个切面类;
  • @Pointcut:定义一个切点,后面跟随一个表达式,表达式可以定义为切某个注解,也可以切某个 package 下的方法;

切点定义好后,就是围绕这个切点做文章了:

  • @Before: 在切点之前,织入相关代码;
  • @After: 在切点之后,织入相关代码;
  • @AfterReturning: 在切点返回内容后,织入相关代码,一般用于对返回值做些加工处理的场景;
  • @AfterThrowing: 用来处理当织入的代码抛出异常后的逻辑处理;
  • @Around: 环绕,可以在切入点前后织入代码,并且可以自由的控制何时执行切点;

一、添加依赖

? ? ? ? 关键在于AOP依赖,Jackson依赖主要用于将出入参转为json字符串

        <!-- AOP 切面 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-aop</artifactId>
		</dependency>

		<!-- Jackson -->
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
		</dependency>

二、自定义注解

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface ApiLog {
    /**
     * API 功能描述
     *
     * @return
     */
    String description() default "";

}

三、定义JSON工具类

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class JsonUtil {

    private static final ObjectMapper INSTANCE = new ObjectMapper();

    public static String toJsonString(Object obj) {
        try {
            return INSTANCE.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            return obj.toString();
        }
    }
}

四、定义日志切面类

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;

@Aspect
@Component
@Slf4j
public class ApiLogAspect {

    /** 以自定义 @ApiLog 注解为切点,凡是添加 @ApiLog 的方法,都会执行环绕中的代码 */
    @Pointcut("@annotation(com.smiling.aspect.ApiOperationLog)")
    public void apiLog() {}

    /**
     * 环绕
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("apiLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        try {
            // 请求开始时间
            long startTime = System.currentTimeMillis();

            // MDC
            MDC.put("traceId", UUID.randomUUID().toString());

            // 获取被请求的类和方法
            String className = joinPoint.getTarget().getClass().getSimpleName();
            String methodName = joinPoint.getSignature().getName();

            // 请求入参
            Object[] args = joinPoint.getArgs();
            // 入参转 JSON 字符串
            String argsJsonStr = Arrays.stream(args).map(toJsonStr()).collect(Collectors.joining(", "));

            // 功能描述信息
            String description = getApiLogDescription(joinPoint);

            // 打印请求相关参数
            log.info("====== 请求开始: [{}], 入参: {}, 请求类: {}, 请求方法: {} =================================== ",
                    description, argsJsonStr, className, methodName);

            // 执行切点方法
            Object result = joinPoint.proceed();

            // 执行耗时
            long executionTime = System.currentTimeMillis() - startTime;

            // 打印出参等相关信息
            log.info("====== 请求结束: [{}], 耗时: {}ms, 出参: {} =================================== ",
                    description, executionTime, JsonUtil.toJsonString(result));

            return result;
        } finally {
            MDC.clear();
        }
    }

    /**
     * 获取注解的描述信息
     * @param joinPoint
     * @return
     */
    private String getApiLogDescription(ProceedingJoinPoint joinPoint) {
        // 1. 从 ProceedingJoinPoint 获取 MethodSignature
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();

        // 2. 使用 MethodSignature 获取当前被注解的 Method
        Method method = signature.getMethod();

        // 3. 从 Method 中提取 LogExecution 注解
        ApiOperationLog apiOperationLog = method.getAnnotation(ApiOperationLog.class);

        // 4. 从 LogExecution 注解中获取 description 属性
        return apiOperationLog.description();
    }

    /**
     * 转 JSON 字符串
     * @return
     */
    private Function<Object, String> toJsonStr() {
        return arg -> JsonUtil.toJsonString(arg);
    }

}

五、使用

在需要进行日志记录的接口上使用自定义注解@ApiLog即可

@RestController
@Slf4j
public class TestController {

    @PostMapping("/test")
    @ApiLog(description = "测试接口")
    public User test(@RequestBody User user) {
        // 返参
        return user;
    }

}

文章来源:https://blog.csdn.net/qq_40386113/article/details/134850544
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。