自定义 Spring Boot Starter: 封装 API 请求日志切面业务组件

本小节中,我们将自定义一个 Spring Boot Starter 组件,将 API 请求日志切面功能封装进去,后续新建新的服务时,只需添加这个 starter , 即可拿来即用。

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

hanserwei-framewrok 模块上,继续右键 | New | Module... , 为基础设施层添加新的子模块 —— 业务日志切面组件

image-20260202223947755

填写相关选项,如下图所示:

image-20260202224026442

点击 Create 按钮,开始创建子模块,创建完成后,查看 hanserwei-framework 模块的 pom.xml 文件,可以看到 <modules> 节点下自动添加好了该子模块:

image-20260202224126682

编辑切面日志组件的 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.hanserwei</groupId>
        <artifactId>hanserwei-framework</artifactId>
        <version>${revision}</version>
    </parent>

    <!-- 当前模块的artifactId -->
    <artifactId>hanserwei-spring-boot-starter-biz-operationlog</artifactId>
    <!-- 打包方式:jar包 -->
    <packaging>jar</packaging>

    <!-- 项目名称 -->
    <name>hanserwei-spring-boot-starter-biz-operationlog</name>
    <!-- 项目描述:接口日志组件 -->
    <description>接口日志组件</description>

    <!-- 项目属性配置 -->
    <properties>
        <!-- 项目构建时的源码编码格式 -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <!-- 项目依赖 -->
    <dependencies>
        <!-- 公共工具依赖 -->
        <dependency>
            <groupId>com.hanserwei</groupId>
            <artifactId>hanserwei-common</artifactId>
        </dependency>

        <!-- Spring Boot AOP切面支持,用于实现操作日志的切面拦截 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aspectj</artifactId>
        </dependency>
    </dependencies>
</project>

注意:在Spring Boot 4中AOP的Starter名称变了!

image-20260202224427483

接着,将一些不需要的类删除掉:

image-20260202224530116

3. 添加 JSON 工具类

由于在日志切面 starter 组件中,需要以 json 的格式打印出参,所以,我们需要先封装一个 Json 工具类。由于我们在最外层已经导入了pom文件,里面已经定义了Jackson的版本信息。所有我们不用再显示指定!

编辑 hanserwei-common 公共模块的 pom.xml, 添加 Jackson 相关依赖:

image-20260202224852666

注意:Jackson3的包名变了而且有关Java8的新日期API也已经合并到Jackson3中了,不用再额外引入了!

hanserwei-common 模块中,新建一个 util 工具包,用于统一放置相关工具类,并创建 JsonUtils , 如下图所示:

image-20260202225411225

代码如下:

package com.hanserwei.framework.common.util;

import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.json.JsonMapper;

/**
 * JSON 工具类,提供对象与 JSON 字符串之间的转换功能
 *
 * @author hanser
 */
public class JsonUtils {

    /**
     * 初始化 Jackson 的 JsonMapper 实例
     */
    private static final JsonMapper JSON_MAPPER = JsonMapper.builder()
            // 在反序列化时,忽略在 JSON 中存在但 Java 对象中不存在的属性,防止报错
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
            // 在序列化时,允许序列化空的 POJO 类(没有属性的类),防止报错
            .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
            // 自动查找并注册所有的 Jackson 模块(如 Java 8 时间模块等)
            .findAndAddModules()
            .build();

    /**
     * 将对象序列化为 JSON 字符串
     *
     * @param object 需要转换的对象
     * @return 转换后的 JSON 字符串
     */
    public static String toJsonString(Object object) {
        return JSON_MAPPER.writeValueAsString(object);
    }
}

4. 添加日志切面

Json 工具类编写完成后,回到日志切面 starter 组件中,创建 /aspect 包,用于放置切面相关的类,在里面添加自定义注解以及切面,这块的代码就是很经典的AOP的代码,不做赘述,直接拿过用就行:

package com.hanserwei.framework.biz.operationlog.aspect;

import java.lang.annotation.*;

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

}

package com.hanserwei.framework.biz.operationlog.aspect;

import com.hanserwei.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;

/**
 * API操作日志切面
 *
 * @author hanser
 */
@Aspect
@Slf4j
public class ApiOperationLogAspect {

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

    /**
     * 环绕通知:记录API操作日志
     *
     * @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 中提取 ApiOperationLog 注解
        ApiOperationLog apiOperationLog = method.getAnnotation(ApiOperationLog.class);

        // 4. 从 ApiOperationLog 注解中获取 description 属性
        return apiOperationLog.description();
    }

    /**
     * 转换为 JSON 字符串
     *
     * @return 转换函数
     */
    private Function<Object, String> toJsonStr() {
        return JsonUtils::toJsonString;
    }

}

5. starter 自动配置

接下来,就是自定义 starter 的重头戏 —— 自动配置。新建一个 /config 包,并创建日志切面自动配置类,如下图所示:

package com.hanserwei.framework.biz.operationlog.config;

import com.hanserwei.framework.biz.operationlog.aspect.ApiOperationLogAspect;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;

/**
 * @author hanser
 */
@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 固定步骤,需要严格按照此格式来书写,如下图所示:

image-20260202230019147

注意在创建多级目录的时候要使用 /而不是 .

imports 文件中内容如下,填写 ApiOperationLogAutoConfiguration 配置类的完整包路径:

com.hanserwei.framework.biz.operationlog.config.ApiOperationLogAutoConfiguration

至此,自定义 starter 步骤就完成了。

6. 统一版本控制

接下来,我们想在 hannote-auth 认证服务中使用刚刚封装好的 starter 组件。回到最外层的 pom.xml , 声明该组件依赖以及版本号:

image-20260202230243325

7. 使用 starter

接着,编辑 hanserwei-auth 认证服务的 pom.xml , 添加日志切面 starter 的依赖, 代码如下:

image-20260202230357993

若出现爆红问题,点击右侧 Reload 按钮,重新刷新一下 Maven 依赖。最后,为 /test 接口添加 @ApiOperationLog 日志切面注解:

package com.hanserwei.hannote.auth.controller;

import com.hanserwei.framework.biz.operationlog.aspect.ApiOperationLog;
import com.hanserwei.framework.common.response.Response;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author hanser
 */
@RestController
public class TestController {

    @GetMapping("/test")
    @ApiOperationLog
    public Response<String> test() {
        return Response.success("Hello, Hanserwei!");
    }
}

重启项目, 浏览器访问 localhost:8080/test 接口,自测一波,看看日志切面是否能够正常工作:

image-20260202230610095

OK , 没有任何问题,自定义的日志切面 starter 工作良好!

8. 测试一下,切面打印出参中包含 Java 8 新日期 API

上面我们讲到,Jackson3已经支持 Java 8 新日志 API 问题。这块也需要单独测试一下:

image-20260202230729357

创建一个 User 用户实体类,并添加两个字段,包含一个 LocalDateTime 日期字段,代码如下:

package com.hanserwei.hannote.auth.controller;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

/**
 * @author hanser
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class User {
    /**
     * 昵称
     */
    private String nickName;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;
}


编辑 TestController 控制器,新增一个 /test2 接口,代码如下:

@GetMapping("/test2")
@ApiOperationLog(description = "测试接口2")
public Response<User> test2() {
    return Response.success(User.builder()
            .nickName("Hanserwei")
            .createTime(LocalDateTime.now())
            .build());
}

重启项目,浏览器访问: http://localhost:8080/test2 , 观察控制台日志,如下:

image-20260202230923575

可以看到,即使出参对象中包含 Java 8 新日期 API 字段, 也是正常的,没有出现报异常情况。就是日期序列化格式不太友好,如上图所示。

9. 适配日期序列化格式

为了解决上述问题,我们需要手动指定 Jackson 日期的序列化和反序列化规则。在 hanserwei-common 通用模块中,新建包 /constant 全局常量包, 并创建 DateConstants 日期常量接口,代码如下:

image-20260203203650272

代码如下:

package com.hanserwei.framework.common.constant;

/**
 * @author hanser
 */
public interface DateConstants {
    /**
     * 年-月-日 时:分:秒
     */
    String Y_M_D_H_M_S_FORMAT = "yyyy-MM-dd HH:mm:ss";
}

接着,编辑 JsonUtils 工具类,手动配置 LocalDateTime 日期格式化规则,注意,Jackson3的配置方法和 Jackson2的配置方式有点区别:

package com.hanserwei.framework.common.util;

import com.hanserwei.framework.common.constant.DateConstants;
import tools.jackson.databind.DeserializationFeature;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.ext.javatime.deser.LocalDateTimeDeserializer;
import tools.jackson.databind.ext.javatime.ser.LocalDateTimeSerializer;
import tools.jackson.databind.json.JsonMapper;
import tools.jackson.databind.module.SimpleModule;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.TimeZone;

/**
 * JSON 工具类,提供对象与 JSON 字符串之间的转换功能
 *
 * @author hanser
 */
public class JsonUtils {

    /**
     * 初始化 Jackson 的 JsonMapper 实例
     */
    private static final JsonMapper JSON_MAPPER;


    static {
        // 创建日期时间格式化器,使用 yyyy-MM-dd HH:mm:ss 格式
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DateConstants.Y_M_D_H_M_S_FORMAT);

        // 创建自定义模块,用于注册 LocalDateTime 的序列化和反序列化器
        SimpleModule customizeModule = new SimpleModule();
        // 注册 LocalDateTime 反序列化器,将 JSON 字符串转换为 LocalDateTime 对象
        customizeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(dateTimeFormatter));
        // 注册 LocalDateTime 序列化器,将 LocalDateTime 对象转换为 JSON 字符串
        customizeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(dateTimeFormatter));

        // 构建 JsonMapper 实例
        JSON_MAPPER = JsonMapper.builder()
                // 在反序列化时,忽略在 JSON 中存在但 Java 对象中不存在的属性,防止报错
                .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
                // 在序列化时,允许序列化空的 POJO 类(没有属性的类),防止报错
                .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
                // 设置默认时区为东八区(北京时间)
                .defaultTimeZone(TimeZone.getTimeZone("GMT+8"))
                // 添加自定义模块
                .addModule(customizeModule)
                // 自动查找并添加所有可用的 Jackson 模块
                .findAndAddModules()
                .build();
    }

    /**
     * 将对象序列化为 JSON 字符串
     *
     * @param object 需要转换的对象
     * @return 转换后的 JSON 字符串
     */
    public static String toJsonString(Object object) {
        return JSON_MAPPER.writeValueAsString(object);
    }
}

再次重启项目,测试 /test2 接口,观察控制台日志,现在看到日期打印格式就友好多了~

image-20260203205106212

10. 结语

本小节中,我们了解了什么是 Spring Boot Starter , 并自己动手实现了一个日志切面 starter 组件,后续创建新的服务时,只需添加该 starter 组件, 然后,为想要打印接口出入参的接口,添加 @ApiOperationLog 注解,即可快速使用上日志切面功能了,非常方便,有木有!