引入 jackson 和 aop 依赖

在父项目 weblog-springboot 中的 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>

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

打开 weblog-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>

添加自定义AOP注解接口

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

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)在生成文档时展示关于注解的信息

添加 JSON 工具类

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

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
 
/**
 * @author: 犬小哈
 * @url: www.quanxiaoha.com
 * @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 , 代码如下,附有详细注释:

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);
    }
 
}

配置包扫描

在启动类 WeblogWebApplication 中,手动添加包扫描 @ComponentScan , 指定扫描 com.quanxiaoha.weblog 包下面的所有类:

package com.quanxiaoha.weblog.web;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
 
@SpringBootApplication
@ComponentScan({"com.quanxiaoha.weblog.*"}) // 多模块项目中,必需手动指定扫描 com.quanxiaoha.weblog 包下面的所有类
public class WeblogWebApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(WeblogWebApplication.class, args);
    }
 
}

测试

@RestController  
@Slf4j  
public class TestController {  
  
    @PostMapping("/test")  
    @ApiOperationLog(description = "测试接口")  
    public User test(User user) {  
        return user;  
    }}

在api请求中设置 ContextType:application/json ,并且设置对应User的参数即可。

{
 
    "name": "admin",
 
    "sex": 1
 
}

返回这个就说明成功了,查看一下服务器日志: