引入 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
}
返回这个就说明成功了,查看一下服务器日志: