精通Spring Boot 3 : 14. 扩展 Spring Boot 框架 (2)

构建 myretro-spring-boot-starter 自定义启动模块

接下来,让我们来编译自定义启动器。如果之前的步骤都顺利进行,那么您可以使用以下命令来构建自定义启动器:

./gradlew build

此命令会在 build/libs 文件夹中生成 myretro-spring-boot-starter-0.0.1.jar 文件,准备好使用!如果您希望跳过将 JAR 发布到 Maven 仓库的步骤,可以直接在您的 Users App 项目或 My Retro App 项目中以以下方式使用此 JAR(在您的 Users App 或 My Retro App 的 build.gradle 文件中):

//...
dependencies {
    //...
    implementation files('../myretro-spring-boot-starter/build/libs/myretro-spring-boot-starter-0.0.1.jar')
    //...
}

本声明假设您在下一级目录中有 myretro-spring-boot-starter 项目。

但是自定义启动器的理念是每位开发者都能访问它,对吗?因此,我们需要将其发布到 Maven 仓库。

将自定义启动器作为 Maven 工件发布到 GitHub 上

为了发布我们的启动项目,我们可以将 GitHub 用作 Maven 工件库。好处在于,GitHub 不仅为 Maven 提供这些发布功能,还支持 Docker 镜像。

发布 Maven 工件需要我们完成三项工作:

  1. 1.
  2. 创建一个 GitHub 令牌,以便我们能够向我们的代码库进行写入和发布。请参考以下文档以创建您的个人令牌:https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens。
  3. 2.
  4. 在 GitHub 上创建一个新的空仓库,作为一个常规项目。在我的例子中,我在这里创建了一个新的仓库:https://github.com/felipeg48/myretro-spring-boot-starter。
  5. 3.
  6. 在 build.gradle 文件中添加必要的声明,以便发布工件。

接下来,让我们来修改一下 build.gradle 文件。请查看清单 14-20。

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.2' apply false
    id 'io.spring.dependency-management' version '1.1.4'
    id 'maven-publish'
}
group = 'com.apress'
version = '0.0.1'
java {
    sourceCompatibility = '17'
}
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
tasks.named('compileJava') {
    inputs.files(tasks.named('processResources'))
}
repositories {
    mavenCentral()
}
dependencyManagement {
    imports {
        mavenBom org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES
    }
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
    runtimeOnly 'com.h2database:h2'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
    useJUnitPlatform()
}
publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java
            artifactId = 'myretro-spring-boot-starter'
            versionMapping {
                usage('java-api') {
                    fromResolutionOf('runtimeClasspath')
                }
                usage('java-runtime') {
                    fromResolutionResult()
                }
            }
            pom {
                name = 'My Retro Starter'
                description = 'A spring-boot-starter library example'
                licenses {
                    license {
                        name = 'The Apache License, Version 2.0'
                        url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    }
                }
                developers {
                    developer {
                        id = 'felipeg48'
                        name = 'Felipe'
                        email = ''
                    }
                }
                scm {
                    connection = 'scm:git:git://github.com/felipeg48/myretro-spring-boot-starter.git'
                    developerConnection = 'scm:git:ssh://github.com/felipeg48/myretro-spring-boot-starter.git'
                    url = 'https://github.com/felipeg48/myretro-spring-boot-starter'
                }
            }
        }
    }
    repositories {
        maven {
            name = "GitHubPackages"
            url = uri("https://maven.pkg.github.com/felipeg48/myretro-spring-boot-starter")
            credentials {
                username = project.findProperty("GITHUB_USERNAME") ?: System.getenv("GITHUB_USERNAME")
                password = project.findProperty("GITHUB_TOKEN") ?: System.getenv("GITHUB_TOKEN")
            }
        }
    }
}

让我们逐部分回顾一下修改后的 build.gradle 文件:

  • 在这里,我们添加了 maven-publish 插件,该插件负责将构件发布到 Maven 仓库。
  • 本节帮助定义 Maven 仓库所需的元数据,并说明如何登录,特别是在仓库为私有或需要凭据及身份验证机制的情况下。
  • 发布出版物。在这里,我们定义了工件在 Maven 仓库中注册所需的元数据。审查过程非常简单。
  • 本节定义了存储库的位置。我们有一个特殊的 URL,https://maven.pkg.github.com/felipeg48/myretro-spring-boot-starter。这个 URL 仅用于声明,帮助查找软件包。它还定义了所需的凭据,在这种情况下,我们需要通过 GITHUB_USERNAME 和 GITHUB_TOKEN 变量来获取用户名和密码(这些变量可以在 $HOME/.gradle/gradle.properties 文件中设置,或者通过环境变量设置)。

现在我们已经准备好一切,让我们来发布我们的工件。请记得您需要创建一个空的仓库;在我的例子中,我在 GitHub 上创建了 myretro-spring-boot-starter(作为一个项目)。请执行以下命令以在您的仓库中发布工件(见图 14-1):

./gradlew publish


GitHub 上的图 14-1 包 (https://github.com/felipeg48/myretro-spring-boot-starter/packages)

正如您所见,我们的包已经成功部署。现在是时候开始使用它了。

使用 myretro-spring-boot-starter 自定义启动模块

让我们打开用户应用程序项目;您可以从 14-extending/users 目录将其导入到您喜欢的 IDE 中。或者,如果您想从 Spring Initializr(https://start.spring.io)开始,请将 Group 字段设置为 com.apress,将 Artifact 和 Name 字段都设置为 users。添加依赖项:Web、JPA、Processor、Validation、Actuator、Lombok 和 H2。生成并下载项目,解压缩后导入到您喜欢的 IDE 中。

接下来,打开 build.gradle 文件,参见清单 14-21。

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.2'
    id 'io.spring.dependency-management' version '1.1.4'
    id 'org.hibernate.orm' version '6.4.1.Final'
}
group = 'com.apress'
version = '0.0.1-SNAPSHOT'
java {
    sourceCompatibility = '17'
}
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
repositories {
    mavenCentral()
    maven {
        url 'https://maven.pkg.github.com/felipeg48/myretro-spring-boot-starter'
        credentials {
            username = project.findProperty("GITHUB_USERNAME") ?: System.getenv("GITHUB_USERNAME")
            password = project.findProperty("GITHUB_TOKEN") ?: System.getenv("GITHUB_TOKEN")
        }
    }
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    // H2 runtime only
    runtimeOnly 'com.h2database:h2'
    // My Retro Starter
          implementation 'com.apress:myretro-spring-boot-starter:0.0.1'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    // Web
    implementation 'org.webjars:bootstrap:5.2.3'
    // Test
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
    useJUnitPlatform()
}

列表 14-21 的 build.gradle 文件

让我们一起查看 build.gradle 文件:

  • 在这个语句中,我们使用了设置的 URL:https://maven.pkg.github.com/felipeg48/myretro-spring-boot-starter。在凭据部分,我们查看 GITHUB_USERNAME 和 GITHUB_TOKEN 变量(这些变量可以在 $HOME/.gradle/gradle.properties 文件中设置,或者通过环境变量设置)。如果您的仓库是公开的,您可以省略凭据部分。
  • 这里我们使用的是刚刚发布的工件!请记住,如果您需要单独测试 JAR,可以使用以下命令(前提是您将项目放在同一文件夹中):
implementation files('../myretro-spring-boot-starter/build/libs/myretro-spring-boot-starter-0.0.1.jar')

如果您正在使用 14-extending/users 中的源代码,您可以继续进行,但如果您是从头开始创建,您可以通过删除事件包并修复 UserService 中使用/发布事件的部分,来使用 JPA 章节中的代码。这将会有所改变。

接下来,打开或创建 UserService 类。请参阅清单 14-22。

package com.apress.users.service;
import com.apress.myretro.annotations.MyRetroAudit;
import com.apress.myretro.annotations.MyRetroAuditOutputFormat;
import com.apress.users.actuator.LogEventEndpoint;
import com.apress.users.model.User;
import com.apress.users.repository.UserRepository;
import lombok.AllArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import java.util.Optional;
@AllArgsConstructor
@Service
public class UserService {
    private UserRepository userRepository;
    private ApplicationEventPublisher publisher;
    private LogEventEndpoint logEventsEndpoint;
    public Iterable<User> getAllUsers() {
        return this.userRepository.findAll();
    }
    public Optional<User> findUserByEmail(String email) {
        return this.userRepository.findById(email);
    }
    @MyRetroAudit(showArgs = true, message = "Saving or updating user", format = MyRetroAuditOutputFormat.JSON, prettyPrint = false)
    public User saveUpdateUser(User user) {
        User userResult = this.user repository.save(user);
        return userResult;
    }
    public void removeUserByEmail(String email) {
        this.userRepository.deleteById(email);
    }
}

文件路径:src/main/java/com/apress/users/UserService.java(列表 14-22)

列表 14-22 显示我们正在使用@MyRetroAudit 注解,并且在这里使用了一些参数。

现在,如果你尝试运行应用程序(使用 ./gradlew bootRun),什么也不会发生。没有日志,也没有数据库中的事件记录。我们缺少 @EnableMyRetroAudit 注解。因此,让我们添加它。请打开或创建 UserConfiguration 类。请参见清单 14-23。

package com.apress.users.config;
import com.apress.myretro.annotations.EnableMyRetroAudit;
import com.apress.users.model.User;
import com.apress.users.model.UserRole;
import com.apress.users.service.UserService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@EnableMyRetroAudit
@Configuration
@EnableConfigurationProperties({UserProperties.class})
public class UserConfiguration {
    @Bean
    CommandLineRunner init(UserService userService) {
        return args -> {
            userService.saveUpdateUser(new User("ximena@email.com", "Ximena", "https://www.gravatar.com/avatar/23bb62a7d0ca63c9a804908e57bf6bd4?d=wavatar", "aw2s0meR!", List.of(UserRole.USER), true));
            userService.saveUpdateUser(new User("norma@email.com", "Norma", "https://www.gravatar.com/avatar/f07f7e553264c9710105edebe6c465e7?d=wavatar", "aw2s0meR!", List.of(UserRole.USER, UserRole.ADMIN), false));
        };
    }
}

用户配置文件的列表 14-23src/main/java/com/apress/users/config/UserConfiguration.java

列表 14-23 显示我们现在使用的是@EnableMyRetroAudit 注解(没有参数,意味着它将使用默认值)。

在我们运行之前,让我们在 application.yaml 文件中添加一些属性。请参见清单 14-24。

spring:
  application:
    name: users-service
  h2:
    console:
      enabled: true
  jpa:
    generate-ddl: true
    show-sql: true
    hibernate:
      ddl-auto: update
  datasource:
    url: jdbc:h2:mem:users_db
info:
  developer:
    name: Felipe
    email: felipe@email.com
  api:
    version: 1.0
management:
  endpoints:
    web:
     exposure:
       include: health,info,event-config,shutdown,configprops,beans
  endpoint:
    configprops:
      show-values: always
    health:
      show-details: always
      status:
        order: events-down, fatal, down, out-of-service, unknown, up
    shutdown:
      enabled: true
  info:
    env:
      enabled: true
server:
  port: ${PORT:8091}
myretro:
  audit:
    useLogger: true
    prefix: '>>> '

列表 14-24 源自 main/resources/application.yaml

列表 14-24 显示我们使用 myretro.audit.*属性来设置 useLogger 和前缀。如果你在 IDE 中尝试这些属性,你应该能看到我们在 additional-spring-configuration-metadata.json 文件中添加的帮助、描述和提示。

现在我们可以开始运行这个应用了。

使用 myretro-spring-boot-starter 启动用户应用程序

要运行用户应用,您可以使用您的 IDE,或者使用以下命令:

./gradle bootRun

当你运行它时,在 UserConfiguration#init 方法中,我们使用 UserService 保存了两个用户,因此你应该在控制台看到如下输出:

...
...
INFO 19475 --- [users-service] [main] MyRetroAudit: >>> {"id":1,"timestamp":"2024-02-20 18:34:38","interceptor":"BEFORE","method":"saveUpdateUser","args":"[User(email=ximena@email.com, name=Ximena, gravatarUrl=https://www.gravatar.com/avatar/23bb62a7d0ca63c9a804908e57bf6bd4?d=wavatar, password=aw2s0meR!, userRole=[USER], active=true)]","result":"User(email=ximena@email.com, name=Ximena, gravatarUrl=https://www.gravatar.com/avatar/23bb62a7d0ca63c9a804908e57bf6bd4?d=wavatar, password=aw2s0meR!, userRole=[USER], active=true)","message":"Saving or updating user"}
....
....
INFO 19475 --- [users-service] [main] MyRetroAudit: >>> {"id":2,"timestamp":"2024-02-20 18:34:38","interceptor":"BEFORE","method":"saveUpdateUser","args":"[User(email=norma@email.com, name=Norma, gravatarUrl=https://www.gravatar.com/avatar/f07f7e553264c9710105edebe6c465e7?d=wavatar, password=aw2s0meR!, userRole=[USER, ADMIN], active=false)]","result":"User(email=norma@email.com, name=Norma, gravatarUrl=https://www.gravatar.com/avatar/f07f7e553264c9710105edebe6c465e7?d=wavatar, password=aw2s0meR!, userRole=[USER, ADMIN], active=false)","message":"Saving or updating user"}
....
....

是的!我们有一个自定义的启动器,使用日志记录和 JSON 格式。

现在,让我们检查事件是否已成功保存到数据库中。因为在 application.yaml 文件中,我们启用了 H2 控制台(spring.h2.console.enabled=true)。请打开浏览器,访问 http://localhost:8091/h2-console。在 URL 字段中输入 jdbc:h2:mem:users_db,然后点击连接。您将看到两个表,分别是 PEOPLE 和 MY_RETRO_AUDIT_EVENT。选择 MY_RETRO_AUDIT_EVENT 表并运行以下 SQL 语句:

SELECT * FROM MY_RETRO_AUDIT_EVENT

你应该能看到如图 14-2 所示的两行。

图 14-2 http://localhost:8091/h2-console

恭喜您!您刚刚成功创建了 Spring Boot 启动器!

你可以尝试我们添加的所有设置;例如,如果你将 prettyPrint 设置为 true 并重新运行应用程序,你应该会看到类似以下的输出(以美观的 JSON 格式呈现!):

2024-02-20T18:45:26.405-05:00  INFO 20644 --- [users-service] [      main] MyRetroAudit                             : >>>
{
  "id" : 2,
  "timestamp" : "2024-02-20 18:45:26",
  "interceptor" : "BEFORE",
  "method" : "saveUpdateUser",
  "args" : "[User(email=norma@email.com, name=Norma, gravatarUrl=https://www.gravatar.com/avatar/f07f7e553264c9710105edebe6c465e7?d=wavatar, password=aw2s0meR!, userRole=[USER, ADMIN], active=false)]",
  "result" : "User(email=norma@email.com, name=Norma, gravatarUrl=https://www.gravatar.com/avatar/f07f7e553264c9710105edebe6c465e7?d=wavatar, password=aw2s0meR!, userRole=[USER, ADMIN], active=false)",
  "message" : "Saving or updating user"
}

概要

在本章中,您学习了如何创建自己的 Spring Boot 启动器。同时,您还深入了解了 @Conditional*、@Enable* 和 @AutoConfiguration 注解,这些注解在背后发挥着重要作用,帮助您实现所需功能。

你了解了 Condition 接口,并学习了如何对其进行修改,以便通过更多的配置和逻辑来创建自己的 bean,或者跳过下一个自动配置。

你了解到需要在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中声明自动配置类。你还学习了 BeanFactoryPostProcessor,以及如何使用它来初始化 bean 或查找信息,就像你在@EnableMyRetroAudit 注解中所做的那样。

现在您对 Spring Boot 的工作原理和它能做什么有了更清晰的理解。在第 15 章中,我们将回顾两个新的 Spring 项目:Spring Modulith 和 Spring AI。