自定义 Spring Boot 3.x Starter: 封装 API 请求日志切面业务组件
友情提示 : 推荐使用谷歌浏览器来阅读本专栏,其他浏览器可能存在兼容性问题。
本小节中,我们将自定义一个 Spring Boot Starter
组件,将 API 请求日志切面功能封装进去,后续新建新的服务时,只需添加这个 starter
, 即可拿来即用。关于接口日志切面,不清楚的小伙伴,可翻阅星球第一个项目:《Spring Boot 自定义注解,实现 API 请求日志切面》 小节。
1. 什么是 Spring Boot Starter ?
Spring Boot Starter 就像是一个“工具包”,里面已经包含了你所需要的东西。它们把一些常用的功能和技术打包好了,比如处理数据库、处理 Web 请求等等。你只需要在你的项目中引入这些 Starter,它们就会自动帮你配置好所需的依赖项和参数。这样,你就可以省去很多繁琐的配置工作。
举个栗子,当你想要使用 Spring Boot 开发一个 Web 应用时,只需要引入 spring-boot-starter-web
Starter。这个 Starter 包含了一系列依赖项和配置,使得开发 Web 应用变得更加简单。
具体来说,引入 spring-boot-starter-web
Starter 后,你可以享受到以下好处:
- 内嵌的 Web 服务器支持:Spring Boot 内置了多种 Web 服务器支持,比如 Tomcat、Jetty、Undertow。
spring-boot-starter-web
会自动配置一个默认的 Web 服务器,你无需手动配置即可启动你的应用。 - Spring MVC 框架支持:Spring Boot 基于 Spring MVC 构建了强大的 Web 开发框架,包括了控制器、视图解析器等。引入
spring-boot-starter-web
后,你可以直接使用 Spring MVC 来处理 Web 请求。 - 静态资源支持:
spring-boot-starter-web
Starter 自动配置了对静态资源(如 HTML、CSS、JavaScript 文件)的处理,你可以直接在项目中放置这些文件,Spring Boot 就能够正确地访问它们。 - 自动配置:Spring Boot 会根据你的 classpath 自动配置应用程序。比如,如果你引入了
spring-boot-starter-web
,Spring Boot 就会自动配置 DispatcherServlet、ViewResolver 等关键组件,从而让你的 Web 应用能够顺利地工作起来。
简而言之,Spring Boot Starter 是一种方便的方式,让你能够更快、更轻松地开始使用 Spring Boot 框架,并集成各种常用功能和技术。
2. 自定义 Spring Boot Starter
在 xiaoha-framewrok
模块上,继续右键 | New | Module... , 为基础设施层添加新的子模块 —— 业务日志切面组件:
填写相关选项,如下图所示:
解释一下标注的部分:
- ①:选择
Maven Archetype
来创建一个Maven
子模块;- ②:项目名称填写
xiaoha-spring-boot-starter-biz-operationlog
;- ③:项目使用的
JDK
版本,本项目使用的是JDK 17
;- ④:父模块选择
xiaoha-framework
;- ⑤:选择
Internal
。- ⑥:选择
maven-archetype-quickstart
。- ⑦:填写
com.quanxiaoha.framework.biz.operationlog
;
点击 Create 按钮,开始创建子模块,创建完成后,查看 xiaoha-framework
模块的 pom.xml
文件,可以看到 <modules>
节点下自动添加好了该子模块:
<modules>
<!-- 通用工具模块 -->
<module>xiaoha-common</module>
<!-- 接口日志组件 -->
<module>xiaoha-spring-boot-starter-biz-operationlog</module>
</modules>
编辑切面日志组件的 pom.xml
文件,内容如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- 指定父项目 -->
<parent>
<groupId>com.quanxiaoha</groupId>
<artifactId>xiaoha-framework</artifactId>
<version>${revision}</version>
</parent>
<!-- 指定打包方式 -->
<packaging>jar</packaging>
<artifactId>xiaoha-spring-boot-starter-biz-operationlog</artifactId>
<name>${project.artifactId}</name>
<description>接口日志组件</description>
<dependencies>
<dependency>
<groupId>com.quanxiaoha</groupId>
<artifactId>xiaoha-common</artifactId>
</dependency>
<!-- AOP 切面 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
</project>
- 指定父项目为
xiaoha-framework
;- 指定打包方式为
Jar
,以及填写此模块的功能描述 ;- 添加
xiaoha-common
通用模块依赖,以及 AOP 切面依赖;
接着,将一些不需要的类删除掉:
3. 添加 JSON 工具类
由于在日志切面 starter
组件中,需要以 json
的格式打印出参,所以,我们需要先封装一个 Json
工具类。编辑最外层的 pom.xml
, 声明 Jackson
相关依赖,统一管理版本号:
<properties>
// 省略...
<!-- 依赖包版本 -->
// 省略...
<jackson.version>2.16.1</jackson.version>
</properties>
<!-- 统一依赖管理 -->
<dependencyManagement>
<dependencies>
// 省略...
<!-- 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>
</dependencies>
</dependencyManagement>
添加完成后,编辑 xiaoha-common
公共模块的 pom.xml
, 添加 Jackson
相关依赖:
<dependencies>
// 省略...
<!-- 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>
</dependencies>
TIP : 在开发星球第一个项目 中,日志切面打印出参时,如果出参中含有
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
库来解决此问题。
在 xiaoha-common
模块中,新建一个 util
工具包,用于统一放置相关工具类,并创建 JsonUtils
, 如下图所示:
代码如下:
package com.quanxiaoha.framework.common.util;
import com.fasterxml.jackson.annotation.JsonInclude;
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 obj
* @return
*/
@SneakyThrows
public static String toJsonString(Object obj) {
return OBJECT_MAPPER.writeValueAsString(obj);
}
}
解释一波:
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
: 创建了一个私有的静态不可变的ObjectMapper
实例,ObjectMapper
是 Jackson 库中用于序列化和反序列化 JSON 的核心类。static { ... }
: 这是一个静态初始化块,用于在类加载时执行一些初始化操作。在这里,OBJECT_MAPPER
被配置以在反序列化时忽略未知属性和在序列化时忽略空的 Java Bean 属性,并且注册了一个JavaTimeModule
模块,用于解决LocalDateTime
类型的序列化问题。public static String toJsonString(Object obj) { ... }
: 这是一个公共静态方法,用于将给定的 Java 对象序列化为 JSON 字符串。它接受一个Object
类型的参数obj
,并使用OBJECT_MAPPER
将其转换为 JSON 字符串并返回。@SneakyThrows
: 这是 Lombok 提供的一个注解,用于简化异常处理。它会将被标注的方法中的受检异常转换为不受检异常,使得代码看起来更加简洁。
4. 添加日志切面
Json
工具类编写完成后,回到日志切面 starter
组件中,创建 /aspect
包,用于放置切面相关的类,在里面添加自定义注解以及切面,这块的代码在第一个项目中已经讲解过了,不做赘述,直接拿过用就行:
代码如下:
package com.quanxiaoha.framework.biz.operationlog.aspect;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Documented
public @interface ApiOperationLog {
/**
* API 功能描述
*
* @return
*/
String description() default "";
}
package com.quanxiaoha.framework.biz.operationlog.aspect;
import com.quanxiaoha.framework.common.util.JsonUtils;
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 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(com.quanxiaoha.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 apiOperationLog = method.getAnnotation(ApiOperationLog.class);
// 4. 从 LogExecution 注解中获取 description 属性
return apiOperationLog.description();
}
/**
* 转 JSON 字符串
* @return
*/
private Function<Object, String> toJsonStr() {
return JsonUtils::toJsonString;
}
}
注意:这个类和前一个项目区别是,去除掉了
TraceId
以及@Component
注解。
5. starter 自动配置
接下来,就是自定义 starter
的重头戏 —— 自动配置。新建一个 /config
包,并创建日志切面自动配置类,如下图所示:
代码如下:
package com.quanxiaoha.framework.biz.operationlog.config;
import com.quanxiaoha.framework.biz.operationlog.aspect.ApiOperationLogAspect;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
@AutoConfiguration
public class ApiOperationLogAutoConfiguration {
@Bean
public ApiOperationLogAspect apiOperationLogAspect() {
return new ApiOperationLogAspect();
}
}
这是一个自动配置类,用于配置 API 操作日志记录功能,并且通过
@Bean
注解的apiOperationLogAspect()
方法来创建一个ApiOperationLogAspect
实例,以实现注入到 Spring 容器中。
接着,在 /main
文件夹下,创建 /resources
包,再创建 /META-INF
文件夹,再在里面创建 /spring
文件夹 , 以及 org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件,注意,这是自定义 starter
固定步骤,需要严格按照此格式来书写,如下图所示:
imports
文件中内容如下,填写 ApiOperationLogAutoConfiguration
配置类的完整包路径:
com.quanxiaoha.framework.biz.operationlog.config.ApiOperationLogAutoConfiguration
TIP : 创建的
imports
文件,必须保证前面有小绿叶标识,如果不是,可能导致自定义starter
被 IDEA 无法识别的问题:如果你手动创建的文件,没有小绿叶标识,可以下载本小节的源码,通过 IDEA 打开后,直接将
/resouces
目录复制到你的项目中,正常都会有小绿叶的。
至此,自定义 starter
步骤就完成了。
6. 统一版本控制
接下来,我们想在 xiaoha-auth
认证服务中使用刚刚封装好的 starter
组件。回到最外层的 pom.xml
, 声明该组件依赖以及版本号:
// 省略...
<!-- 统一依赖管理 -->
<dependencyManagement>
<dependencies>
// 省略...
<!-- 业务接口日志组件 -->
<dependency>
<groupId>com.quanxiaoha</groupId>
<artifactId>xiaoha-spring-boot-starter-biz-operationlog</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>
// 省略...
7. 使用 starter
接着,编辑 xiaoha-auth
认证服务的 pom.xml
, 添加日志切面 starter
的依赖, 代码如下:
<dependencies>
// 省略...
<!-- 业务接口日志组件 -->
<dependency>
<groupId>com.quanxiaoha</groupId>
<artifactId>xiaoha-spring-boot-starter-biz-operationlog</artifactId>
</dependency>
</dependencies>
若出现爆红问题,点击右侧 Reload 按钮,重新刷新一下 Maven 依赖。最后,为 /test
接口添加 @ApiOperationLog
日志切面注解:
package com.quanxiaoha.xiaohashu.auth.controller;
import com.quanxiaoha.framework.biz.operationlog.aspect.ApiOperationLog;
import com.quanxiaoha.framework.common.response.Response;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@GetMapping("/test")
@ApiOperationLog(description = "测试接口")
public Response<String> test() {
return Response.success("Hello, 犬小哈专栏");
}
}
重启项目, 浏览器访问 localhost:8080/test
接口,自测一波,看看日志切面是否能够正常工作:
OK , 没有任何问题,自定义的日志切面 starter
工作良好!
8. 测试一下,切面打印出参中包含 Java 8 新日期 API
上面我们讲到,通过 jackson-datatype-jsr310
适配了 Jackson
不支持 Java 8 新日志 API 问题。这块也需要单独测试一下:
创建一个 User
用户实体类,并添加两个字段,包含一个 LocalDateTime
日期字段,代码如下:
package com.quanxiaoha.xiaohashu.auth.controller;
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;
}
编辑 TestController
控制器,新增一个 /test2
接口,代码如下:
package com.quanxiaoha.xiaohashu.auth.controller;
import com.quanxiaoha.framework.biz.operationlog.aspect.ApiOperationLog;
import com.quanxiaoha.framework.common.response.Response;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
@RestController
public class TestController {
// 省略..
@GetMapping("/test2")
@ApiOperationLog(description = "测试接口2")
public Response<User> test2() {
return Response.success(User.builder()
.nickName("犬小哈")
.createTime(LocalDateTime.now())
.build());
}
}
重启项目,浏览器访问: http://localhost:8080/test2 , 观察控制台日志,如下:
可以看到,即使出参对象中包含 Java 8 新日期 API 字段, 也是正常的,没有出现报异常情况。就是日期序列化格式不太友好,如上图所示。
9. 适配日期序列化格式
为了解决上述问题,我们需要手动指定 Jackson
日期的序列化和反序列化规则。在 xiaoha-common
通用模块中,新建包 /constant
全局常量包, 并创建 DateConstants
日期常量接口,代码如下:
public interface DateConstants {
/**
* 年-月-日 时:分:秒
*/
String Y_M_D_H_M_S_FORMAT = "yyyy-MM-dd HH:mm:ss";
}
接着,编辑 JsonUtils
工具类,手动配置 LocalDateTime
日期格式化规则:
static {
OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
OBJECT_MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
// JavaTimeModule 用于指定序列化和反序列化规则
JavaTimeModule javaTimeModule = new JavaTimeModule();
// 支持 LocalDateTime
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 的序列化问题
}
再次重启项目,测试 /test2
接口,观察控制台日志,现在看到日期打印格式就友好多了~
10. 结语
本小节中,我们了解了什么是 Spring Boot Starter
, 并自己动手实现了一个日志切面 starter
组件,后续创建新的服务时,只需添加该 starter
组件, 然后,为想要打印接口出入参的接口,添加 @ApiOperationLog
注解,即可快速使用上日志切面功能了,非常方便,有木有!