Spring Boot 自定义注解,实现 API 请求日志切面

什么是自定义注解 (Custom Annotations)?

Java 注解是从 Java 5 开始引入的,它为我们提供了一种元编程的方法,允许我们在不改变代码逻辑的情况下为代码添加元数据。这些元数据可以在编译时或运行时通过反射被访问。

自定义注解就是用户定义的,用于为代码提供元数据的注解。例如,本小节中自定义的 @ApiOperationLog 注解,它用来表示一个方法在执行时需要被记录日志。

什么是 AOP (面向切面编程)?

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

主要概念包括:

  • 切点 (Pointcuts): 定义在哪里应用切面(即在哪里插入横切关注点的代码)。
  • 通知 (Advices): 定义在特定切点上要执行的代码。常见的通知类型有:前置通知、后置通知、环绕通知等。
  • 切面 (Aspects): 切面将切点和通知结合起来,定义了在何处和何时应用特定的逻辑。

例如,使用AOP,我们可以为所有使用 @ApiOperationLog 注解的方法自动添加日志逻辑,而不需要在每个方法中手动添加。

添加依赖

在项目  中的 pom.xml 文件中,添加 jackson 工具,它用于将出入参转为 json 字符串:

<!-- 版本号统一管理 -->
<properties>
    ...省略
    <jackson.version>2.15.2</jackson.version>
</properties>

<!-- 统一依赖管理 -->
    <dependencyManagement>
        <dependencies>
            ...省略

            <!-- Jackson -->
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>${jackson.version}</version>
            </dependency>

        </dependencies>
    </dependencyManagement>

因为日志切面属于前台、后台管理接口通用的功能,所以和该功能相关代码可以统一放置于 xx-module-common 公共模块中。

打开 xx-module-common 公共模块中的 pom.xml , 引用具体依赖:

<dependencies>
		...省略

		<!-- 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>
	</dependencies>

自定义注解

xx-module-common 公共通用模块下,新建一个名为 aspect 的包,用于放置切面相关的功能类,接着,在其中创建一个名为 ApiOperationLog 的自定义注解:

package com.yanxiaosheng.xx.common.aspect;

import java.lang.annotation.*;

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

}

元注解说明:

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

日志切面

aspectj 注解说明

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

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

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

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

在定义日志切面之前,我们先来创建一个 JSON 工具类,这在日志切面中打印出入参为 JSON 字符串会用到。在 xx-module-common 通过通用模块下,创建一个 utils 包,用于统一放置工具类相关,然后,新建一个名为 JsonUtil 的工具类, 代码如下:

package com.yanxiaosheng.xx.common.utils;

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

/**
 * @author: 闫小生
 * @date: 2023-08-14 16:27
 * @description: JSON 工具类
 **/
@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();
        }
    }
}

上面的代码中,我们使用了 Spring Boot 内置的 JSON 工具Jackson , 同时,创建了一个静态的 ObjectMapper 类,并写个一个 toJsonString 方法,用于将传入的对象打印成 JSON 字符串。

定义日志切面类

工具类搞定后,在 aspect 包下,新建切面类 ApiOperationLogAspect , 代码如下,附有详细注释:

package com.yanxiaosheng.xx.common.aspect;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.quanxiaoha.weblog.common.utils.JsonUtil;
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 ApiOperationLogAspect {

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

    /**
     * 环绕
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around("apiOperationLog()")
    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 = getApiOperationLogDescription(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 getApiOperationLogDescription(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);
    }

}

新增 TestController 类,定义一个 POST 格式,路径为 /test 接口,代码如下:

package com.yanxiaosheng.xx.web.controller;

import com.yanxiaosheng.xx.web.model.User;
import com.yanxiaosheng.xx.common.aspect.ApiOperationLog;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: 闫小生
 * @date: 2023-08-10 10:34
 * @description: TODO
 **/
@RestController
@Slf4j
public class TestController {

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

}

  • 18
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它提供了许多便捷的功能和特性,其中包括自定义注解自定义注解是在Java中使用元数据的一种方式,它允许开发人员在代码中添加自己定义的标记,以便在运行时使用。在Spring Boot中,自定义注解可以用于各种场景,例如配置类、AOP切面请求处理等。 要创建一个自定义注解,可以使用Java的注解声明语法,并通过元注解来指定注解的行为。下面是一个示例: ```java import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CustomAnnotation { String value() default ""; } ``` 在上面的示例中,我们创建了一个名为`CustomAnnotation`的自定义注解。通过`@Target`元注解,我们指定了该注解可以应用于方法上。通过`@Retention`元注解,我们指定了该注解在运行时可见。 接下来,我们可以在Spring Boot应用程序中使用这个自定义注解。例如,我们可以将它应用于一个Controller的方法上: ```java @RestController public class MyController { @GetMapping("/") @CustomAnnotation("Hello") public String hello() { return "Hello, World!"; } } ``` 在上面的示例中,我们在`hello()`方法上使用了`@CustomAnnotation`注解,并传入了一个值"Hello"。在运行时,我们可以通过反射获取到这个注解,并根据注解的值执行相应的逻辑。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值