log aop starter
自定义 SpringBoot3.x Starter 封装 API 请求日志切面业务组件
自定义 SpringBoot Starter
在项目中新建一个module,命名规则为:项目名称-spring-boot-starter-要做的业务,选择Maven Archetype里面的quickstart
-
选择
Maven Archetype来创建一个Maven子模块 -
JDK:我这里时 17
-
选择
maven-archetype-quickstart -
打包方式为
jar -
主要依赖为
spring-boot-starter-aop
<dependencies>
<!-- 此时你可能有别的 common 模块 -->
<!-- AOP 切面 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>将quickstart下载下来的一些不需要的类删除,比如App.java和测试包里的AppTest.java
添加 JSON 工具类
当前添加的是一个日志组件,这需要以json的 格式打印出参,要封装一个Json工具类,添加Jackson相关的依赖,
统一管理版本号为:2.16.1
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>添加完成后,我们通常是在common模块里引入对应的依赖
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<!-- 解决 Jackson Java 8 新日期 API 的序列化问题 -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>使用日志切面打印出参时,如果出参中含有Java 8新的日期API,如LocalDatetime,可能会遇到如下异常
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Java 8 date/time type `java.time.LocalDateTime` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling (through reference chain: com.quanxiaoha.framework.common.response.Response["data"]->com.quanxiaoha.xiaohashu.auth.controller.User["createTime"])这是由于Jackson本身不支持新的日期API,需要使用Jackson-datatype-jsr310库来解决此问题。
在common模块里新建一个util的工具包,用于统一存放相关的工具类,并创建JsonUtils.java
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.SneakyThrows;
public class JsonUtils {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
static {
OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
OBJECT_MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
OBJECT_MAPPER.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化问题
}
/**
* 将对象转换为 JSON 字符串
* @param object 对象
* @return JSON 字符串
*/
@SneakyThrows
public static String toJsonString(Object object) {
return OBJECT_MAPPER.writeValueAsString(object);
}
}- 创建了一个私有的静态不可变的
ObjectMapper实例,ObjectMapper是Jackson库中用于序列化和反序列化JSON的核心类 static {}静态化初始块,用于在类加载时执行一些初始化操作。在这里,OBJECT_MAPPER被配置以在反序列化时忽略未知属性和在序列化时忽略空的Java Bean属性,并注册了一个JavaTimeModule模块,用于解决LocalDateTime类型的序列化问题toJsonString这是一个公共静态方法,用于将给定的Java对象序列化为JSON字符串,它接收一个Object类型的参数,并转换为JSON字符串返回@SneakyThrows:这是lombok的一个注解,用于简化异常处理。它会将被标注的方法中的受检异常转换为不受检异常,使得代码看起来更加简洁
添加日志切面
在组件模块中,创建aspect包,用于放置切面相关的类,在里面添加自定义注解及切面
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface ApiOperationLog {
/**
* API 功能描述
*/
String description() default "";
}import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import top.wjstar.framework.common.util.JsonUtils;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.function.Function;
import java.util.stream.Collectors;
@Aspect
@Slf4j
public class ApiOperationLogAspect {
/**
* 以自定义 @ApiOperationLog 注解为切点,凡是添加 @ApiOperationLog 的方法,都会仔细环绕中的代码
*/
@Pointcut("@annotation(xxx.xxx.framework.biz.operationlog.aspect.ApiOperationLog)")
public void apiOperationLog() {}
/**
* 环绕
* @param joinPoint 切点
* @return
* @throws Throwable
*/
@Around("apiOperationLog()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 请求开始时间
long startTime = System.currentTimeMillis();
// 获取被请求的类和方法
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, JsonUtils.toJsonString(result));
return result;
}
/**
* 获取注解的描述信息
* @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 operationLog = method.getAnnotation(ApiOperationLog.class);
// 4. 从 LogExecution 注解中获取 description 属性
return operationLog.description();
}
private Function<Object, String> toJsonStr() {
return JsonUtils::toJsonString;
}
}starter 自动配置
新建一个config包,创建日志切面自动配置类
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import top.wjstar.framework.biz.operationlog.aspect.ApiOperationLogAspect;
@AutoConfiguration
public class ApiOperationLogAutoConfiguration {
@Bean
public ApiOperationLogAspect apiOperationLogAspect() {
return new ApiOperationLogAspect();
}
}自动配置类,用于配置 API 操作日志记录功能,并且通过@Bean注解的方法来创建一个 ApiOperationLogAspect实例,以实现注入到Spring容器中
接着在/main文件夹下创建/resources包,再创建/META_INF文件夹,在里面创建/spring文件夹,以及org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,注意,这个是自定义starter的固定捕捉,需要严格按照此格式来书写。
文件里面填写ApiOperationLogAutoConfiguration配置类的完整包路径
xxx.xxx.framework.biz.operationlog.config.ApiOperationLogAutoConfiguration创建的imports文件前面必须有小绿叶标识,如果不是,可能导致自定义的starter被idea无法识别;到此,自定义starter步骤就算完成了。
统一版本控制
如果我们想在别的模块里使用,则必须在最外面的pom.xml文件里声明该依赖以及版本号
<dependencyManagement>
<dependencies>
<!-- 业务接口日志组件 -->
<dependency>
<groupId>xxx.xxxx</groupId>
<artifactId>xxxxx-spring-boot-starter-biz-operationlog</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>使用 starter
在别的模块里编辑pom.xml引入日志切面依赖
<dependencies>
// 省略...
<!-- 业务接口日志组件 -->
<dependency>
<groupId>xxx.xxxx</groupId>
<artifactId>xxxx-spring-boot-starter-biz-operationlog</artifactId>
</dependency>
</dependencies>为测试接口添加注解
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import top.wjstar.framework.biz.operationlog.aspect.ApiOperationLog;
import top.wjstar.framework.common.response.Response;
@RestController
public class TestController {
@GetMapping("/test")
@ApiOperationLog(description = "测试接口")
public Response<String> test() {
return Response.success("success");
}
}重启项目,调用接口,自测一波,看看日志切面是否正常工作。
====== 请求开始: [测试接口], 入参: , 请求类: TestController, 请求方法: test ===================================
2025-05-24T15:36:34.340+08:00 INFO 80306 --- [nio-8080-exec-1] t.w.f.b.o.aspect.ApiOperationLogAspect : ====== 请求结束: [测试接口], 耗时: 1ms, 出参: {"success":true,"message":null,"errorCode":null,"data":"success"} ===================================测试日期 API
模拟一个用户实体类
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User {
private String nickname;
private LocalDateTime createTime;
}import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import top.wjstar.framework.biz.operationlog.aspect.ApiOperationLog;
import top.wjstar.framework.common.response.Response;
import java.time.LocalDateTime;
@RestController
public class TestController {
@GetMapping("/test")
@ApiOperationLog(description = "测试接口")
public Response<String> test() {
return Response.success("success");
}
@GetMapping("/test2")
@ApiOperationLog(description = "测试接口2")
public Response<User> test2() {
return Response.success(User
.builder()
.nickname("无解")
.createTime(LocalDateTime.now())
.build());
}
}接口访问
{
"success": true,
"message": null,
"errorCode": null,
"data": {
"nickname": "无解",
"createTime": "2025-05-24T15:39:22.513713"
}
}====== 请求开始: [测试接口2], 入参: , 请求类: TestController, 请求方法: test2 ===================================
2025-05-24T15:39:22.527+08:00 INFO 80439 --- [nio-8080-exec-1] t.w.f.b.o.aspect.ApiOperationLogAspect : ====== 请求结束: [测试接口2], 耗时: 1ms, 出参: {"success":true,"message":null,"errorCode":null,"data":{"nickname":"无解","createTime":[2025,5,24,15,39,22,513713000]}} ===================================能正常打印,没出现异常,但是格式不太好
适配日期序列化格式
在common包里新增常量包和常量接口
public interface DateConstants {
/**
* 年-月-日 时:分:秒
*/
String Y_M_D_H_M_S_FORMAT = "yyyy-MM-dd HH:mm:ss";
}调整JsonUtils
static {
OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
OBJECT_MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// JavaTimeModule 用于指定序列化和反序列化规则
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DateConstants.Y_M_D_H_M_S_FORMAT)));
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DateConstants.Y_M_D_H_M_S_FORMAT)));
OBJECT_MAPPER.registerModules(javaTimeModule); // 解决 LocalDateTime 的序列化问题
}重启后查看日志记录
====== 请求结束: [测试接口2], 耗时: 1ms, 出参: {"success":true,"message":null,"errorCode":null,"data":{"nickname":"无解","createTime":"2025-05-24 15:46:48"}} ===================================现在可以看到打印日期格式友好很多了。