专栏目录

四、IDEA 搭建 Spring Cloud Alibaba 微服务项目骨架试读

专栏目录

四、IDEA 搭建 Spring Cloud Alibaba 微服务项目骨架试读

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

作者头像 犬小哈
3,478字
12分钟
14张图
7,638

友情提示 : 推荐使用谷歌浏览器来阅读本专栏,其他浏览器可能存在兼容性问题。

本小节中,我们将自定义一个 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... , 为基础设施层添加新的子模块 —— 业务日志切面组件

imgimg

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

解释一下标注的部分:

  • ①:选择 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);
    }

}

解释一波:

  1. private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();: 创建了一个私有的静态不可变的 ObjectMapper 实例,ObjectMapper 是 Jackson 库中用于序列化和反序列化 JSON 的核心类。
  2. static { ... }: 这是一个静态初始化块,用于在类加载时执行一些初始化操作。在这里,OBJECT_MAPPER 被配置以在反序列化时忽略未知属性和在序列化时忽略空的 Java Bean 属性,并且注册了一个 JavaTimeModule 模块,用于解决 LocalDateTime 类型的序列化问题。
  3. public static String toJsonString(Object obj) { ... }: 这是一个公共静态方法,用于将给定的 Java 对象序列化为 JSON 字符串。它接受一个 Object 类型的参数 obj,并使用 OBJECT_MAPPER 将其转换为 JSON 字符串并返回。
  4. @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 注解,即可快速使用上日志切面功能了,非常方便,有木有!

11. 本小节源码下载

https://t.zsxq.com/wvKaU

添加小哈私人微信

已加入星球的小伙伴,记得添加小哈私人微信,拉你进 VIP 讨论群

一定要备注 “星球” 哟,不然通过率极低

分享有赏

打开星球 APP,操作图如下

觉得星球还不错的话,可邀请好友加入呀

通过星球 APP 专属的分享链接加入,分享者将获得票价的 40% 分成,同时可获得大量积分,每月积分靠前者,可获取小哈赠书哟,一波赢回票价~

微信扫一扫,加入星球

领券加入,超便宜~

专属的项目实战 / 1v1 提问 / Java学习路线

社群讨论 / 每月赠书 / 学习打卡

👉星球介绍