本节深入介绍了Spring Boot的细节。在这里,您可以了解到您可能想要使用和自定义的关键功能。如果您还没有这样做,您可能希望阅读“入门”和“使用Spring Boot进行开发”部分,以便您对基础知识有一个良好的基础。

1. SpringApplication 应用程序

SpringApplication类提供了一种方便的方式来引导一个Spring应用程序,该应用程序是从一个main()方法启动的。在许多情况下,您可以委托给静态的SpringApplication.run方法,如下例所示:

Java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

}
Kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication


@SpringBootApplication
class MyApplication

fun main(args: Array<String>) {
    runApplication<MyApplication>(*args)
}

当您的应用程序启动时,您应该看到类似以下输出:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.3)

2024-02-22T18:33:08.922Z  INFO 35077 --- [           main] o.s.b.d.f.logexample.MyApplication       : 使用Java 17.0.10启动MyApplication,PID为35077(/opt/apps/myapp.jar由myuser在/opt/apps/启动)
2024-02-22T18:33:08.927Z  INFO 35077 --- [           main] o.s.b.d.f.logexample.MyApplication       : 未设置活动配置文件,回退到默认配置文件:"default"
2024-02-22T18:33:10.115Z  INFO 35077 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat在端口8080(http)初始化
2024-02-22T18:33:10.130Z  INFO 35077 --- [           main] o.apache.catalina.core.StandardService   : 启动服务[Tomcat]
2024-02-22T18:33:10.130Z  INFO 35077 --- [           main] o.apache.catalina.core.StandardEngine    : 启动Servlet引擎:[Apache Tomcat/10.1.19]
2024-02-22T18:33:10.200Z  INFO 35077 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : 初始化Spring嵌入式WebApplicationContext
2024-02-22T18:33:10.202Z  INFO 35077 --- [           main] w.s.c.ServletWebServerApplicationContext : 根WebApplicationContext:初始化完成,耗时1199毫秒
2024-02-22T18:33:10.724Z  INFO 35077 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat在端口8080(http)上启动,上下文路径为''
2024-02-22T18:33:10.739Z  INFO 35077 --- [           main] o.s.b.d.f.logexample.MyApplication       : MyApplication在2.279秒内启动(进程运行时间为2.605秒)

默认情况下,会显示INFO日志消息,包括一些相关的启动详细信息,如启动应用程序的用户。如果您需要除INFO之外的日志级别,可以进行设置,如日志级别中所述。应用程序版本是使用主应用程序类的包中的实现版本确定的。可以通过将spring.main.log-startup-info设置为false来关闭启动信息日志记录。这也将关闭应用程序的活动配置文件的日志记录。

要在启动期间添加额外的日志记录,可以在SpringApplication的子类中覆盖logStartupInfo(boolean)方法。

1.1. 启动失败

如果您的应用程序启动失败,注册的FailureAnalyzers将有机会提供专门的错误消息和具体的修复问题的操作。例如,如果您在端口8080上启动Web应用程序,而该端口已经被使用,您应该看到类似以下消息:

***************************
应用程序启动失败
***************************

描述:

嵌入式Servlet容器启动失败。端口8080已经被使用。

操作:

识别并停止监听端口8080的进程,或配置此应用程序以监听另一个端口。
Spring Boot提供了许多FailureAnalyzer实现,您可以添加自己的

如果没有故障分析器能够处理异常,您仍然可以显示完整的条件报告,以更好地了解出了什么问题。为此,您需要启用debug属性启用DEBUG日志记录以用于org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

例如,如果您通过java -jar运行应用程序,可以如下启用debug属性:

$ java -jar myproject-0.0.1-SNAPSHOT.jar --debug

1.2. 懒加载

SpringApplication允许应用程序进行懒加载初始化。启用懒加载初始化时,bean将在需要时创建,而不是在应用程序启动期间创建。因此,启用懒加载初始化可以减少应用程序启动所需的时间。在Web应用程序中,启用懒加载初始化将导致许多与Web相关的bean直到收到HTTP请求才被初始化。

懒加载的一个缺点是它可能会延迟发现应用程序的问题。如果一个配置错误的bean被懒加载初始化,那么故障将不会在启动期间发生,问题只有在初始化bean时才会显现。还必须注意确保JVM有足够的内存来容纳所有应用程序的bean,而不仅仅是在启动期间初始化的那些bean。出于这些原因,默认情况下不启用懒加载初始化,并建议在启用懒加载初始化之前对JVM的堆大小进行微调。

可以通过在SpringApplicationBuilder上使用lazyInitialization方法或在SpringApplication上使用setLazyInitialization方法来以编程方式启用懒加载初始化。另外,可以使用以下示例中的spring.main.lazy-initialization属性来启用:

属性
spring.main.lazy-initialization=true
Yaml
spring:
  main:
    lazy-initialization: true
如果您想在使用懒加载初始化的同时禁用某些bean的懒加载,可以使用@Lazy(false)注解显式将它们的lazy属性设置为false。

1.3. 自定义横幅

可以通过在类路径中添加banner.txt文件或设置spring.banner.location属性为该文件的位置来更改启动时打印的横幅。如果文件的编码不是UTF-8,可以设置spring.banner.charset

在您的banner.txt文件中,您可以使用Environment中可用的任何键,以及以下任何占位符:

表1. 横幅变量
变量 描述

${application.version}

您的应用程序的版本号,如在MANIFEST.MF中声明的。例如,Implementation-Version: 1.0打印为1.0

${application.formatted-version}

您的应用程序的版本号,如在MANIFEST.MF中声明的,并格式化为显示(用括号括起来并以v为前缀)。例如(v1.0)

${spring-boot.version}

您正在使用的Spring Boot版本。例如3.2.3

${spring-boot.formatted-version}

您正在使用的Spring Boot版本,格式化为显示(用括号括起来并以v为前缀)。例如(v3.2.3)

${Ansi.NAME}(或${AnsiColor.NAME}${AnsiBackground.NAME}${AnsiStyle.NAME}

其中NAME是ANSI转义代码的名称。有关详细信息,请参见AnsiPropertySource

${application.title}

您的应用程序的标题,如在MANIFEST.MF中声明的。例如Implementation-Title: MyApp打印为MyApp

如果要动态生成横幅,可以使用SpringApplication.setBanner(…​)方法。使用org.springframework.boot.Banner接口并实现自己的printBanner()方法。

您还可以使用spring.main.banner-mode属性确定横幅是要打印在System.outconsole)上,发送到配置的记录器(log),还是根本不生成(off)。

打印的横幅注册为一个名为springBootBanner的单例bean。

只有在使用java -jarjava -cp与Spring Boot启动器时,application.titleapplication.versionapplication.formatted-version属性才可用。如果您正在运行未打包的jar并使用java -cp <classpath> <mainclass>启动它,或者将应用程序作为本机映像运行,则这些值将不会被解析。

要使用application.属性,请使用java -jar启动您的应用程序作为打包的jar,或者使用java org.springframework.boot.loader.launch.JarLauncher启动未打包的jar。这将在构建类路径和启动应用程序之前初始化application.横幅属性。

1.4. 自定义SpringApplication

如果SpringApplication的默认设置不符合您的口味,您可以创建一个本地实例并进行自定义。例如,要关闭横幅,您可以编写:

Java
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(MyApplication.class);
        application.setBannerMode(Banner.Mode.OFF);
        application.run(args);
    }

}
Kotlin
import org.springframework.boot.Banner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class MyApplication

fun main(args: Array<String>) {
    runApplication<MyApplication>(*args) {
        setBannerMode(Banner.Mode.OFF)
    }
}
传递给SpringApplication的构造函数参数是Spring bean的配置源。在大多数情况下,这些是对@Configuration类的引用,但也可以是对@Component类的直接引用。

还可以通过使用application.properties文件配置SpringApplication。有关详细信息,请参阅外部化配置

有关配置选项的完整列表,请参阅SpringApplication Javadoc

1.5. 流畅构建器API

如果需要构建ApplicationContext层次结构(具有父/子关系的多个上下文)或者更喜欢使用“流畅”构建器API,则可以使用SpringApplicationBuilder

SpringApplicationBuilder允许您链接多个方法调用,并包括parentchild方法,让您创建一个层次结构,如下例所示:

Java
new SpringApplicationBuilder().sources(Parent.class)
    .child(Application.class)
    .bannerMode(Banner.Mode.OFF)
    .run(args);
Kotlin
SpringApplicationBuilder()
    .sources(Parent::class.java)
    .child(Application::class.java)
    .bannerMode(Banner.Mode.OFF)
    .run(*args)
创建ApplicationContext层次结构时存在一些限制。例如,Web组件必须包含在子上下文中,并且父上下文和子上下文使用相同的Environment。有关完整详细信息,请参阅SpringApplicationBuilder Javadoc

1.6. 应用程序可用性

在部署在平台上时,应用程序可以利用基础设施(如Kubernetes探针)向平台提供关于其可用性的信息。Spring Boot内置支持常用的“活跃”和“就绪”可用性状态。如果您使用Spring Boot的“执行器”支持,则这些状态将作为健康端点组公开。

此外,您还可以通过将ApplicationAvailability接口注入到自己的bean中来获取可用性状态。

1.6.1. 活跃状态

应用程序的“活跃”状态告诉应用程序的内部状态是否允许其正确工作,或者如果当前失败是否可以自行恢复。破损的“活跃”状态意味着应用程序处于无法恢复的状态,基础设施应重新启动应用程序。

一般来说,“活跃”状态不应基于外部检查,例如健康检查。如果是这样,外部系统(数据库、Web API、外部缓存)的失败将触发大规模重启和平台上的级联故障。

Spring Boot应用程序的内部状态主要由Spring的ApplicationContext表示。如果应用程序上下文已成功启动,Spring Boot假定应用程序处于有效状态。一旦上下文已刷新,应用程序被视为处于活动状态,请参阅Spring Boot应用程序生命周期和相关应用程序事件

1.6.2. 就绪状态

应用程序的“就绪”状态告诉应用程序是否准备好处理流量。失败的“就绪”状态告诉平台暂时不要将流量路由到应用程序。这通常发生在启动期间,当处理CommandLineRunnerApplicationRunner组件时,或者在应用程序决定自身对于额外流量太忙时。

一旦应用程序和命令行运行器被调用,应用程序被视为就绪,请参阅Spring Boot应用程序生命周期和相关应用程序事件

预期在启动期间运行的任务应由CommandLineRunnerApplicationRunner组件执行,而不是使用Spring组件生命周期回调,如@PostConstruct

1.6.3. 管理应用程序可用性状态

应用程序组件可以随时通过将ApplicationAvailability接口注入并调用其方法来检索当前可用性状态。通常,应用程序将希望监听状态更新或更新应用程序的状态。

例如,我们可以将应用程序的“就绪”状态导出到文件中,以便Kubernetes的“执行探针”可以查看此文件:

Java
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.ReadinessState;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class MyReadinessStateExporter {

    @EventListener
    public void onStateChange(AvailabilityChangeEvent<ReadinessState> event) {
        switch (event.getState()) {
            case ACCEPTING_TRAFFIC -> {
                // 创建文件 /tmp/healthy
            }
            case REFUSING_TRAFFIC -> {
                // 删除文件 /tmp/healthy
            }
        }
    }

}
Kotlin
import org.springframework.boot.availability.AvailabilityChangeEvent
import org.springframework.boot.availability.ReadinessState
import org.springframework.context.event.EventListener
import org.springframework.stereotype.Component

@Component
class MyReadinessStateExporter {

    @EventListener
    fun onStateChange(event: AvailabilityChangeEvent<ReadinessState?>) {
        when (event.state) {
            ReadinessState.ACCEPTING_TRAFFIC -> {
                // 创建文件 /tmp/healthy
            }
            ReadinessState.REFUSING_TRAFFIC -> {
                // 删除文件 /tmp/healthy
            }
            else -> {
                // ...
            }
        }
    }

}

当应用程序出现故障且无法恢复时,我们也可以更新应用程序的状态:

Java
import org.springframework.boot.availability.AvailabilityChangeEvent;
import org.springframework.boot.availability.LivenessState;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

@Component
public class MyLocalCacheVerifier {

    private final ApplicationEventPublisher eventPublisher;

    public MyLocalCacheVerifier(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void checkLocalCache() {
        try {
            // ...
        }
        catch (CacheCompletelyBrokenException ex) {
            AvailabilityChangeEvent.publish(this.eventPublisher, ex, LivenessState.BROKEN);
        }
    }

}
Kotlin
import org.springframework.boot.availability.AvailabilityChangeEvent
import org.springframework.boot.availability.LivenessState
import org.springframework.context.ApplicationEventPublisher
import org.springframework.stereotype.Component

@Component
class MyLocalCacheVerifier(private val eventPublisher: ApplicationEventPublisher) {

    fun checkLocalCache() {
        try {
            // ...
        } catch (ex: CacheCompletelyBrokenException) {
            AvailabilityChangeEvent.publish(eventPublisher, ex, LivenessState.BROKEN)
        }
    }

}

Spring Boot提供了与执行器健康端点一起的“活跃”和“就绪”的Kubernetes HTTP探针。您可以在专用部分中获取有关在Kubernetes上部署Spring Boot应用程序的更多指导

1.7. 应用事件和监听器

除了通常的Spring Framework事件,例如ContextRefreshedEventSpringApplication还会发送一些额外的应用事件。

有些事件实际上是在创建ApplicationContext之前触发的,因此您无法将监听器注册为@Bean。您可以使用SpringApplication.addListeners(…​)方法或SpringApplicationBuilder.listeners(…​)方法注册它们。

如果您希望这些监听器被自动注册,无论应用程序如何创建,您可以在项目中添加一个META-INF/spring.factories文件,并通过使用org.springframework.context.ApplicationListener键引用您的监听器,如下例所示:

org.springframework.context.ApplicationListener=com.example.project.MyListener

当您的应用程序运行时,应用事件按照以下顺序发送:

  1. 在运行开始时但在任何处理之前发送ApplicationStartingEvent,除了注册监听器和初始化器。

  2. 在已知要在上下文中使用的Environment但在上下文创建之前发送ApplicationEnvironmentPreparedEvent

  3. 在准备好ApplicationContext并调用ApplicationContextInitializers之后但在加载任何bean定义之前发送ApplicationContextInitializedEvent

  4. 在刷新开始之前但在加载bean定义之后发送ApplicationPreparedEvent

  5. 在上下文刷新后但在调用任何应用程序和命令行运行器之前发送ApplicationStartedEvent

  6. LivenessState.CORRECT之后立即发送AvailabilityChangeEvent,表示应用程序被视为活动。

  7. 在调用任何应用程序和命令行运行器之后发送ApplicationReadyEvent

  8. ReadinessState.ACCEPTING_TRAFFIC之后立即发送AvailabilityChangeEvent,表示应用程序已准备好服务请求。

  9. 如果启动时出现异常,则发送ApplicationFailedEvent

上述列表仅包括与SpringApplication相关联的SpringApplicationEvent。除此之外,在ApplicationPreparedEvent之后和ApplicationStartedEvent之前还会发布以下事件:

  • WebServer准备就绪后发送WebServerInitializedEventServletWebServerInitializedEventReactiveWebServerInitializedEvent分别是servlet和响应式变体。

  • ApplicationContext刷新时发送ContextRefreshedEvent

通常情况下,您不需要使用应用事件,但了解它们的存在可能会很方便。在内部,Spring Boot使用事件来处理各种任务。
事件监听器不应执行可能耗时的任务,因为它们默认在同一线程中执行。考虑使用应用程序和命令行运行器代替。

应用事件是通过Spring Framework的事件发布机制发送的。该机制的一部分确保发布到子上下文中的事件也会发布到任何祖先上下文中的监听器。因此,如果您的应用程序使用一系列SpringApplication实例的层次结构,监听器可能会接收同一类型应用事件的多个实例。

为了使您的监听器能够区分其上下文的事件和后代上下文的事件,应请求注入其应用程序上下文,然后将注入的上下文与事件的上下文进行比较。可以通过实现ApplicationContextAware或者如果监听器是一个bean,则使用@Autowired来注入上下文。

1.8. Web环境

SpringApplication尝试代表您创建正确类型的ApplicationContext。用于确定WebApplicationType的算法如下:

  • 如果存在Spring MVC,则使用AnnotationConfigServletWebServerApplicationContext

  • 如果不存在Spring MVC但存在Spring WebFlux,则使用AnnotationConfigReactiveWebServerApplicationContext

  • 否则,使用AnnotationConfigApplicationContext

这意味着,如果您在同一应用程序中同时使用Spring MVC和Spring WebFlux中的新WebClient,默认情况下将使用Spring MVC。您可以通过调用setWebApplicationType(WebApplicationType)轻松覆盖这一点。

还可以通过调用setApplicationContextFactory(…​)完全控制所使用的ApplicationContext类型。

在使用SpringApplication进行JUnit测试时,通常希望调用setWebApplicationType(WebApplicationType.NONE)

1.9. 访问应用程序参数

如果您需要访问传递给SpringApplication.run(…​)的应用程序参数,可以注入一个org.springframework.boot.ApplicationArguments bean。ApplicationArguments接口提供对原始String[]参数以及解析的optionnon-option参数的访问,如下例所示:

Java
import java.util.List;

import org.springframework.boot.ApplicationArguments;
import org.springframework.stereotype.Component;

@Component
public class MyBean {

    public MyBean(ApplicationArguments args) {
        boolean debug = args.containsOption("debug");
        List<String> files = args.getNonOptionArgs();
        if (debug) {
            System.out.println(files);
        }
        // if run with "--debug logfile.txt" prints ["logfile.txt"]
    }

}
Kotlin
import org.springframework.boot.ApplicationArguments
import org.springframework.stereotype.Component

@Component
class MyBean(args: ApplicationArguments) {

    init {
        val debug = args.containsOption("debug")
        val files = args.nonOptionArgs
        if (debug) {
            println(files)
        }
        // if run with "--debug logfile.txt" prints ["logfile.txt"]
    }

}
Spring Boot还向Spring Environment注册了一个CommandLinePropertySource。这使您可以使用@Value注解来注入单个应用程序参数。

1.10. 使用ApplicationRunner或CommandLineRunner

如果您需要在SpringApplication启动后运行特定代码,可以实现ApplicationRunnerCommandLineRunner接口。这两个接口的工作方式相同,并提供一个run方法,在SpringApplication.run(…​)完成之前调用该方法。

这个约定非常适合在应用程序启动后但在开始接受流量之前运行的任务。

CommandLineRunner接口提供对应用程序参数的访问,参数以字符串数组形式传入,而ApplicationRunner使用之前讨论过的ApplicationArguments接口。以下示例展示了一个带有run方法的CommandLineRunner

Java
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class MyCommandLineRunner implements CommandLineRunner {

    @Override
    public void run(String... args) {
        // 做一些事情...
    }

}
Kotlin
import org.springframework.boot.CommandLineRunner
import org.springframework.stereotype.Component

@Component
class MyCommandLineRunner : CommandLineRunner {

    override fun run(vararg args: String) {
        // 做一些事情...
    }

}

如果定义了多个必须按特定顺序调用的CommandLineRunnerApplicationRunner bean,还可以实现org.springframework.core.Ordered接口或使用org.springframework.core.annotation.Order注解。

1.11. 应用程序退出

每个SpringApplication都会向JVM注册一个关闭挂钩,以确保ApplicationContext在退出时正常关闭。可以使用所有标准的Spring生命周期回调(如DisposableBean接口或@PreDestroy注解)。

此外,如果bean希望在调用SpringApplication.exit()时返回特定的退出代码,则可以实现org.springframework.boot.ExitCodeGenerator接口。然后可以将此退出代码传递给System.exit()以将其作为状态代码返回,如下例所示:

Java
import org.springframework.boot.ExitCodeGenerator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class MyApplication {

    @Bean
    public ExitCodeGenerator exitCodeGenerator() {
        return () -> 42;
    }

    public static void main(String[] args) {
        System.exit(SpringApplication.exit(SpringApplication.run(MyApplication.class, args)));
    }

}
Kotlin
import org.springframework.boot.ExitCodeGenerator
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.context.annotation.Bean

import kotlin.system.exitProcess

@SpringBootApplication
class MyApplication {

    @Bean
    fun exitCodeGenerator() = ExitCodeGenerator { 42 }

}

fun main(args: Array<String>) {
    exitProcess(SpringApplication.exit(
        runApplication<MyApplication>(*args)))
}

此外,异常也可以实现ExitCodeGenerator接口。当遇到这样的异常时,Spring Boot将返回由实现的getExitCode()方法提供的退出代码。

如果存在多个ExitCodeGenerator,则使用生成的第一个非零退出代码。要控制调用生成器的顺序,还可以实现org.springframework.core.Ordered接口或使用org.springframework.core.annotation.Order注解。

1.12. 管理功能

通过指定spring.application.admin.enabled属性,可以为应用程序启用与管理相关的功能。这将在平台MBeanServer上公开SpringApplicationAdminMXBean。您可以使用此功能远程管理Spring Boot应用程序。此功能还可用于任何服务包装器实现。

如果想知道应用程序运行在哪个HTTP端口上,请使用键为local.server.port的属性。

1.13. 应用程序启动跟踪

在应用程序启动期间,SpringApplicationApplicationContext执行许多与应用程序生命周期、bean生命周期甚至处理应用程序事件相关的任务。使用ApplicationStartup,Spring Framework允许您使用StartupStep对象跟踪应用程序启动顺序。这些数据可以用于性能分析,或者只是为了更好地了解应用程序启动过程。

在设置SpringApplication实例时,可以选择一个ApplicationStartup实现。例如,要使用BufferingApplicationStartup,可以编写如下代码:

Java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;

@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(MyApplication.class);
        application.setApplicationStartup(new BufferingApplicationStartup(2048));
        application.run(args);
    }

}
Kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup
import org.springframework.boot.runApplication

@SpringBootApplication
class MyApplication

fun main(args: Array<String>) {
    runApplication<MyApplication>(*args) {
        applicationStartup = BufferingApplicationStartup(2048)
    }
}

Spring Framework提供的第一个可用实现是FlightRecorderApplicationStartup。它将Spring特定的启动事件添加到Java Flight Recorder会话中,用于分析应用程序并将其Spring上下文生命周期与JVM事件(如分配、GC、类加载等)相关联。配置后,可以通过启用Flight Recorder来记录数据:

$ java -XX:StartFlightRecording:filename=recording.jfr,duration=10s -jar demo.jar

Spring Boot提供了BufferingApplicationStartup变体;此实现用于缓冲启动步骤并将其传输到外部度量系统。应用程序可以在任何组件中请求类型为BufferingApplicationStartup的bean。

还可以配置Spring Boot以公开一个提供此信息的startup端点,返回一个JSON文档。

1.14. 虚拟线程

如果您正在使用Java 21或更高版本,可以通过将属性spring.threads.virtual.enabled设置为true来启用虚拟线程。

在为应用程序启用此选项之前,您应该考虑阅读官方Java虚拟线程文档。在某些情况下,应用程序可能会因为“固定虚拟线程”而经历较低的吞吐量;此页面还解释了如何使用JDK Flight Recorder或jcmd CLI检测这种情况。

虚拟线程的一个副作用是这些线程是守护线程。如果没有非守护线程,JVM将退出。当您依赖于例如@Scheduled beans来保持应用程序活动时,这种行为可能会成为问题。如果使用虚拟线程,调度程序线程是虚拟线程,因此是守护线程,不会保持JVM活动。这不仅影响调度,还可能适用于其他技术!为了在所有情况下保持JVM运行,建议将属性spring.main.keep-alive设置为true。这确保了即使所有线程都是虚拟线程,JVM也会保持活动状态。

2. 外部化配置

Spring Boot允许您外部化配置,以便在不同环境中使用相同的应用程序代码。您可以使用各种外部配置源,包括Java属性文件、YAML文件、环境变量和命令行参数。

可以通过使用@Value注解直接将属性值注入到您的bean中,通过Spring的Environment抽象访问,或者通过@ConfigurationProperties将其绑定到结构化对象中。

Spring Boot使用非常特定的PropertySource顺序,旨在允许合理覆盖值。后续的属性源可以覆盖先前定义的值。按照以下顺序考虑源:

  1. 默认属性(通过设置SpringApplication.setDefaultProperties指定)。

  2. @PropertySource注解在您的@Configuration类上。请注意,此类属性源直到应用程序上下文正在刷新时才添加到Environment中。这对于配置某些属性(如logging.*spring.main.*)来说太晚了,在刷新开始之前就会读取这些属性。

  3. 配置数据(例如application.properties文件)。

  4. 仅在random.*中具有属性的RandomValuePropertySource

  5. 操作系统环境变量。

  6. Java系统属性(System.getProperties())。

  7. java:comp/env中的JNDI属性。

  8. ServletContext初始化参数。

  9. ServletConfig初始化参数。

  10. SPRING_APPLICATION_JSON中的属性(嵌入在环境变量或系统属性中的内联JSON)。

  11. 命令行参数。

  12. 您测试中的properties属性。可用于@SpringBootTest和用于测试应用程序特定部分的测试注解

  13. 您测试中的@DynamicPropertySource注解。

  14. 您测试中的@TestPropertySource注解。

  15. 当devtools激活时,在$HOME/.config/spring-boot目录中的Devtools全局设置属性

配置数据文件按以下顺序考虑:

  1. 打包在您的jar文件中的应用程序属性(application.properties和YAML变体)。

  2. 打包在您的jar文件中的特定配置文件属性(application-{profile}.properties和YAML变体)。

  3. 不在您的打包jar文件中的应用程序属性(application.properties和YAML变体)。

  4. 不在您的打包jar文件中的特定配置文件属性(application-{profile}.properties和YAML变体)。

建议始终使用一种格式来配置整个应用程序。如果配置文件中同时包含.properties和YAML格式,.properties将优先。
如果使用环境变量而不是系统属性,大多数操作系统不允许使用以句点分隔的键名,但您可以改用下划线(例如,使用SPRING_CONFIG_NAME代替spring.config.name)。有关详细信息,请参见从环境变量绑定
如果您的应用程序在Servlet容器或应用程序服务器中运行,则可以使用JNDI属性(在java:comp/env中)或Servlet上下文初始化参数来替代环境变量或系统属性。

为了提供一个具体的示例,假设您开发了一个使用name属性的@Component,如下例所示:

Java
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class MyBean {

    @Value("${name}")
    private String name;

    // ...

}
Kotlin
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Component

@Component
class MyBean {

    @Value("\${name}")
    private val name: String? = null

    // ...

}

在您的应用程序类路径上(例如,在您的jar文件中),您可以拥有一个application.properties文件,为name提供一个合理的默认属性值。在新环境中运行时,可以在jar文件之外提供一个application.properties文件,用于覆盖name。对于一次性测试,您可以使用特定的命令行开关启动(例如,java -jar app.jar --name="Spring")。

在确定属性具有特定值的原因时,envconfigprops端点可能很有用。您可以使用这两个端点来诊断意外的属性值。有关详细信息,请参见"生产就绪功能"部分。

2.1. 访问命令行属性

默认情况下,SpringApplication将任何命令行选项参数(即以--开头的参数,例如--server.port=9000)转换为一个property并将其添加到Spring的Environment中。如前所述,命令行属性始终优先于基于文件的属性源。

如果不希望将命令行属性添加到Environment中,可以通过使用SpringApplication.setAddCommandLineProperties(false)来禁用它们。

2.2. JSON应用程序属性

环境变量和系统属性通常有限制,意味着某些属性名称无法使用。为了解决这个问题,Spring Boot允许您将一组属性编码为单个JSON结构。

当您的应用程序启动时,任何spring.application.jsonSPRING_APPLICATION_JSON属性都将被解析并添加到Environment中。

例如,在UNIX shell中,可以在命令行中提供SPRING_APPLICATION_JSON属性作为环境变量:

$ SPRING_APPLICATION_JSON='{"my":{"name":"test"}}' java -jar myapp.jar

在上面的示例中,您将在Spring的Environment中得到my.name=test

同样的JSON也可以作为系统属性提供:

$ java -Dspring.application.json='{"my":{"name":"test"}}' -jar myapp.jar

或者您可以通过使用命令行参数提供JSON:

$ java -jar myapp.jar --spring.application.json='{"my":{"name":"test"}}'

如果部署到经典应用程序服务器,则还可以使用名为java:comp/env/spring.application.json的JNDI变量。

尽管JSON中的null值将被添加到生成的属性源中,但PropertySourcesPropertyResolvernull属性视为缺失值。这意味着JSON无法使用null值覆盖较低顺序属性源中的属性。

2.3. 外部应用程序属性

当您的应用程序启动时,Spring Boot将自动查找并加载以下位置的application.propertiesapplication.yaml文件:

  1. 从类路径中

    1. 类路径根目录

    2. 类路径中的/config

  2. 从当前目录中

    1. 当前目录

    2. 当前目录中的config/子目录

    3. config/子目录的直接子目录

列表按优先顺序排列(较低项目的值会覆盖较早的值)。从加载的文件中的文档将作为PropertySources添加到Spring的Environment中。

如果您不喜欢application作为配置文件名,您可以通过指定spring.config.name环境属性来切换到另一个文件名。例如,要查找myproject.propertiesmyproject.yaml文件,您可以按以下方式运行应用程序:

$ java -jar myproject.jar --spring.config.name=myproject

您还可以通过使用spring.config.location环境属性引用显式位置。此属性接受一个逗号分隔的一个或多个要检查的位置列表。

以下示例显示了如何指定两个不同的文件:

$ java -jar myproject.jar --spring.config.location=\
    optional:classpath:/default.properties,\
    optional:classpath:/override.properties
如果位置是可选的,并且您不介意它们不存在,请使用前缀optional:
spring.config.namespring.config.locationspring.config.additional-location在早期用于确定要加载的文件。它们必须定义为环境属性(通常是操作系统环境变量、系统属性或命令行参数)。

如果spring.config.location包含目录(而不是文件),它们应以/结尾。在运行时,它们将附加在从spring.config.name生成的名称之后再加载。在spring.config.location中指定的文件将直接导入。

目录和文件位置值也会扩展以检查特定配置文件。例如,如果您的spring.config.locationclasspath:myconfig.properties,您还将找到适当的classpath:myconfig-<profile>.properties文件被加载。

在大多数情况下,您添加的每个spring.config.location项将引用单个文件或目录。位置按定义的顺序处理,后面的位置可以覆盖前面位置的值。

如果您有复杂的位置设置,并且使用特定配置文件,您可能需要提供进一步的提示,以便Spring Boot知道它们应该如何分组。位置组是一组同时考虑的位置。例如,您可能希望将所有类路径位置分组,然后将所有外部位置分组。位置组内的项目应使用;分隔。有关更多详细信息,请参见“特定配置文件”部分中的示例。

通过使用spring.config.location配置的位置将替换默认位置。例如,如果spring.config.location配置为值optional:classpath:/custom-config/,optional:file:./custom-config/,则考虑的完整位置集如下:

  1. optional:classpath:custom-config/

  2. optional:file:./custom-config/

如果您希望添加额外位置而不是替换它们,可以使用spring.config.additional-location。从额外位置加载的属性可以覆盖默认位置中的属性。例如,如果spring.config.additional-location配置为值optional:classpath:/custom-config/,optional:file:./custom-config/,则考虑的完整位置集如下:

  1. optional:classpath:/;optional:classpath:/config/

  2. optional:file:./;optional:file:./config/;optional:file:./config/*/

  3. optional:classpath:custom-config/

  4. optional:file:./custom-config/

此搜索顺序使您可以在一个配置文件中指定默认值,然后在另一个配置文件中有选择地覆盖这些值。您可以在一个默认位置的application.properties(或者您选择的其他基本名称与spring.config.name)中为应用程序提供默认值。然后,可以在运行时使用位于自定义位置之一的不同文件覆盖这些默认值。

2.3.1. 可选位置

默认情况下,当指定的配置数据位置不存在时,Spring Boot将抛出ConfigDataLocationNotFoundException,并且您的应用程序将无法启动。

如果要指定一个位置,但不介意它并非总是存在,您可以使用optional:前缀。您可以将此前缀与spring.config.locationspring.config.additional-location属性一起使用,以及与spring.config.import声明一起使用。

例如,spring.config.import值为optional:file:./myconfig.properties允许您的应用程序启动,即使myconfig.properties文件不存在。

如果要忽略所有ConfigDataLocationNotFoundExceptions并始终继续启动应用程序,您可以使用spring.config.on-not-found属性。使用SpringApplication.setDefaultProperties(…​)或系统/环境变量将值设置为ignore

2.3.2. 通配符位置

如果配置文件位置包含最后一个路径段的*字符,则被视为通配符位置。在加载配置时,通配符将被展开,以便也检查直接子目录。通配符位置在诸如Kubernetes之类的环境中特别有用,当存在多个配置属性来源时。

例如,如果您有一些Redis配置和一些MySQL配置,您可能希望将这两个配置分开,同时要求这两个配置都存在于一个application.properties文件中。这可能导致两个单独的application.properties文件挂载在不同的位置,例如/config/redis/application.properties/config/mysql/application.properties。在这种情况下,具有config/*/的通配符位置将导致两个文件都被处理。

默认情况下,Spring Boot在默认搜索位置中包含config/*/。这意味着将搜索/config目录之外的jar文件的所有子目录。

您可以使用spring.config.locationspring.config.additional-location属性自己使用通配符位置。

通配符位置必须仅包含一个*,并以*/结尾,用于搜索目录的搜索位置,或以*/<filename>结尾,用于搜索文件的搜索位置。具有通配符的位置根据文件名的绝对路径按字母顺序排序。
通配符位置仅适用于外部目录。您不能在classpath:位置中使用通配符。

2.3.3. 特定配置文件

除了application属性文件外,Spring Boot还会尝试加载特定配置文件,使用命名约定application-{profile}。例如,如果您的应用激活了名为prod的配置文件并使用YAML文件,则application.yamlapplication-prod.yaml都会被考虑。

特定配置文件会从与标准application.properties相同的位置加载,特定配置文件始终会覆盖非特定配置文件。如果指定了多个配置文件,将采用最后覆盖策略。例如,如果spring.profiles.active属性指定了prod,live配置文件,则application-prod.properties中的值可以被application-live.properties中的值覆盖。

最后覆盖策略适用于位置组级别。具有classpath:/cfg/,classpath:/ext/spring.config.location不会具有与classpath:/cfg/;classpath:/ext/相同的覆盖规则。

例如,继续上面的prod,live示例,我们可能有以下文件:

/cfg
  application-live.properties
/ext
  application-live.properties
  application-prod.properties

当我们有classpath:/cfg/,classpath:/ext/spring.config.location时,我们会处理所有/cfg文件,然后处理所有/ext文件:

  1. /cfg/application-live.properties

  2. /ext/application-prod.properties

  3. /ext/application-live.properties

当我们有classpath:/cfg/;classpath:/ext/(使用;分隔符)时,我们会同时处理/cfg/ext

  1. /ext/application-prod.properties

  2. /cfg/application-live.properties

  3. /ext/application-live.properties

Environment有一组默认配置文件(默认为[default]),如果没有设置活动配置文件,则会使用这些默认配置文件。换句话说,如果没有明确激活配置文件,则会考虑application-default中的属性。

属性文件只会被加载一次。如果您已经直接导入了特定配置文件,则不会再次导入。

2.3.4. 导入额外数据

应用程序属性可以使用spring.config.import属性从其他位置导入更多配置数据。导入会在发现时进行处理,并被视为立即插入声明导入的文档下方的附加文档。

例如,您可能在类路径下的application.properties文件中有以下内容:

属性
spring.application.name=myapp
spring.config.import=optional:file:./dev.properties
Yaml
spring:
  application:
    name: "myapp"
  config:
    import: "optional:file:./dev.properties"

这将触发导入当前目录中的dev.properties文件(如果存在)。导入的dev.properties中的值将优先于触发导入的文件。在上面的示例中,dev.properties可以重新定义spring.application.name为不同的值。

无论声明多少次,导入只会导入一次。在单个文档内定义导入的顺序在属性/yaml文件中不重要。例如,下面的两个示例产生相同的结果:

属性
spring.config.import=my.properties
my.property=value
Yaml
spring:
  config:
    import: "my.properties"
my:
  property: "value"
属性
my.property=value
spring.config.import=my.properties
Yaml
my:
  property: "value"
spring:
  config:
    import: "my.properties"

在上述两个示例中,my.properties文件中的值将优先于触发其导入的文件。

可以在单个spring.config.import键下指定多个位置。位置将按照定义的顺序进行处理,后续导入将优先。

在适当的情况下,还会考虑特定配置文件变体进行导入。上面的示例将同时导入my.properties和任何my-<profile>.properties变体。

Spring Boot包含可插拔的API,允许支持各种不同的位置地址。默认情况下,您可以导入Java属性、YAML和“配置树”。

第三方jar包可以提供对其他技术的支持(文件不一定要在本地)。例如,您可以想象来自外部存储(如Consul、Apache ZooKeeper或Netflix Archaius)的配置数据。

如果要支持自己的位置,请参阅org.springframework.boot.context.config包中的ConfigDataLocationResolverConfigDataLoader类。

2.3.5. 导入无扩展名文件

一些云平台无法向挂载的文件添加文件扩展名。要导入这些无扩展名文件,您需要给Spring Boot一个提示,以便它知道如何加载它们。您可以通过在方括号中放置扩展提示来实现这一点。

例如,假设您有一个希望作为yaml导入的/etc/config/myconfig文件。您可以在您的application.properties中使用以下内容导入它:

属性
spring.config.import=file:/etc/config/myconfig[.yaml]
Yaml
spring:
  config:
    import: "file:/etc/config/myconfig[.yaml]"

2.3.6. 使用配置树

在云平台(如Kubernetes)上运行应用程序时,通常需要读取平台提供的配置值。通常情况下,为此目的使用环境变量并不罕见,但这可能会有缺点,特别是如果该值应该保密。

作为环境变量的替代方案,许多云平台现在允许您将配置映射到挂载的数据卷中。例如,Kubernetes可以挂载ConfigMapsSecrets

有两种常见的挂载模式可以使用:

  1. 一个文件包含完整的属性集(通常以YAML编写)。

  2. 多个文件写入到一个目录树中,文件名成为“键”,内容成为“值”。

对于第一种情况,您可以直接使用spring.config.import导入YAML或Properties文件,如上所述。对于第二种情况,您需要使用configtree:前缀,以便Spring Boot知道它需要将所有文件公开为属性。

例如,假设Kubernetes已挂载了以下卷:

etc/
  config/
    myapp/
      username
      password

username文件的内容将是一个配置值,password的内容将是一个秘密。

要导入这些属性,您可以将以下内容添加到您的application.propertiesapplication.yaml文件中:

属性
spring.config.import=optional:configtree:/etc/config/
Yaml
spring:
  config:
    import: "optional:configtree:/etc/config/"

然后,您可以以通常的方式从Environment中访问或注入myapp.usernamemyapp.password属性。

配置树中的文件夹和文件的名称形成属性名称。在上面的示例中,要访问属性usernamepassword,您可以将spring.config.import设置为optional:configtree:/etc/config/myapp
带有点符号的文件名也会被正确映射。例如,在上面的示例中,位于/etc/config中的名为myapp.username的文件将导致Environment中的myapp.username属性。
配置树值可以绑定到字符串Stringbyte[]类型,具体取决于预期的内容。

如果您有多个要从同一父文件夹导入的配置树,可以使用通配符快捷方式。任何以/*/结尾的configtree:位置将导入所有直接子项作为配置树。与非通配符导入一样,每个配置树下的文件夹和文件的名称形成属性名称。

例如,给定以下卷:

etc/
  config/
    dbconfig/
      db/
        username
        password
    mqconfig/
      mq/
        username
        password

您可以使用configtree:/etc/config/*/作为导入位置:

属性
spring.config.import=optional:configtree:/etc/config/*/
Yaml
spring:
  config:
    import: "optional:configtree:/etc/config/*/"

这将添加db.usernamedb.passwordmq.usernamemq.password属性。

使用通配符加载的目录按字母顺序排序。如果需要不同的顺序,则应将每个位置列为单独的导入。

配置树也可以用于Docker Secrets。当Docker Swarm服务被授予对秘密的访问权限时,秘密会被挂载到容器中。例如,如果名为db.password的秘密被挂载到位置/run/secrets/,您可以使用以下方式使db.password在Spring环境中可用:

属性
spring.config.import=optional:configtree:/run/secrets/
Yaml
spring:
  config:
    import: "optional:configtree:/run/secrets/"

2.3.7. 属性占位符

在使用时,application.propertiesapplication.yaml中的值会通过现有的Environment进行过滤,因此您可以引用先前定义的值(例如,来自系统属性或环境变量)。标准的${name}属性占位符语法可以在值的任何位置使用。属性占位符还可以使用:来分隔默认值和属性名称,例如${name:default}

以下示例展示了带有和不带默认值的占位符的使用:

属性
app.name=MyApp
app.description=${app.name} is a Spring Boot application written by ${username:Unknown}
Yaml
app:
  name: "MyApp"
  description: "${app.name} is a Spring Boot application written by ${username:Unknown}"

假设username属性未在其他地方设置,app.description将具有值MyApp is a Spring Boot application written by Unknown

您应始终使用属性名称的规范形式(使用仅小写字母的kebab-case)。这将允许Spring Boot使用与relaxed binding @ConfigurationProperties相同的逻辑。

例如,${demo.item-price}将从application.properties文件中提取demo.item-pricedemo.itemPrice形式,以及从系统环境中提取DEMO_ITEMPRICE。如果您使用${demo.itemPrice},则不会考虑demo.item-priceDEMO_ITEMPRICE

您还可以使用此技术创建现有Spring Boot属性的“短”变体。有关详细信息,请参阅howto.html中的操作指南。

2.3.8. 使用多文档文件

Spring Boot允许您将单个物理文件拆分为多个逻辑文档,每个文档都可以独立添加。文档按顺序从上到下处理。后续文档可以覆盖先前定义的属性。

对于application.yaml文件,使用标准的YAML多文档语法。三个连续的破折号表示一个文档的结束,下一个文档的开始。

例如,以下文件有两个逻辑文档:

spring:
  application:
    name: "MyApp"
---
spring:
  application:
    name: "MyCloudApp"
  config:
    activate:
      on-cloud-platform: "kubernetes"

对于application.properties文件,使用特殊的#---!---注释来标记文档分隔:

spring.application.name=MyApp
#---
spring.application.name=MyCloudApp
spring.config.activate.on-cloud-platform=kubernetes
属性文件分隔符不能有任何前导空格,必须恰好有三个连字符。分隔符之前和之后的行不能是相同的注释前缀。
多文档属性文件通常与激活属性一起使用,例如spring.config.activate.on-profile。有关详细信息,请参阅下一节
无法使用@PropertySource@TestPropertySource注解加载多文档属性文件。

2.3.9. 激活属性

有时仅在满足特定条件时激活一组属性很有用。例如,当特定配置文件处于活动状态时,您可能只有相关属性。

您可以使用spring.config.activate.*有条件地激活属性文档。

以下激活属性可用:

表2. 激活属性
属性 说明

on-profile

必须匹配的配置文件表达式,文档才能处于活动状态。

on-cloud-platform

必须检测到的CloudPlatform,文档才能处于活动状态。

例如,以下指定第二个文档仅在运行在Kubernetes上时处于活动状态,并且仅在“prod”或“staging”配置文件处于活动状态时:

属性
myprop=always-set
#---
spring.config.activate.on-cloud-platform=kubernetes
spring.config.activate.on-profile=prod | staging
myotherprop=sometimes-set
Yaml
myprop:
  "always-set"
---
spring:
  config:
    activate:
      on-cloud-platform: "kubernetes"
      on-profile: "prod | staging"
myotherprop: "sometimes-set"

2.4. 加密属性

Spring Boot不提供任何内置支持来加密属性值,但是它提供了必要的挂钩点来修改Spring Environment中包含的值。 EnvironmentPostProcessor接口允许您在应用程序启动之前操纵Environment。有关详细信息,请参见howto.html

如果您需要一种安全的方式来存储凭据和密码,Spring Cloud Vault项目提供了在HashiCorp Vault中存储外部配置的支持。

2.5. 使用YAML

YAML是JSON的超集,因此是一种方便的格式来指定分层配置数据。当您的类路径上有SnakeYAML库时,SpringApplication类会自动支持YAML作为属性的替代。

如果您使用“Starters”,则spring-boot-starter会自动提供SnakeYAML。

2.5.1. 将YAML映射到属性

YAML文档需要从其分层格式转换为可以与Spring Environment一起使用的扁平结构。例如,请考虑以下YAML文档:

environments:
  dev:
    url: "https://dev.example.com"
    name: "Developer Setup"
  prod:
    url: "https://another.example.com"
    name: "My Cool App"
Environment中访问这些属性,它们将被扁平化如下:

environments.dev.url=https://dev.example.com
environments.dev.name=Developer Setup
environments.prod.url=https://another.example.com
environments.prod.name=My Cool App
[index]解引用器的属性键。例如,请考虑以下YAML:

my:
 servers:
 - "dev.example.com"
 - "another.example.com"

my.servers[0]=dev.example.com
my.servers[1]=another.example.com
使用[index]表示法的属性可以使用Spring Boot的Binder类绑定到Java ListSet对象。有关更多详细信息,请参见下面的“类型安全的配置属性”部分。
无法使用@PropertySource@TestPropertySource注解加载YAML文件。因此,在需要以这种方式加载值的情况下,您需要使用属性文件。

2.5.2. 直接加载YAML

YamlPropertiesFactoryBean将YAML加载为 Properties,而 YamlMapFactoryBean将YAML加载为 Map

PropertySource,还可以使用 YamlPropertySourceLoader类。

2.6. 配置随机值

RandomValuePropertySource对于注入随机值(例如,用于密码或测试用例)非常有用。它可以生成整数、长整数、uuid或字符串,如以下示例所示:

属性
my.secret=${random.value}
my.number=${random.int}
my.bignumber=${random.long}
my.uuid=${random.uuid}
my.number-less-than-ten=${random.int(10)}
my.number-in-range=${random.int[1024,65536]}
Yaml
my:
  secret: "${random.value}"
  number: "${random.int}"
  bignumber: "${random.long}"
  uuid: "${random.uuid}"
  number-less-than-ten: "${random.int(10)}"
  number-in-range: "${random.int[1024,65536]}"

random.int*语法为OPEN value (,max) CLOSE,其中OPEN,CLOSE是任何字符,value,max是整数。如果提供了max,则value是最小值,max是最大值(不包括)。

2.7. 配置系统环境属性

SpringApplication上设置。

input,则诸如 remote.timeout之类的属性也将在系统环境中解析为 input.remote.timeout

2.8. 类型安全的配置属性

有时使用@Value("${property}")注解来注入配置属性可能会很繁琐,特别是当您需要处理多个属性或者您的数据具有层次结构时。Spring Boot提供了另一种方法来处理属性,让强类型的bean来管理和验证应用程序的配置。

还可以查看@Value和类型安全配置属性之间的区别

2.8.1. JavaBean属性绑定

可以将声明标准JavaBean属性的bean绑定到配置,如下例所示:

Java
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("my.service")
public class MyProperties {

    private boolean enabled;

    private InetAddress remoteAddress;

    private final Security security = new Security();

    // getters / setters...

    public boolean isEnabled() {
        return this.enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public InetAddress getRemoteAddress() {
        return this.remoteAddress;
    }

    public void setRemoteAddress(InetAddress remoteAddress) {
        this.remoteAddress = remoteAddress;
    }

    public Security getSecurity() {
        return this.security;
    }

    public static class Security {

        private String username;

        private String password;

        private List<String> roles = new ArrayList<>(Collections.singleton("USER"));

        // getters / setters...

        public String getUsername() {
            return this.username;
        }

        public void setUsername(String username) {
            this.username = username;
        }

        public String getPassword() {
            return this.password;
        }

        public void setPassword(String password) {
            this.password = password;
        }

        public List<String> getRoles() {
            return this.roles;
        }

        public void setRoles(List<String> roles) {
            this.roles = roles;
        }

    }

}
Kotlin
import org.springframework.boot.context.properties.ConfigurationProperties
import java.net.InetAddress

@ConfigurationProperties("my.service")
class MyProperties {

    var isEnabled = false

    var remoteAddress: InetAddress? = null

    val security = Security()

    class Security {

        var username: String? = null

        var password: String? = null

        var roles: List<String> = ArrayList(setOf("USER"))

    }

}

上述POJO定义了以下属性:

  • my.service.enabled,默认值为false

  • my.service.remote-address,其类型可以从String转换而来。

  • my.service.security.username,带有一个名为"security"的嵌套对象,其名称由属性的名称确定。特别地,在那里类型根本不被使用,也可以是SecurityProperties

  • my.service.security.password

  • my.service.security.roles,包含默认为USERString集合。

映射到Spring Boot中的@ConfigurationProperties类的属性可以通过属性文件、YAML文件、环境变量和其他机制进行配置,这些是公共API,但类本身的访问器(getter/setter)不应直接使用。

这种安排依赖于默认的空构造函数,通常需要getter和setter,因为绑定是通过标准Java Bean属性描述符进行的,就像在Spring MVC中一样。在以下情况下可以省略setter:

  • 映射,只要它们被初始化,需要一个getter但不一定需要一个setter,因为它们可以被绑定器修改。

  • 集合和数组可以通过索引(通常使用YAML)或使用单个逗号分隔值(属性)进行访问。在后一种情况下,setter是必需的。我们建议始终为这些类型添加setter。如果初始化集合,请确保它不是不可变的(如前面的示例)。

  • 如果初始化了嵌套的POJO属性(如前面示例中的Security字段),则不需要setter。如果希望绑定器通过其默认构造函数动态创建实例,则需要setter。

有些人使用Project Lombok自动添加getter和setter。确保Lombok不为此类型生成任何特定的构造函数,因为容器会自动使用它来实例化对象。

最后,只有标准的Java Bean属性被考虑,静态属性上的绑定不受支持。

2.8.2. 构造函数绑定

前一节中的示例可以以不可变的方式重写,如下所示:

Java
import java.net.InetAddress;
import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;

@ConfigurationProperties("my.service")
public class MyProperties {

    // fields...

    private final boolean enabled;

    private final InetAddress remoteAddress;

    private final Security security;

    public MyProperties(boolean enabled, InetAddress remoteAddress, Security security) {
        this.enabled = enabled;
        this.remoteAddress = remoteAddress;
        this.security = security;
    }

    // getters...

    public boolean isEnabled() {
        return this.enabled;
    }

    public InetAddress getRemoteAddress() {
        return this.remoteAddress;
    }

    public Security getSecurity() {
        return this.security;
    }

    public static class Security {

        // fields...

        private final String username;

        private final String password;

        private final List<String> roles;

        public Security(String username, String password, @DefaultValue("USER") List<String> roles) {
            this.username = username;
            this.password = password;
            this.roles = roles;
        }

        // getters...

        public String getUsername() {
            return this.username;
        }

        public String getPassword() {
            return this.password;
        }

        public List<String> getRoles() {
            return this.roles;
        }

    }

}
Kotlin
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.bind.DefaultValue
import java.net.InetAddress

@ConfigurationProperties("my.service")
class MyProperties(val enabled: Boolean, val remoteAddress: InetAddress,
        val security: Security) {

    class Security(val username: String, val password: String,
            @param:DefaultValue("USER") val roles: List<String>)

}

在这种设置中,存在单个参数化构造函数意味着应该使用构造函数绑定。这意味着绑定器将查找具有您希望绑定的参数的构造函数。如果您的类有多个构造函数,则可以使用@ConstructorBinding注解来指定用于构造函数绑定的构造函数。要在具有单个参数化构造函数的类中选择退出构造函数绑定,必须使用@Autowired注解构造函数。构造函数绑定可用于记录。除非您的记录具有多个构造函数,否则无需使用@ConstructorBinding

构造函数绑定类的嵌套成员(例如上面示例中的Security)也将通过它们的构造函数绑定。

可以使用@DefaultValue在构造函数参数和记录组件上指定默认值。转换服务将被应用于将注解的String值强制转换为缺少属性的目标类型。

参考前面的示例,如果没有属性绑定到Security,则MyProperties实例将包含securitynull值。即使没有属性绑定到它(在使用Kotlin时,这将需要将Securityusernamepassword参数声明为可为空,因为它们没有默认值),也可以使用空的@DefaultValue注解使其包含非空的Security实例:

Java
public MyProperties(boolean enabled, InetAddress remoteAddress, @DefaultValue Security security) {
    this.enabled = enabled;
    this.remoteAddress = remoteAddress;
    this.security = security;
}
Kotlin
class MyProperties(val enabled: Boolean, val remoteAddress: InetAddress,
        @DefaultValue val security: Security) {

    class Security(val username: String?, val password: String?,
            @param:DefaultValue("USER") val roles: List<String>)

}
要使用构造函数绑定,类必须使用@EnableConfigurationProperties或配置属性扫描启用。您不能将构造函数绑定与由常规Spring机制创建的bean一起使用(例如@Component bean、使用@Bean方法创建的bean或使用@Import加载的bean)。
要在本机映像中使用构造函数绑定,类必须使用-parameters进行编译。如果使用Spring Boot的Gradle插件或使用Maven和spring-boot-starter-parent,则会自动发生这种情况。
不建议在@ConfigurationProperties中使用java.util.Optional,因为它主要用作返回类型。因此,它不适合于配置属性注入。为了与其他类型的属性保持一致,如果声明了一个Optional属性并且它没有值,则会绑定null而不是空的Optional

2.8.3. 启用 @ConfigurationProperties 注释的类型

Spring Boot提供基础设施来绑定 @ConfigurationProperties 类型并将它们注册为bean。您可以逐个类启用配置属性,也可以启用类似于组件扫描的配置属性扫描。

有时,使用 @ConfigurationProperties 注释的类可能不适合扫描,例如,如果您正在开发自己的自动配置或者想要有条件地启用它们。在这些情况下,使用 @EnableConfigurationProperties 注释指定要处理的类型列表。可以在任何 @Configuration 类上执行此操作,如以下示例所示:

Java
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(SomeProperties.class)
public class MyConfiguration {

}
Kotlin
import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.context.annotation.Configuration

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(SomeProperties::class)
class MyConfiguration
Java
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("some.properties")
public class SomeProperties {

}
Kotlin
import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties("some.properties")
class SomeProperties

要使用配置属性扫描,将 @ConfigurationPropertiesScan 注释添加到您的应用程序中。通常,它添加到使用 @SpringBootApplication 注释的主应用程序类,但也可以添加到任何 @Configuration 类中。默认情况下,扫描将从声明注释的类的包中进行。如果要定义要扫描的特定包,可以按照以下示例进行操作:

Java
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;

@SpringBootApplication
@ConfigurationPropertiesScan({ "com.example.app", "com.example.another" })
public class MyApplication {

}
Kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan

@SpringBootApplication
@ConfigurationPropertiesScan("com.example.app", "com.example.another")
class MyApplication

当使用配置属性扫描或通过 @EnableConfigurationProperties 注释注册 @ConfigurationProperties bean 时,bean 具有传统名称:<prefix>-<fqn>,其中 <prefix> 是在 @ConfigurationProperties 注释中指定的环境键前缀,<fqn> 是bean的完全限定名称。如果注释没有提供任何前缀,则仅使用bean的完全限定名称。

假设它位于 com.example.app 包中,上面 SomeProperties 示例的 bean 名称为 some.properties-com.example.app.SomeProperties

我们建议 @ConfigurationProperties 仅处理环境,特别是不从上下文中注入其他bean。对于特殊情况,可以使用setter注入或框架提供的任何 *Aware 接口(例如,如果需要访问 Environment,可以使用 EnvironmentAware)。如果仍然希望使用构造函数注入其他bean,则配置属性bean 必须带有 @Component 注释并使用基于JavaBean的属性绑定。

2.8.4. 使用 @ConfigurationProperties 注释的类型

这种配置风格与 SpringApplication 外部 YAML 配置特别配合,如以下示例所示:

my:
  service:
    remote-address: 192.168.1.1
    security:
      username: "admin"
      roles:
      - "USER"
      - "ADMIN"

要使用 @ConfigurationProperties bean,可以像任何其他bean一样注入它们,如以下示例所示:

Java
import org.springframework.stereotype.Service;

@Service
public class MyService {

    private final MyProperties properties;

    public MyService(MyProperties properties) {
        this.properties = properties;
    }

    public void openConnection() {
        Server server = new Server(this.properties.getRemoteAddress());
        server.start();
        // ...
    }

    // ...

}
Kotlin
import org.springframework.stereotype.Service

@Service
class MyService(val properties: MyProperties) {

    fun openConnection() {
        val server = Server(properties.remoteAddress)
        server.start()
        // ...
    }

    // ...

}
使用 @ConfigurationProperties 还可以生成元数据文件,供IDE用于为您自己的键提供自动完成。有关详细信息,请参阅 附录

2.8.5. 第三方配置

除了在类上使用 @ConfigurationProperties 注释外,还可以在公共 @Bean 方法上使用它。当您想要将属性绑定到您无法控制的第三方组件时,这样做尤其有用。

要从 Environment 属性配置bean,请将 @ConfigurationProperties 添加到其bean注册中,如以下示例所示:

Java
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false)
public class ThirdPartyConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "another")
    public AnotherComponent anotherComponent() {
        return new AnotherComponent();
    }

}
Kotlin
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration(proxyBeanMethods = false)
class ThirdPartyConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "another")
    fun anotherComponent(): AnotherComponent = AnotherComponent()

}

使用 another 前缀定义的任何JavaBean属性都会以类似于上面 SomeProperties 示例的方式映射到该 AnotherComponent bean。

2.8.6. 宽松绑定

Spring Boot对将Environment属性绑定到@ConfigurationProperties bean使用了一些宽松的规则,因此Environment属性名称与bean属性名称之间不需要完全匹配。常见的例子包括短横线分隔的环境属性(例如,context-path绑定到contextPath)和大写的环境属性(例如,PORT绑定到port)。

例如,考虑以下的@ConfigurationProperties类:

Java
import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "my.main-project.person")
public class MyPersonProperties {

    private String firstName;

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

}
Kotlin
import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties(prefix = "my.main-project.person")
class MyPersonProperties {

    var firstName: String? = null

}

使用上述代码,以下属性名称都可以使用:

表3. 宽松绑定
属性 说明

my.main-project.person.first-name

Kebab格式,推荐在.properties和YAML文件中使用。

my.main-project.person.firstName

标准的驼峰格式。

my.main-project.person.first_name

下划线表示法,是.properties和YAML文件中的另一种格式。

MY_MAINPROJECT_PERSON_FIRSTNAME

大写格式,推荐在使用系统环境变量时使用。

注解prefix必须使用kebab格式(小写并用-分隔,例如my.main-project.person)。
表4. 每个属性源的宽松绑定规则
属性源 简单 列表

属性文件

驼峰格式、kebab格式或下划线表示法

使用[ ]或逗号分隔值的标准列表语法

YAML文件

驼峰格式、kebab格式或下划线表示法

标准的YAML列表语法或逗号分隔值

环境变量

使用下划线作为分隔符的大写格式(参见从环境变量绑定

用下划线包围的数字值(参见从环境变量绑定

系统属性

驼峰格式、kebab格式或下划线表示法

使用[ ]或逗号分隔值的标准列表语法

我们建议尽可能将属性存储在小写kebab格式中,例如my.person.first-name=Rod
绑定映射

当绑定到Map属性时,您可能需要使用特殊的括号表示法,以便保留原始的key值。如果键没有被[]包围,那么任何非字母数字、-.的字符都将被移除。

例如,考虑将以下属性绑定到Map<String,String>

属性
my.map.[/key1]=value1
my.map.[/key2]=value2
my.map./key3=value3
Yaml
my:
  map:
    "[/key1]": "value1"
    "[/key2]": "value2"
    "/key3": "value3"
对于YAML文件,括号需要用引号括起来,以便正确解析键。

上述属性将绑定到一个Map,其中/key1/key2key3是映射中的键。由于key3没有被方括号包围,因此斜杠已被移除。

当绑定到标量值时,具有.的键不需要被[]包围。标量值包括枚举和java.lang包中的所有类型,除了Object。将a.b=c绑定到Map<String, String>将保留键中的.并返回一个包含条目{"a.b"="c"}的Map。对于其他类型,如果您的key包含.,则需要使用括号表示法。例如,将a.b=c绑定到Map<String, Object>将返回一个包含条目{"a"={"b"="c"}}的Map,而[a.b]=c将返回一个包含条目{"a.b"="c"}的Map。

从环境变量绑定

大多数操作系统对可以用于环境变量的名称施加了严格的规则。例如,Linux shell变量只能包含字母(azAZ)、数字(09)或下划线字符(_)。按照惯例,Unix shell变量的名称也将以大写形式呈现。

Spring Boot的宽松绑定规则尽可能与这些命名限制兼容。

要将规范形式的属性名称转换为环境变量名称,可以遵循以下规则:

  • 用下划线(_)替换点(.)。

  • 删除任何破折号(-)。

  • 转换为大写。

例如,配置属性spring.main.log-startup-info将成为名为SPRING_MAIN_LOGSTARTUPINFO的环境变量。

在绑定到对象列表时,环境变量也可以使用。要绑定到List,元素编号应该在变量名中用下划线包围。

例如,配置属性my.service[0].other将使用名为MY_SERVICE_0_OTHER的环境变量。

缓存

宽松绑定使用缓存来提高性能。默认情况下,此缓存仅应用于不可变的属性源。要自定义此行为,例如启用对可变属性源的缓存,请使用ConfigurationPropertyCaching

2.8.7. 合并复杂类型

当列表在多个地方配置时,覆盖工作是通过替换整个列表来实现的。

例如,假设一个MyPojo对象,默认情况下namedescription属性为null。以下示例从MyProperties中公开了一个MyPojo对象列表:

Java
import java.util.ArrayList;
import java.util.List;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("my")
public class MyProperties {

    private final List<MyPojo> list = new ArrayList<>();

    public List<MyPojo> getList() {
        return this.list;
    }

}
Kotlin
import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties("my")
class MyProperties {

    val list: List<MyPojo> = ArrayList()

}

考虑以下配置:

属性
my.list[0].name=my name
my.list[0].description=my description
#---
spring.config.activate.on-profile=dev
my.list[0].name=my another name
Yaml
my:
  list:
  - name: "my name"
    description: "my description"
---
spring:
  config:
    activate:
      on-profile: "dev"
my:
  list:
  - name: "my another name"

如果dev配置文件未激活,则MyProperties.list包含一个MyPojo条目,如先前定义的。然而,如果启用了dev配置文件,则list仍然只包含一个条目(名称为my another name,描述为null)。此配置不会添加第二个MyPojo实例到列表中,也不会合并项目。

当在多个配置文件中指定List时,只使用优先级最高的一个。考虑以下示例:

属性
my.list[0].name=my name
my.list[0].description=my description
my.list[1].name=another name
my.list[1].description=another description
#---
spring.config.activate.on-profile=dev
my.list[0].name=my another name
Yaml
my:
  list:
  - name: "my name"
    description: "my description"
  - name: "another name"
    description: "another description"
---
spring:
  config:
    activate:
      on-profile: "dev"
my:
  list:
  - name: "my another name"

在上面的示例中,如果dev配置文件处于活动状态,则MyProperties.list包含一个MyPojo条目(名称为my another name,描述为null)。对于YAML,可以使用逗号分隔的列表和YAML列表完全覆盖列表的内容。

对于Map属性,可以绑定来自多个来源的属性值。但是,对于多个来源中相同的属性,只使用优先级最高的一个。以下示例从MyProperties中公开了一个Map<String, MyPojo>

Java
import java.util.LinkedHashMap;
import java.util.Map;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("my")
public class MyProperties {

    private final Map<String, MyPojo> map = new LinkedHashMap<>();

    public Map<String, MyPojo> getMap() {
        return this.map;
    }

}
Kotlin
import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties("my")
class MyProperties {

    val map: Map<String, MyPojo> = LinkedHashMap()

}

考虑以下配置:

属性
my.map.key1.name=my name 1
my.map.key1.description=my description 1
#---
spring.config.activate.on-profile=dev
my.map.key1.name=dev name 1
my.map.key2.name=dev name 2
my.map.key2.description=dev description 2
Yaml
my:
  map:
    key1:
      name: "my name 1"
      description: "my description 1"
---
spring:
  config:
    activate:
      on-profile: "dev"
my:
  map:
    key1:
      name: "dev name 1"
    key2:
      name: "dev name 2"
      description: "dev description 2"

如果dev配置文件未激活,则MyProperties.map包含一个键为key1的条目(名称为my name 1,描述为my description 1)。然而,如果启用了dev配置文件,则map包含两个键为key1(名称为dev name 1,描述为my description 1)和key2(名称为dev name 2,描述为dev description 2)的条目。

上述合并规则适用于所有属性来源,而不仅限于文件。

2.8.8. 属性转换

Spring Boot在绑定到@ConfigurationProperties bean时,会尝试将外部应用程序属性强制转换为正确的类型。如果需要自定义类型转换,可以提供一个ConversionService bean(使用名为conversionService的bean)或自定义属性编辑器(通过CustomEditorConfigurer bean)或自定义Converters(使用标记为@ConfigurationPropertiesBinding的bean定义)。

由于此bean在应用程序生命周期的早期阶段被请求,确保限制您的ConversionService使用的依赖项。通常,您需要的任何依赖项可能在创建时尚未完全初始化。如果自定义ConversionService不是配置键强制转换所必需的,并且仅依赖于使用@ConfigurationPropertiesBinding限定的自定义转换器,则可能需要重命名您的自定义ConversionService
转换持续时间

Spring Boot专门支持表达持续时间。如果公开一个java.time.Duration属性,在应用程序属性中可用以下格式:

  • 常规的long表示(使用毫秒作为默认单位,除非指定了@DurationUnit

  • 标准的ISO-8601格式(由java.time.Duration使用)

  • 一个更易读的格式,其中值和单位是耦合的(10s表示10秒)

考虑以下示例:

Java
import java.time.Duration;
import java.time.temporal.ChronoUnit;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DurationUnit;

@ConfigurationProperties("my")
public class MyProperties {

    @DurationUnit(ChronoUnit.SECONDS)
    private Duration sessionTimeout = Duration.ofSeconds(30);

    private Duration readTimeout = Duration.ofMillis(1000);

    // getters / setters...

    public Duration getSessionTimeout() {
        return this.sessionTimeout;
    }

    public void setSessionTimeout(Duration sessionTimeout) {
        this.sessionTimeout = sessionTimeout;
    }

    public Duration getReadTimeout() {
        return this.readTimeout;
    }

    public void setReadTimeout(Duration readTimeout) {
        this.readTimeout = readTimeout;
    }

}
Kotlin
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.convert.DurationUnit
import java.time.Duration
import java.time.temporal.ChronoUnit

@ConfigurationProperties("my")
class MyProperties {

    @DurationUnit(ChronoUnit.SECONDS)
    var sessionTimeout = Duration.ofSeconds(30)

    var readTimeout = Duration.ofMillis(1000)

}

要指定30秒的会话超时,30PT30S30s都是等效的。500毫秒的读取超时可以用以下任何形式指定:500PT0.5S500ms

您还可以使用任何支持的单位。这些包括:

  • ns表示纳秒

  • us表示微秒

  • ms表示毫秒

  • s表示秒

  • m表示分钟

  • h表示小时

  • d表示天

默认单位是毫秒,可以使用@DurationUnit进行覆盖,就像上面的示例中所示。

如果您更喜欢使用构造函数绑定,可以公开相同的属性,如以下示例所示:

Java
import java.time.Duration;
import java.time.temporal.ChronoUnit;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.convert.DurationUnit;

@ConfigurationProperties("my")
public class MyProperties {

    // fields...

    private final Duration sessionTimeout;

    private final Duration readTimeout;

    public MyProperties(@DurationUnit(ChronoUnit.SECONDS) @DefaultValue("30s") Duration sessionTimeout,
            @DefaultValue("1000ms") Duration readTimeout) {
        this.sessionTimeout = sessionTimeout;
        this.readTimeout = readTimeout;
    }

    // getters...

    public Duration getSessionTimeout() {
        return this.sessionTimeout;
    }

    public Duration getReadTimeout() {
        return this.readTimeout;
    }

}
Kotlin
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.bind.DefaultValue
import org.springframework.boot.convert.DurationUnit
import java.time.Duration
import java.time.temporal.ChronoUnit

@ConfigurationProperties("my")
class MyProperties(@param:DurationUnit(ChronoUnit.SECONDS) @param:DefaultValue("30s") val sessionTimeout: Duration,
        @param:DefaultValue("1000ms") val readTimeout: Duration)
如果要升级Long属性,请确保定义单位(使用@DurationUnit),除非单位是毫秒。这样做可以提供透明的升级路径,同时支持更丰富的格式。
转换周期

除了持续时间,Spring Boot还可以处理java.time.Period类型。应用程序属性中可以使用以下格式:

  • 常规的int表示(使用天作为默认单位,除非指定了@PeriodUnit

  • 标准的ISO-8601格式(由java.time.Period使用)

  • 一个更简单的格式,其中值和单位成对出现(1y3d表示1年和3天)

以下单位支持简单格式:

  • y表示年

  • m表示月

  • w表示周

  • d表示天

java.time.Period类型实际上从不存储周数,它是一个快捷方式,表示“7天”。
转换数据大小

Spring Framework有一个DataSize值类型,用于表示字节大小。如果您暴露一个DataSize属性,在应用程序属性中可用以下格式:

  • 常规的long表示(使用字节作为默认单位,除非指定了@DataSizeUnit

  • 更易读的格式,其中值和单位是耦合的(10MB表示10兆字节)

考虑以下示例:

Java
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.convert.DataSizeUnit;
import org.springframework.util.unit.DataSize;
import org.springframework.util.unit.DataUnit;

@ConfigurationProperties("my")
public class MyProperties {

    @DataSizeUnit(DataUnit.MEGABYTES)
    private DataSize bufferSize = DataSize.ofMegabytes(2);

    private DataSize sizeThreshold = DataSize.ofBytes(512);

    // getters/setters...

    public DataSize getBufferSize() {
        return this.bufferSize;
    }

    public void setBufferSize(DataSize bufferSize) {
        this.bufferSize = bufferSize;
    }

    public DataSize getSizeThreshold() {
        return this.sizeThreshold;
    }

    public void setSizeThreshold(DataSize sizeThreshold) {
        this.sizeThreshold = sizeThreshold;
    }

}
Kotlin
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.convert.DataSizeUnit
import org.springframework.util.unit.DataSize
import org.springframework.util.unit.DataUnit

@ConfigurationProperties("my")
class MyProperties {

    @DataSizeUnit(DataUnit.MEGABYTES)
    var bufferSize = DataSize.ofMegabytes(2)

    var sizeThreshold = DataSize.ofBytes(512)

}

要指定10兆字节的缓冲区大小,1010MB是等效的。可以将256字节的大小阈值指定为256256B

您还可以使用任何支持的单位。这些包括:

  • B代表字节

  • KB代表千字节

  • MB代表兆字节

  • GB代表千兆字节

  • TB代表太字节

默认单位是字节,可以使用@DataSizeUnit进行覆盖,就像上面的示例中所示。

如果您更喜欢使用构造函数绑定,可以暴露相同的属性,如下例所示:

Java
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import org.springframework.boot.convert.DataSizeUnit;
import org.springframework.util.unit.DataSize;
import org.springframework.util.unit.DataUnit;

@ConfigurationProperties("my")
public class MyProperties {

    // fields...

    private final DataSize bufferSize;

    private final DataSize sizeThreshold;

    public MyProperties(@DataSizeUnit(DataUnit.MEGABYTES) @DefaultValue("2MB") DataSize bufferSize,
            @DefaultValue("512B") DataSize sizeThreshold) {
        this.bufferSize = bufferSize;
        this.sizeThreshold = sizeThreshold;
    }

    // getters...

    public DataSize getBufferSize() {
        return this.bufferSize;
    }

    public DataSize getSizeThreshold() {
        return this.sizeThreshold;
    }

}
Kotlin
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.bind.DefaultValue
import org.springframework.boot.convert.DataSizeUnit
import org.springframework.util.unit.DataSize
import org.springframework.util.unit.DataUnit

@ConfigurationProperties("my")
class MyProperties(@param:DataSizeUnit(DataUnit.MEGABYTES) @param:DefaultValue("2MB") val bufferSize: DataSize,
        @param:DefaultValue("512B") val sizeThreshold: DataSize)
如果您正在升级一个Long属性,请确保定义单位(使用@DataSizeUnit),如果不是字节。这样做可以提供一个透明的升级路径,同时支持更丰富的格式。

2.8.9. @ConfigurationProperties 验证

Spring Boot尝试在使用Spring的@Validated注解对@ConfigurationProperties类进行注解时验证这些类。您可以直接在配置类上使用JSR-303 jakarta.validation约束注解。为此,请确保您的类路径上有符合JSR-303规范的实现,然后在字段上添加约束注解,如下例所示:

Java
import java.net.InetAddress;

import jakarta.validation.constraints.NotNull;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@ConfigurationProperties("my.service")
@Validated
public class MyProperties {

    @NotNull
    private InetAddress remoteAddress;

    // getters/setters...

    public InetAddress getRemoteAddress() {
        return this.remoteAddress;
    }

    public void setRemoteAddress(InetAddress remoteAddress) {
        this.remoteAddress = remoteAddress;
    }

}
Kotlin
import jakarta.validation.constraints.NotNull
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.validation.annotation.Validated
import java.net.InetAddress

@ConfigurationProperties("my.service")
@Validated
class MyProperties {

    var remoteAddress: @NotNull InetAddress? = null

}
您还可以通过对创建配置属性的@Bean方法进行@Validated注解来触发验证。

为了确保即使找不到属性,嵌套属性始终会触发验证,关联字段必须使用@Valid进行注解。以下示例基于前面的MyProperties示例:

Java
import java.net.InetAddress;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

@ConfigurationProperties("my.service")
@Validated
public class MyProperties {

    @NotNull
    private InetAddress remoteAddress;

    @Valid
    private final Security security = new Security();

    // getters/setters...

    public InetAddress getRemoteAddress() {
        return this.remoteAddress;
    }

    public void setRemoteAddress(InetAddress remoteAddress) {
        this.remoteAddress = remoteAddress;
    }

    public Security getSecurity() {
        return this.security;
    }

    public static class Security {

        @NotEmpty
        private String username;

        // getters/setters...

        public String getUsername() {
            return this.username;
        }

        public void setUsername(String username) {
            this.username = username;
        }

    }

}
Kotlin
import jakarta.validation.Valid
import jakarta.validation.constraints.NotEmpty
import jakarta.validation.constraints.NotNull
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.validation.annotation.Validated
import java.net.InetAddress

@ConfigurationProperties("my.service")
@Validated
class MyProperties {

    var remoteAddress: @NotNull InetAddress? = null

    @Valid
    val security = Security()

    class Security {

        @NotEmpty
        var username: String? = null

    }

}

您还可以通过创建名为configurationPropertiesValidator的bean定义来添加自定义的Spring Validator@Bean方法应声明为static。配置属性验证器在应用程序生命周期的早期创建,将@Bean方法声明为静态可以让bean在不实例化@Configuration类的情况下创建。这样做可以避免早期实例化可能引起的任何问题。

`spring-boot-actuator`模块包含一个端点,公开所有@ConfigurationProperties bean。将您的Web浏览器指向/actuator/configprops或使用等效的JMX端点。有关详细信息,请参阅"生产就绪功能"部分。

2.8.10. @ConfigurationProperties vs. @Value

@Value注解是核心容器功能,不提供与类型安全配置属性相同的功能。以下表格总结了@ConfigurationProperties@Value支持的功能:

功能 @ConfigurationProperties @Value

松散绑定

有限(请参阅下面的注意

元数据支持

SpEL表达式

如果您确实想使用@Value,我们建议您使用它们的规范形式(仅使用小写字母的kebab-case)引用属性名称。这将允许Spring Boot使用与松散绑定@ConfigurationProperties相同的逻辑。

例如,@Value("${demo.item-price}")将从application.properties文件中提取demo.item-pricedemo.itemPrice形式,以及系统环境中的DEMO_ITEMPRICE。如果您改用@Value("${demo.itemPrice}"),则不会考虑demo.item-priceDEMO_ITEMPRICE

如果为自己的组件定义一组配置键,建议将它们分组在一个使用@ConfigurationProperties注解的POJO中。这样做将为您提供结构化的、类型安全的对象,可以将其注入到您自己的bean中。

来自应用程序属性文件SpEL表达式在解析这些文件和填充环境时不会被处理。但是,可以在@Value中编写SpEL表达式。如果应用程序属性文件中的属性值是SpEL表达式,则在通过@Value消耗时将对其进行评估。

3. 配置文件

Spring配置文件提供了一种方法来分隔应用程序配置的部分,并使其仅在特定环境中可用。任何@Component@Configuration@ConfigurationProperties都可以标记为@Profile以限制其加载时间,如下例所示:

Java
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration(proxyBeanMethods = false)
@Profile("production")
public class ProductionConfiguration {

    // ...

}
Kotlin
import org.springframework.context.annotation.Configuration
import org.springframework.context.annotation.Profile

@Configuration(proxyBeanMethods = false)
@Profile("production")
class ProductionConfiguration {

    // ...

}
如果通过@EnableConfigurationProperties注册@ConfigurationProperties bean,而不是通过自动扫描,那么@Profile注解需要在具有@EnableConfigurationProperties注解的@Configuration类上指定。在扫描@ConfigurationProperties的情况下,@Profile可以在@ConfigurationProperties类本身上指定。

您可以使用spring.profiles.active Environment属性来指定哪些配置文件是活动的。您可以在本章前面描述的任何方式中的任何一种中指定该属性。例如,您可以将其包含在您的application.properties中,如下例所示:

属性
spring.profiles.active=dev,hsqldb
Yaml
spring:
  profiles:
    active: "dev,hsqldb"
--spring.profiles.active=dev,hsqldb

如果没有激活任何配置文件,则启用默认配置文件。默认配置文件的名称为default,可以使用spring.profiles.default Environment属性进行调整,如下例所示:

属性
spring.profiles.default=none
Yaml
spring:
  profiles:
    default: "none"

spring.profiles.activespring.profiles.default只能在非特定配置文件中使用。这意味着它们不能包含在特定配置文件或由spring.config.activate.on-profile激活的文档中。

例如,第二个文档配置是无效的:

属性
# 这个文档是有效的
spring.profiles.active=prod
#---
# 这个文档是无效的
spring.config.activate.on-profile=prod
spring.profiles.active=metrics
Yaml
# 这个文档是有效的
spring:
  profiles:
    active: "prod"
---
# 这个文档是无效的
spring:
  config:
    activate:
      on-profile: "prod"
  profiles:
    active: "metrics"

3.1. 添加活动配置文件

spring.profiles.active属性遵循与其他属性相同的排序规则:最高的PropertySource获胜。这意味着您可以在application.properties中指定活动配置文件,然后通过命令行开关替换它们。

添加到活动配置文件中而不是替换它们是有用的。可以使用 spring.profiles.include属性在 spring.profiles.active属性激活的配置文件之上添加活动配置文件。 SpringApplication入口点还具有用于设置附加配置文件的Java API。请参阅 SpringApplication中的 setAdditionalProfiles()方法。

例如,当运行具有以下属性的应用程序时,即使使用--spring.profiles.active开关运行,也会激活常见和本地配置文件:

属性
spring.profiles.include[0]=common
spring.profiles.include[1]=local
Yaml
spring:
  profiles:
    include:
      - "common"
      - "local"
spring.profiles.active类似,spring.profiles.include只能在非特定配置文件中使用。这意味着它不能包含在特定配置文件或由spring.config.activate.on-profile激活的文档中。

如果给定配置文件处于活动状态,则可以使用在下一节中描述的配置文件组来添加活动配置文件。

3.2. 配置文件组

有时,您在应用程序中定义和使用的配置文件太细粒化,使用起来变得繁琐。例如,您可能有proddbprodmq配置文件,用于独立启用数据库和消息功能。

为了解决这个问题,Spring Boot允许您定义配置文件组。配置文件组允许您为相关的一组配置文件定义逻辑名称。

例如,我们可以创建一个production组,其中包含我们的proddbprodmq配置文件。

属性
spring.profiles.group.production[0]=proddb
spring.profiles.group.production[1]=prodmq
Yaml
spring:
  profiles:
    group:
      production:
      - "proddb"
      - "prodmq"

现在,我们可以使用--spring.profiles.active=production启动我们的应用程序,以一次激活productionproddbprodmq配置文件。

3.3. 编程设置配置文件

SpringApplication.setAdditionalProfiles(…​)来编程设置活动配置文件。还可以通过使用Spring的 ConfigurableEnvironment接口来激活配置文件。

3.4. 特定配置文件

特定配置文件的变体,包括application.properties(或application.yaml)和通过@ConfigurationProperties引用的文件,被视为文件并加载。有关详细信息,请参见"特定配置文件"。

4. 日志

Spring Boot使用Commons Logging进行所有内部日志记录,但保留底层日志实现的开放性。为Java Util LoggingLog4j2Logback提供了默认配置。在每种情况下,日志记录器预先配置为使用控制台输出,还可以选择使用文件输出。

默认情况下,如果您使用“Starters”,则使用Logback进行日志记录。还包括适当的Logback路由,以确保使用Java Util Logging、Commons Logging、Log4J或SLF4J的依赖库都能正常工作。

Java有许多可用的日志框架。如果上面的列表让您感到困惑,请不用担心。通常情况下,您不需要更改日志依赖项,Spring Boot的默认设置就可以正常工作。
当将应用程序部署到Servlet容器或应用服务器时,使用Java Util Logging API执行的日志不会路由到应用程序的日志中。这可以防止容器或其他已部署到其中的应用程序执行的日志出现在应用程序的日志中。

4.1. 日志格式

Spring Boot的默认日志输出类似于以下示例:

2024-02-22T18:32:30.542Z  INFO 33203 --- [myapp] [           main] o.s.b.d.f.logexample.MyApplication       : 使用Java 17.0.10启动MyApplication,PID为33203(/opt/apps/myapp.jar由myuser在/opt/apps/中启动)
2024-02-22T18:32:30.547Z  INFO 33203 --- [myapp] [           main] o.s.b.d.f.logexample.MyApplication       : 未设置活动配置文件,回退到默认配置文件:"default"
2024-02-22T18:32:31.720Z  INFO 33203 --- [myapp] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat使用端口8080(http)初始化
2024-02-22T18:32:31.735Z  INFO 33203 --- [myapp] [           main] o.apache.catalina.core.StandardService   : 启动服务[Tomcat]
2024-02-22T18:32:31.735Z  INFO 33203 --- [myapp] [           main] o.apache.catalina.core.StandardEngine    : 启动Servlet引擎:[Apache Tomcat/10.1.19]
2024-02-22T18:32:31.804Z  INFO 33203 --- [myapp] [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : 初始化Spring嵌入式WebApplicationContext
2024-02-22T18:32:31.805Z  INFO 33203 --- [myapp] [           main] w.s.c.ServletWebServerApplicationContext : 根WebApplicationContext:初始化完成,耗时1176毫秒
2024-02-22T18:32:32.386Z  INFO 33203 --- [myapp] [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat在端口8080(http)上启动,上下文路径''
2024-02-22T18:32:32.401Z  INFO 33203 --- [myapp] [           main] o.s.b.d.f.logexample.MyApplication       : MyApplication在2.419秒内启动(进程运行时间为2.744秒)

输出以下内容:

  • 日期和时间:毫秒精度且易于排序。

  • 日志级别:ERRORWARNINFODEBUGTRACE

  • 进程ID。

  • 一个---分隔符,用于区分实际日志消息的开始。

  • 应用程序名称:用方括号括起来(仅在设置了spring.application.name时默认记录)

  • 线程名称:用方括号括起来(可能会在控制台输出中被截断)。

  • 相关ID:如果启用了跟踪(在上面的示例中未显示)

  • 记录器名称:通常是源类名(通常是缩写)。

  • 日志消息。

Logback没有FATAL级别。它被映射为ERROR
如果有spring.application.name属性但不希望记录它,可以将logging.include-application-name设置为false

4.2. 控制台输出

默认的日志配置会在写入时将消息回显到控制台。默认情况下,记录ERROR级别、WARN级别和INFO级别的消息。您还可以通过使用--debug标志启动应用程序来启用“调试”模式。

$ java -jar myapp.jar --debug
您还可以在application.properties中指定debug=true

启用调试模式后,一些核心记录器(嵌入式容器、Hibernate和Spring Boot)将配置为输出更多信息。启用调试模式不会配置您的应用程序以记录所有带有DEBUG级别的消息。

或者,您可以通过使用--trace标志(或在application.properties中使用trace=true)启动应用程序来启用“跟踪”模式。这样可以为一些核心记录器(嵌入式容器、Hibernate模式生成和整个Spring组合)启用跟踪日志记录。

4.2.1. 色彩编码输出

如果您的终端支持ANSI,将使用彩色输出来帮助阅读。您可以将spring.output.ansi.enabled设置为支持的值以覆盖自动检测。

通过使用%clr转换词来配置颜色编码。在其最简单的形式中,转换器根据日志级别对输出进行着色,如以下示例所示:

%clr(%5p)

以下表描述了日志级别与颜色的映射:

级别 颜色

FATAL

红色

ERROR

红色

WARN

黄色

INFO

绿色

DEBUG

绿色

TRACE

绿色

或者,您可以通过将其作为转换的选项提供来指定应使用的颜色或样式。例如,要使文本变为黄色,请使用以下设置:

%clr(%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}){yellow}

支持以下颜色和样式:

  • blue

  • cyan

  • faint

  • green

  • magenta

  • red

  • yellow

4.3. 文件输出

默认情况下,Spring Boot仅将日志记录到控制台,不会写入日志文件。如果您想要除控制台输出外还要写入日志文件,您需要设置logging.file.namelogging.file.path属性(例如,在您的application.properties中)。

以下表格显示了如何一起使用logging.*属性:

表5. 日志属性
logging.file.name logging.file.path 示例 描述

(无)

(无)

仅控制台记录。

特定文件

(无)

my.log

写入到指定的日志文件。名称可以是确切位置或相对于当前目录。

(无)

特定目录

/var/log

spring.log写入到指定目录。名称可以是确切位置或相对于当前目录。

当日志文件达到10 MB时会进行轮转,并且与控制台输出一样,默认记录ERROR级别、WARN级别和INFO级别的消息。

日志属性独立于实际的日志基础设施。因此,特定的配置键(例如Logback的logback.configurationFile)不受Spring Boot管理。

4.4. 文件轮转

如果您使用Logback,可以通过您的application.propertiesapplication.yaml文件来微调日志轮转设置。对于所有其他日志系统,您需要直接自行配置轮转设置(例如,如果您使用Log4j2,则可以添加一个log4j2.xmllog4j2-spring.xml文件)。

支持以下轮转策略属性:

名称 描述

logging.logback.rollingpolicy.file-name-pattern

用于创建日志归档的文件名模式。

logging.logback.rollingpolicy.clean-history-on-start

应用启动时是否进行日志归档清理。

logging.logback.rollingpolicy.max-file-size

日志文件在归档之前的最大大小。

logging.logback.rollingpolicy.total-size-cap

日志归档在被删除之前可以占用的最大大小。

logging.logback.rollingpolicy.max-history

要保留的归档日志文件的最大数量(默认为7)。

4.5. 日志级别

所有支持的日志系统都可以通过Spring的Environment(例如,在application.properties中)设置日志记录器级别,使用logging.level.<logger-name>=<level>,其中level是TRACE、DEBUG、INFO、WARN、ERROR、FATAL或OFF之一。可以使用logging.level.root配置root记录器。

以下示例显示了在application.properties中可能的日志设置:

属性
logging.level.root=warn
logging.level.org.springframework.web=debug
logging.level.org.hibernate=error
Yaml
logging:
  level:
    root: "warn"
    org.springframework.web: "debug"
    org.hibernate: "error"

还可以使用环境变量设置日志级别。例如,LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_WEB=DEBUGorg.springframework.web设置为DEBUG

上述方法仅适用于包级别的日志记录。由于松散绑定始终将环境变量转换为小写,因此无法以这种方式配置单个类的日志记录。如果需要为类配置日志记录,可以使用SPRING_APPLICATION_JSON变量。

4.6. 日志组

通常有必要将相关的记录器分组在一起,以便可以同时配置它们。例如,您可能经常更改所有与Tomcat相关的记录器的日志级别,但您可能不容易记住顶级包。

为了帮助解决这个问题,Spring Boot允许您在Spring的Environment中定义日志组。例如,以下是如何通过将其添加到您的application.properties中定义“tomcat”组:

属性
logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
Yaml
logging:
  group:
    tomcat: "org.apache.catalina,org.apache.coyote,org.apache.tomcat"

一旦定义,您可以通过一行更改组中所有记录器的级别:

属性
logging.level.tomcat=trace
Yaml
logging:
  level:
    tomcat: "trace"

Spring Boot包含以下预定义的日志组,可以直接使用:

名称 记录器

web

org.springframework.core.codec, org.springframework.http, org.springframework.web, org.springframework.boot.actuate.endpoint.web, org.springframework.boot.web.servlet.ServletContextInitializerBeans

sql

org.springframework.jdbc.core, org.hibernate.SQL, org.jooq.tools.LoggerListener

4.7. 使用日志关闭钩子

为了在应用程序终止时释放日志资源,提供了一个关闭钩子,当JVM退出时将触发日志系统清理。除非您的应用程序部署为war文件,否则此关闭钩子会自动注册。如果您的应用程序具有复杂的上下文层次结构,则关闭钩子可能无法满足您的需求。如果不满足需求,请禁用关闭钩子,并研究底层日志系统直接提供的选项。例如,Logback提供了上下文选择器,允许每个Logger在自己的上下文中创建。您可以使用logging.register-shutdown-hook属性来禁用关闭钩子。将其设置为false将禁用注册。您可以在application.propertiesapplication.yaml文件中设置该属性:

属性
logging.register-shutdown-hook=false
Yaml
logging:
  register-shutdown-hook: false

4.8. 自定义日志配置

通过在类路径中包含适当的库并提供适当的配置文件,可以激活各种日志系统,并可以通过在类路径的根目录或Spring Environment 属性 logging.config 指定的位置提供进一步的自定义。

您可以通过使用 org.springframework.boot.logging.LoggingSystem 系统属性来强制Spring Boot使用特定的日志系统。该值应为 LoggingSystem 实现的完全限定类名。您还可以通过使用值 none 完全禁用Spring Boot的日志配置。

由于日志是在创建 ApplicationContext 之前初始化的,因此无法通过Spring @Configuration 文件中的 @PropertySources 控制日志。更改日志系统或完全禁用它的唯一方法是通过系统属性。

根据您的日志系统,将加载以下文件:

日志系统 自定义

Logback

logback-spring.xmllogback-spring.groovylogback.xmllogback.groovy

Log4j2

log4j2-spring.xmllog4j2.xml

JDK(Java Util Logging)

logging.properties

在可能的情况下,我们建议您使用带有 -spring 变体的日志配置(例如,使用 logback-spring.xml 而不是 logback.xml)。如果使用标准配置位置,Spring 无法完全控制日志初始化。
Java Util Logging 存在已知的类加载问题,导致在从“可执行 jar”运行时出现问题。如果可能的话,我们建议您在从“可执行 jar”运行时避免使用它。

为了帮助自定义,一些其他属性从Spring Environment 转移到系统属性。这允许日志系统配置使用这些属性。例如,在 application.properties 中设置 logging.file.name 或将 LOGGING_FILE_NAME 设置为环境变量将导致设置 LOG_FILE 系统属性。转移的属性在以下表中描述:

Spring Environment 系统属性 注释

logging.exception-conversion-word

LOG_EXCEPTION_CONVERSION_WORD

记录异常时使用的转换词。

logging.file.name

LOG_FILE

如果定义了,则在默认日志配置中使用。

logging.file.path

LOG_PATH

如果定义了,则在默认日志配置中使用。

logging.pattern.console

CONSOLE_LOG_PATTERN

在控制台(stdout)上使用的日志模式。

logging.pattern.dateformat

LOG_DATEFORMAT_PATTERN

日志日期格式的附加器模式。

logging.charset.console

CONSOLE_LOG_CHARSET

用于控制台日志记录的字符集。

logging.threshold.console

CONSOLE_LOG_THRESHOLD

用于控制台日志记录的日志级别阈值。

logging.pattern.file

FILE_LOG_PATTERN

在文件中使用的日志模式(如果启用了 LOG_FILE)。

logging.charset.file

FILE_LOG_CHARSET

用于文件日志记录的字符集(如果启用了 LOG_FILE)。

logging.threshold.file

FILE_LOG_THRESHOLD

用于文件日志记录的日志级别阈值。

logging.pattern.level

LOG_LEVEL_PATTERN

在呈现日志级别时使用的格式(默认为 %5p)。

PID

PID

当前进程 ID(如果可能的话会被发现,且尚未定义为操作系统环境变量)。

如果使用Logback,则还会传输以下属性:

Spring Environment 系统属性 注释

logging.logback.rollingpolicy.file-name-pattern

LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN

滚动日志文件名称的模式(默认为 ${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz)。

logging.logback.rollingpolicy.clean-history-on-start

LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START

启动时是否清除归档日志文件。

logging.logback.rollingpolicy.max-file-size

LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE

最大日志文件大小。

logging.logback.rollingpolicy.total-size-cap

LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP

要保留的日志备份的总大小。

logging.logback.rollingpolicy.max-history

LOGBACK_ROLLINGPOLICY_MAX_HISTORY

要保留的归档日志文件的最大数量。

所有支持的日志系统在解析其配置文件时可以查阅系统属性。查看spring-boot.jar中的默认配置示例:

如果要在日志属性中使用占位符,应使用Spring Boot的语法,而不是底层框架的语法。特别是,如果使用Logback,应使用:作为属性名称和其默认值之间的分隔符,而不是使用:-

您可以通过仅覆盖LOG_LEVEL_PATTERN(或Logback中的logging.pattern.level)来向日志行添加MDC和其他临时内容。例如,如果使用logging.pattern.level=user:%X{user} %5p,则默认日志格式包含一个名为"user"的MDC条目(如果存在),如下例所示。

2019-08-30 12:30:04.031 user:someone INFO 22174 --- [  nio-8080-exec-0] demo.Controller
Handling authenticated request

4.9. Logback扩展

Spring Boot包含了一些Logback的扩展,可以帮助进行高级配置。您可以在logback-spring.xml配置文件中使用这些扩展。

由于标准的logback.xml配置文件加载过早,您无法在其中使用扩展。您需要使用logback-spring.xml或定义一个logging.config属性。
这些扩展不能与Logback的配置扫描一起使用。如果尝试这样做,在更改配置文件时会记录类似以下错误之一:
ERROR in ch.qos.logback.core.joran.spi.Interpreter@4:71 - [springProperty]没有适用的操作,当前ElementPath为[[configuration][springProperty]]
ERROR in ch.qos.logback.core.joran.spi.Interpreter@4:71 - [springProfile]没有适用的操作,当前ElementPath为[[configuration][springProfile]]

4.9.1. 特定配置文件

<springProfile>标签允许您根据活动的Spring配置文件可选地包含或排除配置部分。配置文件部分可以在<configuration>元素的任何位置支持。使用name属性指定接受配置的配置文件。 <springProfile>标签可以包含配置文件名称(例如staging)或配置文件表达式。配置文件表达式允许更复杂的配置文件逻辑,例如production & (eu-central | eu-west)。查看Spring框架参考指南以获取更多详细信息。以下示例显示了三个样本配置文件:

<springProfile name="staging">
    <!-- 当“staging”配置文件处于活动状态时启用的配置 -->
</springProfile>

<springProfile name="dev | staging">
    <!-- 当“dev”或“staging”配置文件处于活动状态时启用的配置 -->
</springProfile>

<springProfile name="!production">
    <!-- 当“production”配置文件未处于活动状态时启用的配置 -->
</springProfile>

4.9.2. 环境属性

<springProperty>标签允许您从Spring的Environment中公开属性,以在Logback中使用。如果您想要在Logback配置中访问application.properties文件中的值,这样做可能很有用。该标签的工作方式类似于Logback的标准<property>标签。但是,您需要指定属性的source(来自Environment)。如果您需要将属性存储在除local范围之外的地方,可以使用scope属性。如果需要一个回退值(以防属性未在Environment中设置),可以使用defaultValue属性。以下示例显示了如何公开用于在Logback中使用的属性:

<springProperty scope="context" name="fluentHost" source="myapp.fluentd.host"
        defaultValue="localhost"/>
<appender name="FLUENT" class="ch.qos.logback.more.appenders.DataFluentAppender">
    <remoteHost>${fluentHost}</remoteHost>
    ...
</appender>
必须以kebab case(例如my.property-name)指定source。但是,可以使用放松的规则将属性添加到Environment中。

4.10. Log4j2扩展

Spring Boot包含了一些Log4j2的扩展,可以帮助进行高级配置。您可以在任何log4j2-spring.xml配置文件中使用这些扩展。

由于标准的log4j2.xml配置文件加载过早,您无法在其中使用扩展。您需要使用log4j2-spring.xml或定义一个logging.config属性。
这些扩展取代了Log4J提供的Spring Boot支持。请确保不在构建中包含org.apache.logging.log4j:log4j-spring-boot模块。

4.10.1. 特定配置文件

<SpringProfile>标签允许您根据活动的Spring配置文件可选地包含或排除配置部分。配置文件部分可以在<Configuration>元素的任何位置支持。使用name属性指定接受配置的配置文件。 <SpringProfile>标签可以包含配置文件名称(例如staging)或配置文件表达式。配置文件表达式允许更复杂的配置文件逻辑,例如production & (eu-central | eu-west)。查看Spring框架参考指南以获取更多详细信息。以下示例显示了三个样本配置文件:

<SpringProfile name="staging">
    <!-- 当“staging”配置文件处于活动状态时启用的配置 -->
</SpringProfile>

<SpringProfile name="dev | staging">
    <!-- 当“dev”或“staging”配置文件处于活动状态时启用的配置 -->
</SpringProfile>

<SpringProfile name="!production">
    <!-- 当“production”配置文件未处于活动状态时启用的配置 -->
</SpringProfile>

4.10.2. 环境属性查找

如果您想要在Log4j2配置中引用Spring Environment中的属性,可以使用以spring:为前缀的查找。如果您想要在Log4j2配置中访问application.properties文件中的值,这样做可能很有用。

以下示例显示了如何设置一个名为applicationName的Log4j2属性,该属性从Spring Environment中读取spring.application.name

<Properties>
    <Property name="applicationName">${spring:spring.application.name}</Property>
</Properties>
查找键应以kebab case(例如my.property-name)指定。

4.10.3. Log4j2系统属性

Log4j2支持许多系统属性,可用于配置各种项目。例如,log4j2.skipJansi系统属性可用于配置ConsoleAppender是否尝试在Windows上使用Jansi输出流。

所有在Log4j2初始化后加载的系统属性都可以从Spring Environment中获取。例如,您可以将log4j2.skipJansi=false添加到您的application.properties文件中,以便ConsoleAppender在Windows上使用Jansi。

仅当系统属性和操作系统环境变量不包含正在加载的值时,才会考虑Spring Environment
在早期Log4j2初始化期间加载的系统属性无法引用Spring Environment。例如,Log4j2用于选择默认Log4j2实现的属性在Spring环境可用之前就已经使用。

5. 国际化

Spring Boot支持本地化消息,使您的应用程序可以满足不同语言偏好的用户。默认情况下,Spring Boot会在类路径的根目录下查找messages资源包的存在。

当配置的资源包的默认属性文件可用时(默认为messages.properties),自动配置将生效。如果您的资源包只包含特定语言的属性文件,则需要添加默认属性文件。如果找不到与任何配置的基本名称匹配的属性文件,将不会自动配置MessageSource

资源包的基本名称以及其他属性可以使用spring.messages命名空间进行配置,如下例所示:

属性
spring.messages.basename=messages,config.i18n.messages
spring.messages.fallback-to-system-locale=false
Yaml
spring:
  messages:
    basename: "messages,config.i18n.messages"
    fallback-to-system-locale: false
spring.messages.basename支持逗号分隔的位置列表,可以是包限定符或从类路径根目录解析的资源。

更多支持的选项,请参阅MessageSourceProperties

6. 面向切面编程

Spring Boot为面向切面编程(AOP)提供了自动配置。您可以在Spring框架参考文档中了解更多关于Spring中AOP的信息。

默认情况下,Spring Boot的自动配置配置Spring AOP使用CGLib代理。要改为使用JDK代理,将configprop:spring.aop.proxy-target-class设置为false

如果AspectJ在类路径上,Spring Boot的自动配置将自动启用AspectJ自动代理,无需使用@EnableAspectJAutoProxy

7. JSON

Spring Boot提供了与三个JSON映射库集成的功能:

  • Gson

  • Jackson

  • JSON-B

Jackson是首选且默认的库。

7.1. Jackson

提供了Jackson的自动配置,并且Jackson是spring-boot-starter-json的一部分。当类路径中存在Jackson时,将自动配置一个ObjectMapper bean。提供了几个配置属性用于自定义ObjectMapper的配置

7.1.1. 自定义序列化器和反序列化器

如果您使用Jackson来序列化和反序列化JSON数据,可能希望编写自己的JsonSerializerJsonDeserializer类。通常,自定义序列化器通过模块与Jackson进行注册,但Spring Boot提供了一个更简单的方式,即使用@JsonComponent注解直接注册Spring Beans。

您可以直接在JsonSerializerJsonDeserializerKeyDeserializer实现上使用@JsonComponent注解。您还可以将其用于包含序列化器/反序列化器的内部类的类,如下例所示:

Java
import java.io.IOException;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import org.springframework.boot.jackson.JsonComponent;

@JsonComponent
public class MyJsonComponent {

    public static class Serializer extends JsonSerializer<MyObject> {

        @Override
        public void serialize(MyObject value, JsonGenerator jgen, SerializerProvider serializers) throws IOException {
            jgen.writeStartObject();
            jgen.writeStringField("name", value.getName());
            jgen.writeNumberField("age", value.getAge());
            jgen.writeEndObject();
        }

    }

    public static class Deserializer extends JsonDeserializer<MyObject> {

        @Override
        public MyObject deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException {
            ObjectCodec codec = jsonParser.getCodec();
            JsonNode tree = codec.readTree(jsonParser);
            String name = tree.get("name").textValue();
            int age = tree.get("age").intValue();
            return new MyObject(name, age);
        }

    }

}
Kotlin
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonProcessingException
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import org.springframework.boot.jackson.JsonComponent
import java.io.IOException

@JsonComponent
class MyJsonComponent {

    class Serializer : JsonSerializer<MyObject>() {
        @Throws(IOException::class)
        override fun serialize(value: MyObject, jgen: JsonGenerator, serializers: SerializerProvider) {
            jgen.writeStartObject()
            jgen.writeStringField("name", value.name)
            jgen.writeNumberField("age", value.age)
            jgen.writeEndObject()
        }
    }

    class Deserializer : JsonDeserializer<MyObject>() {
        @Throws(IOException::class, JsonProcessingException::class)
        override fun deserialize(jsonParser: JsonParser, ctxt: DeserializationContext): MyObject {
            val codec = jsonParser.codec
            val tree = codec.readTree<JsonNode>(jsonParser)
            val name = tree["name"].textValue()
            val age = tree["age"].intValue()
            return MyObject(name, age)
        }
    }

}

所有@JsonComponent bean都会自动注册到ApplicationContext中。由于@JsonComponent是元注解@Component,因此通常的组件扫描规则也适用。

Spring Boot还提供了JsonObjectSerializerJsonObjectDeserializer基类,用于在序列化对象时提供与标准Jackson版本有用的替代方案。有关详细信息,请参阅Javadoc中的JsonObjectSerializerJsonObjectDeserializer

上面的示例可以重写为使用JsonObjectSerializer/JsonObjectDeserializer,如下所示:

Java
import java.io.IOException;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.ObjectCodec;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.SerializerProvider;

import org.springframework.boot.jackson.JsonComponent;
import org.springframework.boot.jackson.JsonObjectDeserializer;
import org.springframework.boot.jackson.JsonObjectSerializer;

@JsonComponent
public class MyJsonComponent {

    public static class Serializer extends JsonObjectSerializer<MyObject> {

        @Override
        protected void serializeObject(MyObject value, JsonGenerator jgen, SerializerProvider provider)
                throws IOException {
            jgen.writeStringField("name", value.getName());
            jgen.writeNumberField("age", value.getAge());
        }

    }

    public static class Deserializer extends JsonObjectDeserializer<MyObject> {

        @Override
        protected MyObject deserializeObject(JsonParser jsonParser, DeserializationContext context, ObjectCodec codec,
                JsonNode tree) throws IOException {
            String name = nullSafeValue(tree.get("name"), String.class);
            int age = nullSafeValue(tree.get("age"), Integer.class);
            return new MyObject(name, age);
        }

    }

}
Kotlin
`object`

import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.ObjectCodec
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.SerializerProvider
import org.springframework.boot.jackson.JsonComponent
import org.springframework.boot.jackson.JsonObjectDeserializer
import org.springframework.boot.jackson.JsonObjectSerializer
import java.io.IOException

@JsonComponent
class MyJsonComponent {

    class Serializer : JsonObjectSerializer<MyObject>() {
        @Throws(IOException::class)
        override fun serializeObject(value: MyObject, jgen: JsonGenerator, provider: SerializerProvider) {
            jgen.writeStringField("name", value.name)
            jgen.writeNumberField("age", value.age)
        }
    }

    class Deserializer : JsonObjectDeserializer<MyObject>() {
        @Throws(IOException::class)
        override fun deserializeObject(jsonParser: JsonParser, context: DeserializationContext,
                codec: ObjectCodec, tree: JsonNode): MyObject {
            val name = nullSafeValue(tree["name"], String::class.java)
            val age = nullSafeValue(tree["age"], Int::class.java)
            return MyObject(name, age)
        }
    }

}

7.1.2. Mixins

Jackson支持混合,可用于将附加注解混合到已在目标类上声明的注解中。Spring Boot的Jackson自动配置将扫描应用程序的包,查找使用@JsonMixin注解的类,并将它们注册到自动配置的ObjectMapper中。注册由Spring Boot的JsonMixinModule执行。

7.2. Gson

提供了Gson的自动配置。当类路径中存在Gson时,将自动配置一个Gson bean。提供了几个spring.gson.*配置属性,用于自定义配置。为了更多控制,可以使用一个或多个GsonBuilderCustomizer bean。

7.3. JSON-B

提供了JSON-B的自动配置。当类路径中存在JSON-B API和实现时,将自动配置一个Jsonb bean。首选的JSON-B实现是Eclipse Yasson,提供了依赖管理。

8. 任务执行和调度

在上下文中没有Executor bean的情况下,Spring Boot会自动配置一个AsyncTaskExecutor。当启用虚拟线程(使用Java 21+和将spring.threads.virtual.enabled设置为true)时,这将是一个使用虚拟线程的SimpleAsyncTaskExecutor。否则,它将是一个具有合理默认值的ThreadPoolTaskExecutor。在任何情况下,自动配置的执行器将自动用于以下操作:

  • 异步任务执行(@EnableAsync

  • Spring for GraphQL中控制器方法返回值Callable的异步处理

  • Spring MVC的异步请求处理

  • Spring WebFlux的阻塞执行支持

如果在上下文中定义了自定义Executor,常规任务执行(即@EnableAsync)和Spring for GraphQL都将使用它。但是,Spring MVC和Spring WebFlux支持仅在它是AsyncTaskExecutor实现(命名为applicationTaskExecutor)时才会使用它。根据您的目标安排,您可以将您的Executor更改为AsyncTaskExecutor,或者定义一个包装您自定义ExecutorAsyncTaskExecutorAsyncConfigurer

自动配置的ThreadPoolTaskExecutorBuilder允许您轻松创建实例,以复制默认情况下自动配置的操作。

当自动配置ThreadPoolTaskExecutor时,线程池使用8个核心线程,可以根据负载增长和收缩。这些默认设置可以通过spring.task.execution命名空间进行微调,如下例所示:

属性
spring.task.execution.pool.max-size=16
spring.task.execution.pool.queue-capacity=100
spring.task.execution.pool.keep-alive=10s
Yaml
spring:
  task:
    execution:
      pool:
        max-size: 16
        queue-capacity: 100
        keep-alive: "10s"

这将使线程池使用有界队列,因此当队列已满(100个任务)时,线程池增加到最大16个线程。线程池的缩减更为积极,因为当线程空闲超过10秒时(而不是默认的60秒),线程将被回收。

如果需要与定时任务执行关联,调度程序也可以自动配置(例如使用@EnableScheduling)。

如果启用了虚拟线程(使用Java 21+和将spring.threads.virtual.enabled设置为true),这将是一个使用虚拟线程的SimpleAsyncTaskScheduler。这个SimpleAsyncTaskScheduler将忽略任何与池相关的属性。

如果未启用虚拟线程,它将是一个具有合理默认值的ThreadPoolTaskScheduler。默认情况下,ThreadPoolTaskScheduler使用一个线程,其设置可以通过spring.task.scheduling命名空间进行微调,如下例所示:

属性
spring.task.scheduling.thread-name-prefix=scheduling-
spring.task.scheduling.pool.size=2
Yaml
spring:
  task:
    scheduling:
      thread-name-prefix: "scheduling-"
      pool:
        size: 2

如果需要创建自定义执行器或调度程序,则上下文中会提供一个ThreadPoolTaskExecutorBuilder bean、一个SimpleAsyncTaskExecutorBuilder bean、一个ThreadPoolTaskSchedulerBuilder bean和一个SimpleAsyncTaskSchedulerBuilder bean。如果启用了虚拟线程(使用Java 21+和将spring.threads.virtual.enabled设置为true),SimpleAsyncTaskExecutorBuilderSimpleAsyncTaskSchedulerBuilder bean会自动配置为使用虚拟线程。

9. 测试

Spring Boot提供了许多实用工具和注解,可帮助测试应用程序。测试支持由两个模块提供:spring-boot-test包含核心项目,spring-boot-test-autoconfigure支持测试的自动配置。

大多数开发人员使用spring-boot-starter-test“Starter”,它导入了Spring Boot测试模块以及JUnit Jupiter、AssertJ、Hamcrest和许多其他有用的库。

如果您有使用JUnit 4的测试,可以使用JUnit 5的vintage引擎来运行它们。要使用vintage引擎,请添加对junit-vintage-engine的依赖,如下例所示:

<dependency>
    <groupId>org.junit.vintage</groupId>
    <artifactId>junit-vintage-engine</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

hamcrest-core被排除,而是使用org.hamcrest:hamcrest,这是spring-boot-starter-test的一部分。

9.1. 测试范围依赖

spring-boot-starter-test“Starter”(在test scope中)包含以下提供的库:

  • JUnit 5:Java应用程序单元测试的事实标准。

  • Spring Test & Spring Boot Test:Spring Boot应用程序的实用程序和集成测试支持。

  • AssertJ:流畅的断言库。

  • Hamcrest:匹配器对象库(也称为约束或谓词)。

  • Mockito:Java模拟框架。

  • JSONassert:用于JSON的断言库。

  • JsonPath:JSON的XPath。

  • Awaitility:用于测试异步系统的库。

通常在编写测试时,我们发现这些常见库很有用。如果这些库不符合您的需求,您可以添加自己的其他测试依赖。

9.2. 测试Spring应用程序

依赖注入的一个主要优势是应该使您的代码更容易进行单元测试。您可以使用new运算符实例化对象,甚至不涉及Spring。您还可以使用模拟对象而不是真实依赖。

通常,您需要超越单元测试并开始集成测试(使用Spring ApplicationContext)。能够执行集成测试而无需部署应用程序或连接到其他基础设施非常有用。

Spring框架包含一个专门用于此类集成测试的测试模块。您可以直接声明对org.springframework:spring-test的依赖,或者使用spring-boot-starter-test“Starter”来传递引入它。

如果您以前没有使用过spring-test模块,应该首先阅读Spring框架参考文档中的相关部分

9.3. 测试Spring Boot应用程序

Spring Boot应用程序是一个Spring ApplicationContext,因此,除了在普通的Spring上下文中通常做的事情外,测试它并不需要做太多特殊的工作。

只有当您使用SpringApplication创建上下文时,Spring Boot的外部属性、日志记录和其他功能才会默认安装在上下文中。

Spring Boot提供了一个@SpringBootTest注解,当您需要Spring Boot功能时,可以将其用作标准spring-test @ContextConfiguration注解的替代方案。该注解通过通过SpringApplication创建用于测试的ApplicationContext来工作。除了@SpringBootTest外,还提供了许多其他注解,用于测试应用程序的更具体部分

如果您使用JUnit 4,请不要忘记在测试中添加@RunWith(SpringRunner.class),否则注解将被忽略。如果您使用JUnit 5,则无需添加等效的@ExtendWith(SpringExtension.class),因为@SpringBootTest和其他@…​Test注解已经带有它。

默认情况下,@SpringBootTest不会启动服务器。您可以使用@SpringBootTestwebEnvironment属性进一步细化测试运行方式:

  • MOCK(默认):加载Web ApplicationContext并提供模拟Web环境。使用此注解时不会启动嵌入式服务器。如果您的类路径上没有Web环境,则此模式会自动回退到创建常规非Web ApplicationContext。它可与@AutoConfigureMockMvc@AutoConfigureWebTestClient一起用于对Web应用程序进行基于模拟的测试。

  • RANDOM_PORT:加载WebServerApplicationContext并提供真实的Web环境。启动嵌入式服务器并侦听随机端口。

  • DEFINED_PORT:加载WebServerApplicationContext并提供真实的Web环境。启动嵌入式服务器并侦听定义的端口(来自您的application.properties)或默认端口8080

  • NONE:通过使用SpringApplication加载ApplicationContext,但不提供任何Web环境(模拟或其他)。

如果您的测试是@Transactional,它会默认在每个测试方法结束时回滚事务。但是,由于使用RANDOM_PORTDEFINED_PORT会隐式提供真实的Servlet环境,HTTP客户端和服务器在单独的线程中运行,因此在单独的事务中运行。在这种情况下,服务器上启动的任何事务不会回滚。
@SpringBootTestwebEnvironment = WebEnvironment.RANDOM_PORT将在应用程序使用不同端口的情况下,还会在单独的随机端口上启动管理服务器。

9.3.1. 检测Web应用程序类型

如果Spring MVC可用,则配置常规基于MVC的应用程序上下文。如果只有Spring WebFlux,则我们将检测到并配置基于WebFlux的应用程序上下文。

如果两者都存在,则Spring MVC优先。如果您想在此场景中测试响应式Web应用程序,必须设置spring.main.web-application-type属性:

Java
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest(properties = "spring.main.web-application-type=reactive")
class MyWebFluxTests {

    // ...

}
Kotlin
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest(properties = ["spring.main.web-application-type=reactive"])
class MyWebFluxTests {

    // ...

}

9.3.2. 检测测试配置

如果您熟悉Spring测试框架,您可能习惯于使用@ContextConfiguration(classes=…​)来指定要加载的Spring @Configuration,或者经常在测试中使用嵌套的@Configuration类。

在测试Spring Boot应用程序时,通常不需要这样做。Spring Boot的@*Test注解会自动搜索您的主要配置,除非您明确定义一个。

搜索算法从包含测试的包开始,直到找到一个带有@SpringBootApplication@SpringBootConfiguration注解的类。只要您以合理的方式组织代码,通常会找到主要配置。

如果您使用测试注解来测试应用程序的更具体部分,应避免在主方法的应用程序类上添加特定于某个区域的配置设置。

@SpringBootApplication的基础组件扫描配置定义了用于确保切片正常工作的排除过滤器。如果在带有@SpringBootApplication注解的类上使用显式的@ComponentScan指令,请注意这些过滤器将被禁用。如果使用切片,应重新定义它们。

如果要自定义主要配置,可以使用嵌套的@TestConfiguration类。与嵌套的@Configuration类不同,后者将用于替代应用程序的主要配置,嵌套的@TestConfiguration类将用于补充应用程序的主要配置。

Spring的测试框架在测试之间缓存应用程序上下文。因此,只要您的测试共享相同的配置(无论如何发现),加载上下文的耗时过程只会发生一次。

9.3.3. 使用测试配置主方法

通常情况下,由@SpringBootTest发现的测试配置将是您的主@SpringBootApplication。在大多数结构良好的应用程序中,这个配置类还将包含用于启动应用程序的main方法。

例如,以下是典型Spring Boot应用程序的非常常见的代码模式:

Java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

}
Kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.docs.using.structuringyourcode.locatingthemainclass.MyApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class MyApplication

fun main(args: Array<String>) {
    runApplication<MyApplication>(*args)
}

在上面的示例中,main方法除了委托给SpringApplication.run之外并不执行任何其他操作。但是,也可以有一个更复杂的main方法,在调用SpringApplication.run之前应用自定义设置。

例如,以下是一个更改横幅模式并设置额外配置文件的应用程序:

Java
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(MyApplication.class);
        application.setBannerMode(Banner.Mode.OFF);
        application.setAdditionalProfiles("myprofile");
        application.run(args);
    }

}
Kotlin
import org.springframework.boot.Banner
import org.springframework.boot.runApplication
import org.springframework.boot.autoconfigure.SpringBootApplication

@SpringBootApplication
class MyApplication

fun main(args: Array<String>) {
    runApplication<MyApplication>(*args) {
        setBannerMode(Banner.Mode.OFF)
        setAdditionalProfiles("myprofile")
    }
}

由于main方法中的自定义设置可能会影响最终的ApplicationContext,因此您可能还希望在测试中使用main方法来创建用于测试的ApplicationContext。默认情况下,@SpringBootTest不会调用您的main方法,而是直接使用类本身来创建ApplicationContext

如果要更改此行为,可以将@SpringBootTestuseMainMethod属性更改为UseMainMethod.ALWAYSUseMainMethod.WHEN_AVAILABLE。当设置为ALWAYS时,如果找不到main方法,测试将失败。当设置为WHEN_AVAILABLE时,如果可用,将使用main方法,否则将使用标准加载机制。

例如,以下测试将调用MyApplicationmain方法以创建ApplicationContext。如果主方法设置了额外配置文件,则在ApplicationContext启动时将激活这些配置文件。

Java
import org.junit.jupiter.api.Test;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.UseMainMethod;

@SpringBootTest(useMainMethod = UseMainMethod.ALWAYS)
class MyApplicationTests {

    @Test
    void exampleTest() {
        // ...
    }

}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.UseMainMethod

@SpringBootTest(useMainMethod = UseMainMethod.ALWAYS)
class MyApplicationTests {

    @Test
    fun exampleTest() {
        // ...
    }

}

9.3.4. 排除测试配置

如果您的应用程序使用组件扫描(例如,如果您使用@SpringBootApplication@ComponentScan),您可能会发现您仅为特定测试而创建的顶级配置类意外地被随处引用。

正如我们之前所见,@TestConfiguration可以用于测试的内部类来自定义主配置。@TestConfiguration也可以用于顶级类。这样做表示该类不应该被扫描到。然后,您可以在需要的地方显式导入该类,如下例所示:

Java
import org.junit.jupiter.api.Test;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;

@SpringBootTest
@Import(MyTestsConfiguration.class)
class MyTests {

    @Test
    void exampleTest() {
        // ...
    }

}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.context.annotation.Import

@SpringBootTest
@Import(MyTestsConfiguration::class)
class MyTests {

    @Test
    fun exampleTest() {
        // ...
    }

}
如果直接使用@ComponentScan(即,不通过@SpringBootApplication),则需要在其中注册TypeExcludeFilter。有关详细信息,请参阅Javadoc
导入的@TestConfiguration会比内部类@TestConfiguration更早处理,并且导入的@TestConfiguration将在通过组件扫描找到的任何配置之前处理。一般来说,这种顺序上的差异没有明显影响,但如果您依赖于bean覆盖,则需要注意这一点。

9.3.5. 使用应用程序参数

如果您的应用程序需要参数,您可以使用@SpringBootTest注入它们,使用args属性。

Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.test.context.SpringBootTest;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(args = "--app.test=one")
class MyApplicationArgumentTests {

    @Test
    void applicationArgumentsPopulated(@Autowired ApplicationArguments args) {
        assertThat(args.getOptionNames()).containsOnly("app.test");
        assertThat(args.getOptionValues("app.test")).containsOnly("one");
    }

}
Kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.test.context.SpringBootTest

@SpringBootTest(args = ["--app.test=one"])
class MyApplicationArgumentTests {

    @Test
    fun applicationArgumentsPopulated(@Autowired args: ApplicationArguments) {
        assertThat(args.optionNames).containsOnly("app.test")
        assertThat(args.getOptionValues("app.test")).containsOnly("one")
    }

}

9.3.6. 使用模拟环境进行测试

默认情况下,@SpringBootTest 不会启动服务器,而是为测试Web端点设置一个模拟环境。

使用Spring MVC,我们可以使用MockMvcWebTestClient来查询我们的Web端点,如下例所示:

Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class MyMockMvcTests {

    @Test
    void testWithMockMvc(@Autowired MockMvc mvc) throws Exception {
        mvc.perform(get("/")).andExpect(status().isOk()).andExpect(content().string("Hello World"));
    }

    // 如果Spring WebFlux在类路径上,您可以使用WebTestClient驱动MVC测试
    @Test
    void testWithWebTestClient(@Autowired WebTestClient webClient) {
        webClient
                .get().uri("/")
                .exchange()
                .expectStatus().isOk()
                .expectBody(String.class).isEqualTo("Hello World");
    }

}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultMatchers

@SpringBootTest
@AutoConfigureMockMvc
class MyMockMvcTests {

    @Test
    fun testWithMockMvc(@Autowired mvc: MockMvc) {
        mvc.perform(MockMvcRequestBuilders.get("/")).andExpect(MockMvcResultMatchers.status().isOk)
            .andExpect(MockMvcResultMatchers.content().string("Hello World"))
    }

    // 如果Spring WebFlux在类路径上,您可以使用WebTestClient驱动MVC测试

    @Test
    fun testWithWebTestClient(@Autowired webClient: WebTestClient) {
        webClient
            .get().uri("/")
            .exchange()
            .expectStatus().isOk
            .expectBody<String>().isEqualTo("Hello World")
    }

}
如果您只想专注于Web层而不启动完整的ApplicationContext,考虑使用而不是使用@WebMvcTest

对于Spring WebFlux端点,您可以使用WebTestClient,如下例所示:

Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.reactive.server.WebTestClient;

@SpringBootTest
@AutoConfigureWebTestClient
class MyMockWebTestClientTests {

    @Test
    void exampleTest(@Autowired WebTestClient webClient) {
        webClient
            .get().uri("/")
            .exchange()
            .expectStatus().isOk()
            .expectBody(String.class).isEqualTo("Hello World");
    }

}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody

@SpringBootTest
@AutoConfigureWebTestClient
class MyMockWebTestClientTests {

    @Test
    fun exampleTest(@Autowired webClient: WebTestClient) {
        webClient
            .get().uri("/")
            .exchange()
            .expectStatus().isOk
            .expectBody<String>().isEqualTo("Hello World")
    }

}

在模拟环境中进行测试通常比在完整的servlet容器中运行更快。但是,由于模拟发生在Spring MVC层,依赖于较低级别servlet容器行为的代码无法直接使用MockMvc进行测试。

例如,Spring Boot的错误处理基于servlet容器提供的“错误页面”支持。这意味着,虽然您可以测试您的MVC层是否按预期引发和处理异常,但您无法直接测试特定的自定义错误页面是否被呈现。如果您需要测试这些较低级别的问题,您可以按照下一节中描述的方式启动一个完全运行的服务器。

9.3.7. 使用运行中的服务器进行测试

如果您需要启动一个完整运行中的服务器,我们建议您使用随机端口。如果您使用@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT),则每次运行测试时都会随机选择一个可用端口。

@LocalServerPort注解可用于将实际使用的端口注入到您的测试中。为了方便,需要向启动的服务器发出REST调用的测试还可以@Autowire一个WebTestClient,它会解析相对链接到运行中的服务器,并提供了一个专用的API来验证响应,如下例所示:

Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.test.web.reactive.server.WebTestClient;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class MyRandomPortWebTestClientTests {

    @Test
    void exampleTest(@Autowired WebTestClient webClient) {
        webClient
            .get().uri("/")
            .exchange()
            .expectStatus().isOk()
            .expectBody(String.class).isEqualTo("Hello World");
    }

}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class MyRandomPortWebTestClientTests {

    @Test
    fun exampleTest(@Autowired webClient: WebTestClient) {
        webClient
            .get().uri("/")
            .exchange()
            .expectStatus().isOk
            .expectBody<String>().isEqualTo("Hello World")
    }

}
WebTestClient也可以与模拟环境一起使用,通过在测试类上注释@AutoConfigureWebTestClient,从而无需运行服务器。

此设置需要在类路径中包含spring-webflux。如果您无法或不想添加webflux,Spring Boot还提供了一个TestRestTemplate工具:

Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class MyRandomPortTestRestTemplateTests {

    @Test
    void exampleTest(@Autowired TestRestTemplate restTemplate) {
        String body = restTemplate.getForObject("/", String.class);
        assertThat(body).isEqualTo("Hello World");
    }

}
Kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
import org.springframework.boot.test.web.client.TestRestTemplate

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class MyRandomPortTestRestTemplateTests {

    @Test
    fun exampleTest(@Autowired restTemplate: TestRestTemplate) {
        val body = restTemplate.getForObject("/", String::class.java)
        assertThat(body).isEqualTo("Hello World")
    }

}

9.3.8. 自定义WebTestClient

要自定义WebTestClient bean,请配置一个WebTestClientBuilderCustomizer bean。所有这些bean都将使用用于创建WebTestClientWebTestClient.Builder调用。

9.3.9. 使用JMX

由于测试上下文框架缓存上下文,JMX默认处于禁用状态,以防止相同组件在同一域上注册。如果这样的测试需要访问MBeanServer,考虑将其标记为脏:

Java
import javax.management.MBeanServer;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(properties = "spring.jmx.enabled=true")
@DirtiesContext
class MyJmxTests {

    @Autowired
    private MBeanServer mBeanServer;

    @Test
    void exampleTest() {
        assertThat(this.mBeanServer.getDomains()).contains("java.lang");
        // ...
    }

}
Kotlin
import javax.management.MBeanServer

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.annotation.DirtiesContext

@SpringBootTest(properties = ["spring.jmx.enabled=true"])
@DirtiesContext
class MyJmxTests(@Autowired val mBeanServer: MBeanServer) {

    @Test
    fun exampleTest() {
        assertThat(mBeanServer.domains).contains("java.lang")
        // ...
    }

}

9.3.10. 使用Observations

如果您使用@AutoConfigureObservability注解对切片测试进行注释,它会自动配置一个ObservationRegistry

9.3.11. 使用Metrics

无论您的类路径如何,除了内存支持的计量注册表外,在使用@SpringBootTest时不会自动配置计量注册表。

如果您需要将指标导出到不同的后端作为集成测试的一部分,请使用@AutoConfigureObservability对其进行注释。

如果您使用@AutoConfigureObservability注解对切片测试进行注释,它会自动配置一个内存中的MeterRegistry。在切片测试中不支持使用@AutoConfigureObservability注解导出数据。

9.3.12. 使用跟踪

无论您的类路径如何,当使用@SpringBootTest时,报告数据的跟踪组件不会自动配置。

如果您需要这些组件作为集成测试的一部分,请使用@AutoConfigureObservability对测试进行注释。

如果您创建了自己的报告组件(例如自定义的SpanExporterSpanHandler),并且不希望它们在测试中激活,您可以使用@ConditionalOnEnabledTracing注解来禁用它们。

如果您使用@AutoConfigureObservability注解对切片测试进行注释,它会自动配置一个无操作的Tracer。在切片测试中不支持使用@AutoConfigureObservability注解导出数据。

9.3.13. 模拟和监视Bean

在运行测试时,有时需要模拟应用程序上下文中的某些组件。例如,您可能有一个对某些远程服务进行封装的外观,在开发过程中无法访问该服务。模拟还可以在您想要模拟很难在真实环境中触发的故障时非常有用。

Spring Boot包含一个@MockBean注解,可用于为您的ApplicationContext中的Bean定义一个Mockito模拟。您可以使用该注解添加新的Bean或替换单个现有的Bean定义。该注解可以直接用于测试类、测试中的字段或@Configuration类和字段。当在字段上使用时,创建的模拟实例也会被注入。模拟Bean在每个测试方法后自动重置。

如果您的测试使用了Spring Boot的测试注解之一(例如@SpringBootTest),则此功能会自动启用。要在不同的安排中使用此功能,必须显式添加监听器,如下例所示:

Java
import org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener;
import org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;

@ContextConfiguration(classes = MyConfig.class)
@TestExecutionListeners({ MockitoTestExecutionListener.class, ResetMocksTestExecutionListener.class })
class MyTests {

    // ...

}
Kotlin
import org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener
import org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.TestExecutionListeners

@ContextConfiguration(classes = [MyConfig::class])
@TestExecutionListeners(
    MockitoTestExecutionListener::class,
    ResetMocksTestExecutionListener::class
)
class MyTests {

    // ...

}

以下示例将现有的RemoteService Bean替换为模拟实现:

Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;

@SpringBootTest
class MyTests {

    @Autowired
    private Reverser reverser;

    @MockBean
    private RemoteService remoteService;

    @Test
    void exampleTest() {
        given(this.remoteService.getValue()).willReturn("spring");
        String reverse = this.reverser.getReverseValue(); // 调用注入的RemoteService
        assertThat(reverse).isEqualTo("gnirps");
    }

}
Kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.mockito.BDDMockito.given
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean

@SpringBootTest
class MyTests(@Autowired val reverser: Reverser, @MockBean val remoteService: RemoteService) {

    @Test
    fun exampleTest() {
        given(remoteService.value).willReturn("spring")
        val reverse = reverser.reverseValue // 调用注入的RemoteService
        assertThat(reverse).isEqualTo("gnirps")
    }

}
@MockBean无法用于模拟在应用程序上下文刷新期间被使用的Bean的行为。在执行测试时,应用程序上下文刷新已经完成,配置模拟行为已经太迟了。我们建议在这种情况下使用@Bean方法来创建和配置模拟。

此外,您可以使用@SpyBean来包装任何现有Bean,使其成为Mockito的spy。有关详细信息,请参阅Javadoc

Spring的测试框架在测试之间缓存应用程序上下文,并为共享相同配置的测试重用上下文,但@MockBean@SpyBean的使用会影响缓存键,这很可能会增加上下文的数量。
如果您使用@SpyBean来监视具有按名称引用参数的@Cacheable方法的Bean,则必须使用-parameters编译应用程序。这样可以确保一旦Bean被监视,参数名称就可供缓存基础设施使用。
当您使用@SpyBean来监视由Spring代理的Bean时,在某些情况下可能需要移除Spring的代理,例如在使用givenwhen设置期望时。使用AopTestUtils.getTargetObject(yourProxiedSpy)来实现。

9.3.14. 自动配置测试

Spring Boot的自动配置系统适用于应用程序,但有时对于测试来说可能有点过于复杂。通常最好只加载所需配置的部分以测试应用程序的“片段”。例如,您可能希望测试Spring MVC控制器是否正确映射URL,并且不希望在这些测试中涉及数据库调用,或者您可能希望测试JPA实体,而在运行这些测试时不涉及Web层。

spring-boot-test-autoconfigure模块包含许多注解,可用于自动配置这种“片段”。它们每个都以类似的方式工作,提供一个@…​Test注解,加载ApplicationContext和一个或多个@AutoConfigure…​注解,可用于自定义自动配置设置。

每个“片段”将组件扫描限制为适当的组件,并加载一组非常受限制的自动配置类。如果需要排除其中一个,大多数@…​Test注解提供excludeAutoConfiguration属性。或者,您可以使用@ImportAutoConfiguration#exclude
在一个测试中使用多个“片段”,即在一个测试中使用多个@…​Test注解,不受支持。如果需要多个“片段”,请选择一个@…​Test注解,并手动包含其他“片段”的@AutoConfigure…​注解。
还可以将@AutoConfigure…​注解与标准的@SpringBootTest注解一起使用。如果不想“切片”应用程序但需要一些自动配置的测试Bean,则可以使用此组合。

9.3.15. 自动配置的JSON测试

为了测试对象的JSON序列化和反序列化是否按预期工作,您可以使用@JsonTest注解。 @JsonTest会自动配置可用的支持的JSON映射器,可以是以下库之一:

  • Jackson ObjectMapper,任何@JsonComponent bean和任何Jackson Module

  • Gson

  • Jsonb

可以在附录中找到由@JsonTest启用的自动配置列表。点击这里查看

如果需要配置自动配置的元素,可以使用@AutoConfigureJsonTesters注解。

Spring Boot包含基于AssertJ的辅助工具,可与JSONAssert和JsonPath库一起使用,以检查JSON是否符合预期。 JacksonTesterGsonTesterJsonbTesterBasicJsonTester类分别用于Jackson,Gson,Jsonb和字符串。在使用@JsonTest时,测试类上的任何辅助字段都可以@Autowired。以下示例显示了用于Jackson的测试类:

Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;

import static org.assertj.core.api.Assertions.assertThat;

@JsonTest
class MyJsonTests {

    @Autowired
    private JacksonTester<VehicleDetails> json;

    @Test
    void serialize() throws Exception {
        VehicleDetails details = new VehicleDetails("Honda", "Civic");
        // 对比与测试类相同包中的`.json`文件
        assertThat(this.json.write(details)).isEqualToJson("expected.json");
        // 或使用基于JSON路径的断言
        assertThat(this.json.write(details)).hasJsonPathStringValue("@.make");
        assertThat(this.json.write(details)).extractingJsonPathStringValue("@.make").isEqualTo("Honda");
    }

    @Test
    void deserialize() throws Exception {
        String content = "{\"make\":\"Ford\",\"model\":\"Focus\"}";
        assertThat(this.json.parse(content)).isEqualTo(new VehicleDetails("Ford", "Focus"));
        assertThat(this.json.parseObject(content).getMake()).isEqualTo("Ford");
    }

}
Kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.json.JsonTest
import org.springframework.boot.test.json.JacksonTester

@JsonTest
class MyJsonTests(@Autowired val json: JacksonTester<VehicleDetails>) {

    @Test
    fun serialize() {
        val details = VehicleDetails("Honda", "Civic")
        // 对比与测试类相同包中的`.json`文件
        assertThat(json.write(details)).isEqualToJson("expected.json")
        // 或使用基于JSON路径的断言
        assertThat(json.write(details)).hasJsonPathStringValue("@.make")
        assertThat(json.write(details)).extractingJsonPathStringValue("@.make").isEqualTo("Honda")
    }

    @Test
    fun deserialize() {
        val content = "{\"make\":\"Ford\",\"model\":\"Focus\"}"
        assertThat(json.parse(content)).isEqualTo(VehicleDetails("Ford", "Focus"))
        assertThat(json.parseObject(content).make).isEqualTo("Ford")
    }

}
JSON辅助类也可以直接在标准单元测试中使用。要这样做,如果不使用@JsonTest,则在@Before方法中调用辅助工具的initFields方法。

如果使用Spring Boot的AssertJ辅助工具来断言给定JSON路径上的数字值,根据类型,您可能无法使用isEqualTo。相反,您可以使用AssertJ的satisfies来断言该值是否符合给定条件。例如,以下示例断言实际数字是接近0.15的浮点值,偏移量为0.01

Java
@Test
void someTest() throws Exception {
    SomeObject value = new SomeObject(0.152f);
    assertThat(this.json.write(value)).extractingJsonPathNumberValue("@.test.numberValue")
        .satisfies((number) -> assertThat(number.floatValue()).isCloseTo(0.15f, within(0.01f)));
}
Kotlin
@Test
fun someTest() {
    val value = SomeObject(0.152f)
    assertThat(json.write(value)).extractingJsonPathNumberValue("@.test.numberValue")
        .satisfies(ThrowingConsumer { number ->
            assertThat(number.toFloat()).isCloseTo(0.15f, within(0.01f))
        })
}

9.3.16. 自动配置的Spring MVC测试

要测试Spring MVC控制器是否按预期工作,请使用@WebMvcTest注解。 @WebMvcTest自动配置Spring MVC基础架构,并将扫描的bean限制为@Controller@ControllerAdvice@JsonComponentConverterGenericConverterFilterHandlerInterceptorWebMvcConfigurerWebMvcRegistrationsHandlerMethodArgumentResolver。 当使用@WebMvcTest注解时,不会扫描常规的@Component@ConfigurationProperties bean。 可以使用@EnableConfigurationProperties来包含@ConfigurationProperties bean。

可以在附录中找到由@WebMvcTest启用的自动配置设置列表。点击这里
如果需要注册额外的组件,例如Jackson Module,可以通过在测试中使用@Import导入额外的配置类。
@WebMvcTest仅限于单个控制器,并与 @MockBean结合使用,为所需的协作者提供模拟实现。

@WebMvcTest还自动配置MockMvc。 Mock MVC提供了一种强大的方式,可以快速测试MVC控制器,而无需启动完整的HTTP服务器。

您还可以在非@WebMvcTest(例如@SpringBootTest)中使用@AutoConfigureMockMvcMockMvc进行自动配置。 以下示例使用MockMvc
Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserVehicleController.class)
class MyControllerTests {

    @Autowired
    private MockMvc mvc;

    @MockBean
    private UserVehicleService userVehicleService;

    @Test
    void testExample() throws Exception {
        given(this.userVehicleService.getVehicleDetails("sboot"))
            .willReturn(new VehicleDetails("Honda", "Civic"));
        this.mvc.perform(get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN))
            .andExpect(status().isOk())
            .andExpect(content().string("Honda Civic"));
    }

}
Kotlin
import org.junit.jupiter.api.Test
import org.mockito.BDDMockito.given
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultMatchers

@WebMvcTest(UserVehicleController::class)
class MyControllerTests(@Autowired val mvc: MockMvc) {

    @MockBean
    lateinit var userVehicleService: UserVehicleService

    @Test
    fun testExample() {
        given(userVehicleService.getVehicleDetails("sboot"))
            .willReturn(VehicleDetails("Honda", "Civic"))
        mvc.perform(MockMvcRequestBuilders.get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN))
            .andExpect(MockMvcResultMatchers.status().isOk)
            .andExpect(MockMvcResultMatchers.content().string("Honda Civic"))
    }

}
如果需要配置自动配置的元素(例如,何时应用Servlet过滤器),可以在@AutoConfigureMockMvc注解中使用属性。
@WebMvcTest还将扫描 WebSecurityConfigurer bean。 您可以使用Spring Security的测试支持,而不是完全禁用安全性进行此类测试。 有关如何使用Spring Security的 MockMvc支持的更多详细信息,请参阅此 howto.html how-to部分。

有时编写Spring MVC测试还不够;Spring Boot可以帮助您运行具有实际服务器的完整端到端测试

9.3.17. 自动配置的Spring WebFlux测试

为了测试Spring WebFlux控制器是否按预期工作,您可以使用@WebFluxTest注解。 @WebFluxTest会自动配置Spring WebFlux基础设施,并将扫描的bean限制为@Controller@ControllerAdvice@JsonComponentConverterGenericConverterWebFilterWebFluxConfigurer。 当使用@WebFluxTest注解时,常规的@Component@ConfigurationProperties bean不会被扫描。 可以使用@EnableConfigurationProperties来包含@ConfigurationProperties bean。

可以在附录中找到由@WebFluxTest启用的自动配置列表,点击这里
如果需要注册额外的组件,例如Jackson Module,可以使用@Import在测试中导入额外的配置类。
@WebFluxTest仅限于单个控制器,并与 @MockBean注解结合使用,为所需的协作者提供模拟实现。

@WebFluxTest还会自动配置WebTestClient,这为快速测试WebFlux控制器提供了一种强大的方式,而无需启动完整的HTTP服务器。

您还可以在非@WebFluxTest(例如@SpringBootTest)中通过使用@AutoConfigureWebTestClientWebTestClient进行自动配置。 以下示例显示了一个同时使用@WebFluxTestWebTestClient的类:
Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;

import static org.mockito.BDDMockito.given;

@WebFluxTest(UserVehicleController.class)
class MyControllerTests {

    @Autowired
    private WebTestClient webClient;

    @MockBean
    private UserVehicleService userVehicleService;

    @Test
    void testExample() {
        given(this.userVehicleService.getVehicleDetails("sboot"))
            .willReturn(new VehicleDetails("Honda", "Civic"));
        this.webClient.get().uri("/sboot/vehicle").accept(MediaType.TEXT_PLAIN).exchange()
            .expectStatus().isOk()
            .expectBody(String.class).isEqualTo("Honda Civic");
    }

}
Kotlin
import org.junit.jupiter.api.Test
import org.mockito.BDDMockito.given
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.MediaType
import org.springframework.test.web.reactive.server.WebTestClient
import org.springframework.test.web.reactive.server.expectBody

@WebFluxTest(UserVehicleController::class)
class MyControllerTests(@Autowired val webClient: WebTestClient) {

    @MockBean
    lateinit var userVehicleService: UserVehicleService

    @Test
    fun testExample() {
        given(userVehicleService.getVehicleDetails("sboot"))
            .willReturn(VehicleDetails("Honda", "Civic"))
        webClient.get().uri("/sboot/vehicle").accept(MediaType.TEXT_PLAIN).exchange()
            .expectStatus().isOk
            .expectBody<String>().isEqualTo("Honda Civic")
    }

}
此设置仅支持WebFlux应用程序,因为在模拟的Web应用程序中使用WebTestClient目前仅适用于WebFlux。
@WebFluxTest无法检测通过功能性Web框架注册的路由。 要在上下文中测试RouterFunction bean,请考虑使用@Import自行导入您的RouterFunction,或者使用@SpringBootTest
@WebFluxTest无法检测注册为@Bean类型的自定义安全配置SecurityWebFilterChain。 要在测试中包含该配置,您需要通过@Import导入注册该bean的配置,或者使用@SpringBootTest
有时编写Spring WebFlux测试是不够的;Spring Boot可以帮助您运行具有实际服务器的完整端到端测试

9.3.18. 自动配置的Spring GraphQL测试

Spring GraphQL提供了一个专门的测试支持模块;您需要将其添加到您的项目中:

Maven
<dependencies>
  <dependency>
    <groupId>org.springframework.graphql</groupId>
    <artifactId>spring-graphql-test</artifactId>
    <scope>test</scope>
  </dependency>
  <!-- 除非已经在编译范围内 -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>
Gradle
dependencies {
  testImplementation("org.springframework.graphql:spring-graphql-test")
  // 除非已经在实现配置中
  testImplementation("org.springframework.boot:spring-boot-starter-webflux")
}

这个测试模块提供了GraphQlTester。测试中广泛使用了这个测试器,因此请确保熟悉如何使用它。有GraphQlTester的变体,Spring Boot会根据测试的类型自动配置它们:

  • ExecutionGraphQlServiceTester在服务器端执行测试,不需要客户端或传输

  • HttpGraphQlTester使用连接到服务器的客户端执行测试,可以有或没有实时服务器

Spring Boot帮助您使用@GraphQlTest注解测试您的Spring GraphQL控制器@GraphQlTest会自动配置Spring GraphQL基础设施,而无需涉及任何传输或服务器。这将限制扫描的bean为@ControllerRuntimeWiringConfigurerJsonComponentConverterGenericConverterDataFetcherExceptionResolverInstrumentationGraphQlSourceBuilderCustomizer。当使用@GraphQlTest注解时,常规的@Component@ConfigurationProperties bean不会被扫描。可以使用@EnableConfigurationProperties来包含@ConfigurationProperties bean。

通过@GraphQlTest启用的自动配置列表可以在附录中找到。
@GraphQlTest仅限于一组控制器,并与 @MockBean注解结合使用,为所需的协作者提供模拟实现。

Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.docs.web.graphql.runtimewiring.GreetingController;
import org.springframework.boot.test.autoconfigure.graphql.GraphQlTest;
import org.springframework.graphql.test.tester.GraphQlTester;

@GraphQlTest(GreetingController.class)
class GreetingControllerTests {

    @Autowired
    private GraphQlTester graphQlTester;

    @Test
    void shouldGreetWithSpecificName() {
        this.graphQlTester.document("{ greeting(name: \"Alice\") } ")
            .execute()
            .path("greeting")
            .entity(String.class)
            .isEqualTo("Hello, Alice!");
    }

    @Test
    void shouldGreetWithDefaultName() {
        this.graphQlTester.document("{ greeting } ")
            .execute()
            .path("greeting")
            .entity(String.class)
            .isEqualTo("Hello, Spring!");
    }

}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.docs.web.graphql.runtimewiring.GreetingController
import org.springframework.boot.test.autoconfigure.graphql.GraphQlTest
import org.springframework.graphql.test.tester.GraphQlTester

@GraphQlTest(GreetingController::class)
internal class GreetingControllerTests {

    @Autowired
    lateinit var graphQlTester: GraphQlTester

    @Test
    fun shouldGreetWithSpecificName() {
        graphQlTester.document("{ greeting(name: \"Alice\") } ").execute().path("greeting").entity(String::class.java)
                .isEqualTo("Hello, Alice!")
    }

    @Test
    fun shouldGreetWithDefaultName() {
        graphQlTester.document("{ greeting } ").execute().path("greeting").entity(String::class.java)
                .isEqualTo("Hello, Spring!")
    }

}

@SpringBootTest测试是完整的集成测试,涉及整个应用程序。当使用随机或定义的端口时,会配置一个实时服务器,并自动贡献一个HttpGraphQlTester bean,以便您可以使用它来测试您的服务器。当配置了MOCK环境时,您还可以通过在测试类上加上@AutoConfigureHttpGraphQlTester来请求一个HttpGraphQlTester bean:

Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureHttpGraphQlTester;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.test.tester.HttpGraphQlTester;

@AutoConfigureHttpGraphQlTester
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class GraphQlIntegrationTests {

    @Test
    void shouldGreetWithSpecificName(@Autowired HttpGraphQlTester graphQlTester) {
        HttpGraphQlTester authenticatedTester = graphQlTester.mutate()
            .webTestClient((client) -> client.defaultHeaders((headers) -> headers.setBasicAuth("admin", "ilovespring")))
            .build();
        authenticatedTester.document("{ greeting(name: \"Alice\") } ")
            .execute()
            .path("greeting")
            .entity(String.class)
            .isEqualTo("Hello, Alice!");
    }

}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.graphql.tester.AutoConfigureHttpGraphQlTester
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.graphql.test.tester.HttpGraphQlTester
import org.springframework.http.HttpHeaders
import org.springframework.test.web.reactive.server.WebTestClient

@AutoConfigureHttpGraphQlTester
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class GraphQlIntegrationTests {

    @Test
    fun shouldGreetWithSpecificName(@Autowired graphQlTester: HttpGraphQlTester) {
        val authenticatedTester = graphQlTester.mutate()
            .webTestClient { client: WebTestClient.Builder ->
                client.defaultHeaders { headers: HttpHeaders ->
                    headers.setBasicAuth("admin", "ilovespring")
                }
            }.build()
        authenticatedTester.document("{ greeting(name: \"Alice\") } ").execute()
            .path("greeting").entity(String::class.java).isEqualTo("Hello, Alice!")
    }
}

9.3.19. 自动配置的Data Cassandra测试

@DataCassandraTest来测试Cassandra应用程序。默认情况下,它配置了一个 CassandraTemplate,扫描 @Table类,并配置Spring Data Cassandra存储库。当使用 @DataCassandraTest注解时,常规的 @Component@ConfigurationProperties bean不会被扫描。可以使用 @EnableConfigurationProperties来包含 @ConfigurationProperties bean。(有关在Spring Boot中使用Cassandra的更多信息,请参见" data.html"。)

通过@DataCassandraTest启用的自动配置设置列表可以在附录中找到。
Java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.cassandra.DataCassandraTest;

@DataCassandraTest
class MyDataCassandraTests {

    @Autowired
    private SomeRepository repository;

}
Kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.data.cassandra.DataCassandraTest

@DataCassandraTest
class MyDataCassandraTests(@Autowired val repository: SomeRepository)

9.3.20. 自动配置的Couchbase数据测试

您可以使用@DataCouchbaseTest来测试Couchbase应用程序。默认情况下,它配置了一个CouchbaseTemplateReactiveCouchbaseTemplate,扫描@Document类,并配置Spring Data Couchbase存储库。当使用@DataCouchbaseTest注解时,不会扫描常规的@Component@ConfigurationProperties bean。可以使用@EnableConfigurationProperties来包含@ConfigurationProperties bean。(有关在Spring Boot中使用Couchbase的更多信息,请参见本章前面的"data.html")

可以在附录中找到由@DataCouchbaseTest启用的自动配置设置列表。点击这里

以下示例显示了在Spring Boot中使用Couchbase测试的典型设置:

Java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.couchbase.DataCouchbaseTest;

@DataCouchbaseTest
class MyDataCouchbaseTests {

    @Autowired
    private SomeRepository repository;

    // ...

}
Kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.data.couchbase.DataCouchbaseTest

@DataCouchbaseTest
class MyDataCouchbaseTests(@Autowired val repository: SomeRepository) {

    // ...

}

9.3.21. 自动配置的Elasticsearch数据测试

您可以使用@DataElasticsearchTest来测试Elasticsearch应用程序。默认情况下,它配置了一个ElasticsearchRestTemplate,扫描@Document类,并配置Spring Data Elasticsearch存储库。当使用@DataElasticsearchTest注解时,不会扫描常规的@Component@ConfigurationProperties bean。可以使用@EnableConfigurationProperties来包含@ConfigurationProperties bean。(有关在Spring Boot中使用Elasticsearch的更多信息,请参见本章前面的"data.html")

可以在附录中找到由@DataElasticsearchTest启用的自动配置设置列表。点击这里

以下示例显示了在Spring Boot中使用Elasticsearch测试的典型设置:

Java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.elasticsearch.DataElasticsearchTest;

@DataElasticsearchTest
class MyDataElasticsearchTests {

    @Autowired
    private SomeRepository repository;

    // ...

}
Kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.data.elasticsearch.DataElasticsearchTest

@DataElasticsearchTest
class MyDataElasticsearchTests(@Autowired val repository: SomeRepository) {

    // ...

}

9.3.22. 自动配置的数据JPA测试

您可以使用@DataJpaTest注解来测试JPA应用程序。默认情况下,它会扫描@Entity类并配置Spring Data JPA存储库。如果类路径上有嵌入式数据库,则也会配置一个。通过将spring.jpa.show-sql属性设置为true,默认情况下会记录SQL查询。可以使用注解的showSql属性来禁用此功能。

当使用@DataJpaTest注解时,不会扫描常规的@Component@ConfigurationProperties bean。可以使用@EnableConfigurationProperties来包含@ConfigurationProperties bean。

可以在附录中找到由@DataJpaTest启用的自动配置设置列表。

默认情况下,数据JPA测试是事务性的,并在每个测试结束时回滚。有关更多详细信息,请参阅Spring Framework参考文档中的相关部分。如果这不是您想要的行为,可以按以下方式为测试或整个类禁用事务管理:

Java
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@DataJpaTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
class MyNonTransactionalTests {

    // ...

}
Kotlin
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional

@DataJpaTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
class MyNonTransactionalTests {

    // ...

}

数据JPA测试还可以注入一个TestEntityManager bean,它提供了一个专为测试设计的替代标准JPA EntityManager

TestEntityManager也可以通过添加@AutoConfigureTestEntityManager自动配置到任何基于Spring的测试类中。在这样做时,请确保您的测试在事务中运行,例如通过在测试类或方法上添加@Transactional

如果需要,还可以使用JdbcTemplate。以下示例展示了@DataJpaTest注解的使用:

Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;

import static org.assertj.core.api.Assertions.assertThat;

@DataJpaTest
class MyRepositoryTests {

    @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private UserRepository repository;

    @Test
    void testExample() {
        this.entityManager.persist(new User("sboot", "1234"));
        User user = this.repository.findByUsername("sboot");
        assertThat(user.getUsername()).isEqualTo("sboot");
        assertThat(user.getEmployeeNumber()).isEqualTo("1234");
    }

}
Kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager

@DataJpaTest
class MyRepositoryTests(@Autowired val entityManager: TestEntityManager, @Autowired val repository: UserRepository) {

    @Test
    fun testExample() {
        entityManager.persist(User("sboot", "1234"))
        val user = repository.findByUsername("sboot")
        assertThat(user?.username).isEqualTo("sboot")
        assertThat(user?.employeeNumber).isEqualTo("1234")
    }

}

通常,内存中的嵌入式数据库非常适合测试,因为它们速度快且不需要任何安装。但是,如果您更喜欢针对真实数据库运行测试,可以使用@AutoConfigureTestDatabase注解,如下例所示:

Java
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
class MyRepositoryTests {

    // ...

}
Kotlin
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class MyRepositoryTests {

    // ...

}

9.3.23. 自动配置的JDBC测试

@JdbcTest类似于@DataJpaTest,但适用于仅需要DataSource且不使用Spring Data JDBC的测试。默认情况下,它会配置一个内存中的嵌入式数据库和一个JdbcTemplate。当使用@JdbcTest注解时,不会扫描常规的@Component@ConfigurationProperties bean。可以使用@EnableConfigurationProperties来包含@ConfigurationProperties bean。

可以在附录中找到由@JdbcTest启用的自动配置列表。

默认情况下,JDBC测试是事务性的,并在每个测试结束时回滚。有关更多详细信息,请参阅Spring Framework参考文档中的相关部分。如果这不是您想要的行为,可以按以下方式为测试或整个类禁用事务管理:

Java
import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@JdbcTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
class MyTransactionalTests {

}
Kotlin
import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional

@JdbcTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
class MyTransactionalTests

如果您希望测试针对真实数据库运行,可以像对@DataJpaTest一样使用@AutoConfigureTestDatabase注解。(参见"自动配置的数据JPA测试")。

9.3.24. 自动配置的数据 JDBC 测试

@DataJdbcTest类似于@JdbcTest,但用于使用Spring Data JDBC存储库的测试。默认情况下,它配置一个内存嵌入式数据库、一个JdbcTemplate和Spring Data JDBC存储库。当使用@DataJdbcTest注解时,只会扫描AbstractJdbcConfiguration子类,不会扫描常规的@Component@ConfigurationProperties beans。可以使用@EnableConfigurationProperties来包含@ConfigurationProperties beans。

可以在附录中找到由@DataJdbcTest启用的自动配置列表。查看附录

默认情况下,Data JDBC 测试是事务性的,并在每个测试结束时回滚。有关更多详细信息,请参阅Spring Framework参考文档中的相关部分。如果这不是您想要的结果,您可以在测试或整个测试类中禁用事务管理,如@DataJdbcTest示例中所示。查看JDBC示例

如果您希望测试针对真实数据库运行,可以像对@DataJpaTest一样使用@AutoConfigureTestDatabase注解。(参见"自动配置的数据 JPA 测试")

9.3.25. 自动配置的数据 R2DBC 测试

@DataR2dbcTest类似于@DataJdbcTest,但用于使用Spring Data R2DBC存储库的测试。默认情况下,它配置一个内存嵌入式数据库、一个R2dbcEntityTemplate和Spring Data R2DBC存储库。当使用@DataR2dbcTest注解时,不会扫描常规的@Component@ConfigurationProperties beans。可以使用@EnableConfigurationProperties来包含@ConfigurationProperties beans。

可以在附录中找到由@DataR2dbcTest启用的自动配置列表。查看附录

默认情况下,Data R2DBC 测试不是事务性的。

如果您希望测试针对真实数据库运行,可以像对@DataJpaTest一样使用@AutoConfigureTestDatabase注解。(参见"自动配置的数据 JPA 测试")

9.3.26. 自动配置的 jOOQ 测试

您可以像使用@JdbcTest一样使用@JooqTest进行与 jOOQ 相关的测试。由于 jOOQ 在很大程度上依赖于与数据库模式对应的基于Java的模式,因此使用现有的DataSource。如果要将其替换为内存数据库,可以使用@AutoConfigureTestDatabase来覆盖这些设置。(有关在Spring Boot中使用jOOQ的更多信息,请参见"data.html")当使用@JooqTest注解时,不会扫描常规的@Component@ConfigurationProperties beans。可以使用@EnableConfigurationProperties来包含@ConfigurationProperties beans。

可以在附录中找到由@JooqTest启用的自动配置列表。查看附录

@JooqTest配置一个DSLContext。以下示例展示了@JooqTest注解的使用:

Java
import org.jooq.DSLContext;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jooq.JooqTest;

@JooqTest
class MyJooqTests {

    @Autowired
    private DSLContext dslContext;

    // ...

}
Kotlin
import org.jooq.DSLContext
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.jooq.JooqTest

@JooqTest
class MyJooqTests(@Autowired val dslContext: DSLContext) {

    // ...

}

jOOQ 测试默认是事务性的,并在每个测试结束时回滚。如果这不是您想要的结果,您可以在测试或整个测试类中禁用事务管理,如@JooqTest示例中所示。

9.3.27. 自动配置的数据 MongoDB 测试

您可以使用@DataMongoTest来测试MongoDB应用程序。默认情况下,它配置一个MongoTemplate,扫描@Document类,并配置Spring Data MongoDB存储库。当使用@DataMongoTest注解时,不会扫描常规的@Component@ConfigurationProperties beans。可以使用@EnableConfigurationProperties来包含@ConfigurationProperties beans。(有关在Spring Boot中使用MongoDB的更多信息,请参见"data.html")

可以在附录中找到由@DataMongoTest启用的自动配置设置列表。查看附录

以下类展示了@DataMongoTest注解的使用:

Java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
import org.springframework.data.mongodb.core.MongoTemplate;

@DataMongoTest
class MyDataMongoDbTests {

    @Autowired
    private MongoTemplate mongoTemplate;

    // ...

}
Kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest
import org.springframework.data.mongodb.core.MongoTemplate

@DataMongoTest
class MyDataMongoDbTests(@Autowired val mongoTemplate: MongoTemplate) {

    // ...

}

9.3.28. 自动配置的数据 Neo4j 测试

您可以使用 @DataNeo4jTest 来测试 Neo4j 应用程序。默认情况下,它会扫描 @Node 类,并配置 Spring Data Neo4j 仓库。当使用 @DataNeo4jTest 注解时,常规的 @Component@ConfigurationProperties bean 不会被扫描。可以使用 @EnableConfigurationProperties 来包含 @ConfigurationProperties bean。(有关在 Spring Boot 中使用 Neo4J 的更多信息,请参见 "data.html"。)

可以在附录中找到由 @DataNeo4jTest 启用的自动配置设置列表。点击此处查看

以下示例展示了在 Spring Boot 中使用 Neo4J 测试的典型设置:

Java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest;

@DataNeo4jTest
class MyDataNeo4jTests {

    @Autowired
    private SomeRepository repository;

    // ...

}
Kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest

@DataNeo4jTest
class MyDataNeo4jTests(@Autowired val repository: SomeRepository) {

    // ...

}

默认情况下,Data Neo4j 测试是事务性的,并在每个测试结束时回滚。有关更多详细信息,请参阅 Spring Framework 参考文档中的相关部分。如果这不是您想要的,可以禁用测试或整个类的事务管理,如下所示:

Java
import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@DataNeo4jTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
class MyDataNeo4jTests {

}
Kotlin
import org.springframework.boot.test.autoconfigure.data.neo4j.DataNeo4jTest
import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional

@DataNeo4jTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
class MyDataNeo4jTests
不支持在响应式访问中进行事务性测试。如果您正在使用这种风格,必须按照上述方式配置 @DataNeo4jTest 测试。

9.3.29. 自动配置的数据 Redis 测试

您可以使用 @DataRedisTest 来测试 Redis 应用程序。默认情况下,它会扫描 @RedisHash 类,并配置 Spring Data Redis 仓库。当使用 @DataRedisTest 注解时,常规的 @Component@ConfigurationProperties bean 不会被扫描。可以使用 @EnableConfigurationProperties 来包含 @ConfigurationProperties bean。(有关在 Spring Boot 中使用 Redis 的更多信息,请参见 "data.html"。)

可以在附录中找到由 @DataRedisTest 启用的自动配置设置列表。点击此处查看

以下示例展示了使用 @DataRedisTest 注解的情况:

Java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.redis.DataRedisTest;

@DataRedisTest
class MyDataRedisTests {

    @Autowired
    private SomeRepository repository;

    // ...

}
Kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.data.redis.DataRedisTest

@DataRedisTest
class MyDataRedisTests(@Autowired val repository: SomeRepository) {

    // ...

}

9.3.30. 自动配置的数据 LDAP 测试

您可以使用 @DataLdapTest 来测试 LDAP 应用程序。默认情况下,它会配置一个内存中的嵌入式 LDAP(如果可用),配置一个 LdapTemplate,扫描 @Entry 类,并配置 Spring Data LDAP 仓库。当使用 @DataLdapTest 注解时,常规的 @Component@ConfigurationProperties bean 不会被扫描。可以使用 @EnableConfigurationProperties 来包含 @ConfigurationProperties bean。(有关在 Spring Boot 中使用 LDAP 的更多信息,请参见 "data.html"。)

可以在附录中找到由 @DataLdapTest 启用的自动配置设置列表。点击此处查看

以下示例展示了使用 @DataLdapTest 注解的情况:

Java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.ldap.DataLdapTest;
import org.springframework.ldap.core.LdapTemplate;

@DataLdapTest
class MyDataLdapTests {

    @Autowired
    private LdapTemplate ldapTemplate;

    // ...

}
Kotlin
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.data.ldap.DataLdapTest
import org.springframework.ldap.core.LdapTemplate

@DataLdapTest
class MyDataLdapTests(@Autowired val ldapTemplate: LdapTemplate) {

    // ...

}

通常,内存中的嵌入式 LDAP 在测试中表现良好,因为它速度快且不需要任何开发人员安装。但是,如果您更喜欢针对真实 LDAP 服务器运行测试,您应该排除嵌入式 LDAP 自动配置,如下例所示:

Java
import org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration;
import org.springframework.boot.test.autoconfigure.data.ldap.DataLdapTest;

@DataLdapTest(excludeAutoConfiguration = EmbeddedLdapAutoConfiguration.class)
class MyDataLdapTests {

    // ...

}
Kotlin
import org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration
import org.springframework.boot.test.autoconfigure.data.ldap.DataLdapTest

@DataLdapTest(excludeAutoConfiguration = [EmbeddedLdapAutoConfiguration::class])
class MyDataLdapTests {

    // ...

}

9.3.31. 自动配置的REST客户端

您可以使用@RestClientTest注解来测试REST客户端。默认情况下,它会自动配置Jackson、GSON和Jsonb支持,配置一个RestTemplateBuilder和一个RestClient.Builder,并添加对MockRestServiceServer的支持。当使用@RestClientTest注解时,常规的@Component@ConfigurationProperties bean不会被扫描。可以使用@EnableConfigurationProperties来包含@ConfigurationProperties bean。

可以在附录中找到由@RestClientTest启用的自动配置设置列表。点击这里查看

要测试的特定bean应通过@RestClientTestvaluecomponents属性指定。

在测试bean中使用RestTemplateBuilder,并且在构建RestTemplate时调用了RestTemplateBuilder.rootUri(String rootUri),则在MockRestServiceServer的期望中应省略根URI,如下例所示:

Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

@RestClientTest(RemoteVehicleDetailsService.class)
class MyRestTemplateServiceTests {

    @Autowired
    private RemoteVehicleDetailsService service;

    @Autowired
    private MockRestServiceServer server;

    @Test
    void getVehicleDetailsWhenResultIsSuccessShouldReturnDetails() {
        this.server.expect(requestTo("/greet/details")).andRespond(withSuccess("hello", MediaType.TEXT_PLAIN));
        String greeting = this.service.callRestService();
        assertThat(greeting).isEqualTo("hello");
    }

}
Kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest
import org.springframework.http.MediaType
import org.springframework.test.web.client.MockRestServiceServer
import org.springframework.test.web.client.match.MockRestRequestMatchers
import org.springframework.test.web.client.response.MockRestResponseCreators

@RestClientTest(RemoteVehicleDetailsService::class)
class MyRestTemplateServiceTests(
    @Autowired val service: RemoteVehicleDetailsService,
    @Autowired val server: MockRestServiceServer) {

    @Test
    fun getVehicleDetailsWhenResultIsSuccessShouldReturnDetails() {
        server.expect(MockRestRequestMatchers.requestTo("/greet/details"))
            .andRespond(MockRestResponseCreators.withSuccess("hello", MediaType.TEXT_PLAIN))
        val greeting = service.callRestService()
        assertThat(greeting).isEqualTo("hello")
    }

}

在测试bean中使用RestClient.Builder,或者在使用RestTemplateBuilder时没有调用rootUri(String rootURI),则在MockRestServiceServer的期望中必须使用完整的URI,如下例所示:

Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

@RestClientTest(RemoteVehicleDetailsService.class)
class MyRestClientServiceTests {

    @Autowired
    private RemoteVehicleDetailsService service;

    @Autowired
    private MockRestServiceServer server;

    @Test
    void getVehicleDetailsWhenResultIsSuccessShouldReturnDetails() {
        this.server.expect(requestTo("https://example.com/greet/details"))
            .andRespond(withSuccess("hello", MediaType.TEXT_PLAIN));
        String greeting = this.service.callRestService();
        assertThat(greeting).isEqualTo("hello");
    }

}
Kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest
import org.springframework.http.MediaType
import org.springframework.test.web.client.MockRestServiceServer
import org.springframework.test.web.client.match.MockRestRequestMatchers
import org.springframework.test.web.client.response.MockRestResponseCreators

@RestClientTest(RemoteVehicleDetailsService::class)
class MyRestClientServiceTests(
    @Autowired val service: RemoteVehicleDetailsService,
    @Autowired val server: MockRestServiceServer) {

    @Test
    fun getVehicleDetailsWhenResultIsSuccessShouldReturnDetails() {
        server.expect(MockRestRequestMatchers.requestTo("https://example.com/greet/details"))
            .andRespond(MockRestResponseCreators.withSuccess("hello", MediaType.TEXT_PLAIN))
        val greeting = service.callRestService()
        assertThat(greeting).isEqualTo("hello")
    }

}

9.3.32. 自动配置的Spring REST Docs测试

您可以使用@AutoConfigureRestDocs注解在您的测试中使用Spring REST Docs与Mock MVC、REST Assured或WebTestClient。它消除了Spring REST Docs中JUnit扩展的需要。

@AutoConfigureRestDocs可用于覆盖默认的输出目录(如果您使用Maven,则为target/generated-snippets,如果您使用Gradle,则为build/generated-snippets)。它还可用于配置出现在任何文档化URI中的主机、方案和端口。

使用Mock MVC的自动配置Spring REST Docs测试

@AutoConfigureRestDocs自定义了MockMvc bean,用于在测试基于Servlet的Web应用程序时使用Spring REST Docs。您可以通过使用@Autowired注入它,并在测试中像使用Mock MVC和Spring REST Docs一样正常使用,如下例所示:

Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserController.class)
@AutoConfigureRestDocs
class MyUserDocumentationTests {

    @Autowired
    private MockMvc mvc;

    @Test
    void listUsers() throws Exception {
        this.mvc.perform(get("/users").accept(MediaType.TEXT_PLAIN))
            .andExpect(status().isOk())
            .andDo(document("list-users"));
    }

}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.http.MediaType
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import org.springframework.test.web.servlet.result.MockMvcResultMatchers

@WebMvcTest(UserController::class)
@AutoConfigureRestDocs
class MyUserDocumentationTests(@Autowired val mvc: MockMvc) {

    @Test
    fun listUsers() {
        mvc.perform(MockMvcRequestBuilders.get("/users").accept(MediaType.TEXT_PLAIN))
            .andExpect(MockMvcResultMatchers.status().isOk)
            .andDo(MockMvcRestDocumentation.document("list-users"))
    }

}

如果您需要比@AutoConfigureRestDocs属性提供的更多对Spring REST Docs配置的控制,您可以使用RestDocsMockMvcConfigurationCustomizer bean,如下例所示:

Java
import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentationConfigurer;
import org.springframework.restdocs.templates.TemplateFormats;

@TestConfiguration(proxyBeanMethods = false)
public class MyRestDocsConfiguration implements RestDocsMockMvcConfigurationCustomizer {

    @Override
    public void customize(MockMvcRestDocumentationConfigurer configurer) {
        configurer.snippets().withTemplateFormat(TemplateFormats.markdown());
    }

}
Kotlin
import org.springframework.boot.test.autoconfigure.restdocs.RestDocsMockMvcConfigurationCustomizer
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentationConfigurer
import org.springframework.restdocs.templates.TemplateFormats

@TestConfiguration(proxyBeanMethods = false)
class MyRestDocsConfiguration : RestDocsMockMvcConfigurationCustomizer {

    override fun customize(configurer: MockMvcRestDocumentationConfigurer) {
        configurer.snippets().withTemplateFormat(TemplateFormats.markdown())
    }

}

如果您想要利用Spring REST Docs支持的参数化输出目录,您可以创建一个RestDocumentationResultHandler bean。自动配置调用alwaysDo与此结果处理程序,从而导致每个MockMvc调用自动生成默认片段。以下示例显示了如何定义RestDocumentationResultHandler

Java
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation;
import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler;

@TestConfiguration(proxyBeanMethods = false)
public class MyResultHandlerConfiguration {

    @Bean
    public RestDocumentationResultHandler restDocumentation() {
        return MockMvcRestDocumentation.document("{method-name}");
    }

}
Kotlin
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation
import org.springframework.restdocs.mockmvc.RestDocumentationResultHandler

@TestConfiguration(proxyBeanMethods = false)
class MyResultHandlerConfiguration {

    @Bean
    fun restDocumentation(): RestDocumentationResultHandler {
        return MockMvcRestDocumentation.document("{method-name}")
    }

}
使用WebTestClient进行自动配置的Spring REST Docs测试

@AutoConfigureRestDocs也可以与WebTestClient一起在测试响应式Web应用程序时使用。您可以通过@Autowired注入它,并在测试中像使用@WebFluxTest和Spring REST Docs一样正常使用,如下例所示:

Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.test.web.reactive.server.WebTestClient;

import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;

@WebFluxTest
@AutoConfigureRestDocs
class MyUsersDocumentationTests {

    @Autowired
    private WebTestClient webTestClient;

    @Test
    void listUsers() {
        this.webTestClient
            .get().uri("/")
        .exchange()
        .expectStatus()
            .isOk()
        .expectBody()
            .consumeWith(document("list-users"));
    }

}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest
import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation
import org.springframework.test.web.reactive.server.WebTestClient

@WebFluxTest
@AutoConfigureRestDocs
class MyUsersDocumentationTests(@Autowired val webTestClient: WebTestClient) {

    @Test
    fun listUsers() {
        webTestClient
            .get().uri("/")
            .exchange()
            .expectStatus()
            .isOk
            .expectBody()
            .consumeWith(WebTestClientRestDocumentation.document("list-users"))
    }

}

如果您需要比@AutoConfigureRestDocs属性提供的更多对Spring REST Docs配置的控制,您可以使用RestDocsWebTestClientConfigurationCustomizer bean,如下例所示:

Java
import org.springframework.boot.test.autoconfigure.restdocs.RestDocsWebTestClientConfigurationCustomizer;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentationConfigurer;

@TestConfiguration(proxyBeanMethods = false)
public class MyRestDocsConfiguration implements RestDocsWebTestClientConfigurationCustomizer {

    @Override
    public void customize(WebTestClientRestDocumentationConfigurer configurer) {
        configurer.snippets().withEncoding("UTF-8");
    }

}
Kotlin
import org.springframework.boot.test.autoconfigure.restdocs.RestDocsWebTestClientConfigurationCustomizer
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentationConfigurer

@TestConfiguration(proxyBeanMethods = false)
class MyRestDocsConfiguration : RestDocsWebTestClientConfigurationCustomizer {

    override fun customize(configurer: WebTestClientRestDocumentationConfigurer) {
        configurer.snippets().withEncoding("UTF-8")
    }

}

如果您想要利用Spring REST Docs支持的参数化输出目录,您可以使用WebTestClientBuilderCustomizer来为每个实体交换结果配置一个消费者。以下示例展示了如何定义这样一个WebTestClientBuilderCustomizer

Java
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.web.reactive.server.WebTestClientBuilderCustomizer;
import org.springframework.context.annotation.Bean;

import static org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.document;

@TestConfiguration(proxyBeanMethods = false)
public class MyWebTestClientBuilderCustomizerConfiguration {

    @Bean
    public WebTestClientBuilderCustomizer restDocumentation() {
        return (builder) -> builder.entityExchangeResultConsumer(document("{method-name}"));
    }

}
Kotlin
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.test.web.reactive.server.WebTestClientBuilderCustomizer
import org.springframework.context.annotation.Bean
import org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation
import org.springframework.test.web.reactive.server.WebTestClient

@TestConfiguration(proxyBeanMethods = false)
class MyWebTestClientBuilderCustomizerConfiguration {

    @Bean
    fun restDocumentation(): WebTestClientBuilderCustomizer {
        return WebTestClientBuilderCustomizer { builder: WebTestClient.Builder ->
            builder.entityExchangeResultConsumer(
                WebTestClientRestDocumentation.document("{method-name}")
            )
        }
    }

}
使用REST Assured进行自动配置的Spring REST Docs测试

@AutoConfigureRestDocs会创建一个预配置为使用Spring REST Docs的RequestSpecification bean,并使其在您的测试中可用。您可以通过使用@Autowired注入它,并在测试中像平常使用REST Assured和Spring REST Docs一样使用它,如下例所示:

Java
import io.restassured.specification.RequestSpecification;
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.server.LocalServerPort;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.is;
import static org.springframework.restdocs.restassured.RestAssuredRestDocumentation.document;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureRestDocs
class MyUserDocumentationTests {

    @Test
    void listUsers(@Autowired RequestSpecification documentationSpec, @LocalServerPort int port) {
        given(documentationSpec)
            .filter(document("list-users"))
        .when()
            .port(port)
            .get("/")
        .then().assertThat()
            .statusCode(is(200));
    }

}
Kotlin
import io.restassured.RestAssured
import io.restassured.specification.RequestSpecification
import org.hamcrest.Matchers
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.restdocs.restassured.RestAssuredRestDocumentation

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureRestDocs
class MyUserDocumentationTests {

    @Test
    fun listUsers(@Autowired documentationSpec: RequestSpecification?, @LocalServerPort port: Int) {
        RestAssured.given(documentationSpec)
            .filter(RestAssuredRestDocumentation.document("list-users"))
            .`when`()
            .port(port)["/"]
            .then().assertThat()
            .statusCode(Matchers.`is`(200))
    }

}

如果您需要比@AutoConfigureRestDocs属性提供的更多对Spring REST Docs配置的控制,可以使用RestDocsRestAssuredConfigurationCustomizer bean,如下例所示:

Java
import org.springframework.boot.test.autoconfigure.restdocs.RestDocsRestAssuredConfigurationCustomizer;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.restdocs.restassured.RestAssuredRestDocumentationConfigurer;
import org.springframework.restdocs.templates.TemplateFormats;

@TestConfiguration(proxyBeanMethods = false)
public class MyRestDocsConfiguration implements RestDocsRestAssuredConfigurationCustomizer {

    @Override
    public void customize(RestAssuredRestDocumentationConfigurer configurer) {
        configurer.snippets().withTemplateFormat(TemplateFormats.markdown());
    }

}
Kotlin
import org.springframework.boot.test.autoconfigure.restdocs.RestDocsRestAssuredConfigurationCustomizer
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.restdocs.restassured.RestAssuredRestDocumentationConfigurer
import org.springframework.restdocs.templates.TemplateFormats

@TestConfiguration(proxyBeanMethods = false)
class MyRestDocsConfiguration : RestDocsRestAssuredConfigurationCustomizer {

    override fun customize(configurer: RestAssuredRestDocumentationConfigurer) {
        configurer.snippets().withTemplateFormat(TemplateFormats.markdown())
    }

}

9.3.33. 自动配置的Spring Web Services测试

自动配置的Spring Web Services客户端测试

您可以使用@WebServiceClientTest来测试调用Spring Web Services项目的Web服务的应用程序。默认情况下,它配置一个模拟的WebServiceServer bean,并自动定制您的WebServiceTemplateBuilder。(有关在Spring Boot中使用Web服务的更多信息,请参见"io.html"。)

可以在附录中找到由@WebServiceClientTest启用的自动配置设置的列表。点击这里查看

以下示例展示了@WebServiceClientTest注解的使用:

Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.webservices.client.WebServiceClientTest;
import org.springframework.ws.test.client.MockWebServiceServer;
import org.springframework.xml.transform.StringSource;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.ws.test.client.RequestMatchers.payload;
import static org.springframework.ws.test.client.ResponseCreators.withPayload;

@WebServiceClientTest(SomeWebService.class)
class MyWebServiceClientTests {

    @Autowired
    private MockWebServiceServer server;

    @Autowired
    private SomeWebService someWebService;

    @Test
    void mockServerCall() {
        this.server
            .expect(payload(new StringSource("<request/>")))
            .andRespond(withPayload(new StringSource("<response><status>200</status></response>")));
        assertThat(this.someWebService.test())
            .extracting(Response::getStatus)
            .isEqualTo(200);
    }

}
Kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.webservices.client.WebServiceClientTest
import org.springframework.ws.test.client.MockWebServiceServer
import org.springframework.ws.test.client.RequestMatchers
import org.springframework.ws.test.client.ResponseCreators
import org.springframework.xml.transform.StringSource

@WebServiceClientTest(SomeWebService::class)
class MyWebServiceClientTests(@Autowired val server: MockWebServiceServer, @Autowired val someWebService: SomeWebService) {

    @Test
    fun mockServerCall() {
        server
            .expect(RequestMatchers.payload(StringSource("<request/>")))
            .andRespond(ResponseCreators.withPayload(StringSource("<response><status>200</status></response>")))
        assertThat(this.someWebService.test()).extracting(Response::status).isEqualTo(200)
    }

}
自动配置的Spring Web Services服务器测试

您可以使用@WebServiceServerTest来测试使用Spring Web Services项目实现Web服务的应用程序。默认情况下,它配置一个MockWebServiceClient bean,可用于调用您的Web服务端点。(有关在Spring Boot中使用Web服务的更多信息,请参见"io.html"。)

可以在附录中找到由@WebServiceServerTest启用的自动配置设置的列表。点击这里查看

以下示例展示了@WebServiceServerTest注解的使用:

Java
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.webservices.server.WebServiceServerTest;
import org.springframework.ws.test.server.MockWebServiceClient;
import org.springframework.ws.test.server.RequestCreators;
import org.springframework.ws.test.server.ResponseMatchers;
import org.springframework.xml.transform.StringSource;

@WebServiceServerTest(ExampleEndpoint.class)
class MyWebServiceServerTests {

    @Autowired
    private MockWebServiceClient client;

    @Test
    void mockServerCall() {
        this.client
            .sendRequest(RequestCreators.withPayload(new StringSource("<ExampleRequest/>")))
            .andExpect(ResponseMatchers.payload(new StringSource("<ExampleResponse>42</ExampleResponse>")));
    }

}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.webservices.server.WebServiceServerTest
import org.springframework.ws.test.server.MockWebServiceClient
import org.springframework.ws.test.server.RequestCreators
import org.springframework.ws.test.server.ResponseMatchers
import org.springframework.xml.transform.StringSource

@WebServiceServerTest(ExampleEndpoint::class)
class MyWebServiceServerTests(@Autowired val client: MockWebServiceClient) {

    @Test
    fun mockServerCall() {
        client
            .sendRequest(RequestCreators.withPayload(StringSource("<ExampleRequest/>")))
            .andExpect(ResponseMatchers.payload(StringSource("<ExampleResponse>42</ExampleResponse>")))
    }

}

9.3.34. 附加的自动配置和切片

每个切片提供一个或多个@AutoConfigure…​注解,明确定义应作为切片的一部分包含的自动配置。可以通过创建自定义的@AutoConfigure…​注解或将@ImportAutoConfiguration添加到测试中的方式,逐个测试地添加附加的自动配置,如以下示例所示:

Java
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration;
import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest;

@JdbcTest
@ImportAutoConfiguration(IntegrationAutoConfiguration.class)
class MyJdbcTests {

}
Kotlin
import org.springframework.boot.autoconfigure.ImportAutoConfiguration
import org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration
import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest

@JdbcTest
@ImportAutoConfiguration(IntegrationAutoConfiguration::class)
class MyJdbcTests
确保不要使用常规的@Import注解来导入自动配置,因为它们在Spring Boot中以特定方式处理。

另外,可以通过在存储在META-INF/spring中的文件中注册它们的方式,为任何切片注解的使用添加附加的自动配置,如以下示例所示:

META-INF/spring/org.springframework.boot.test.autoconfigure.jdbc.JdbcTest.imports
com.example.IntegrationAutoConfiguration

在此示例中,com.example.IntegrationAutoConfiguration将在每个使用@JdbcTest注解的测试中启用。

您可以在此文件中使用带有#的注释。
只要元注释为@ImportAutoConfiguration,就可以通过此方式自定义切片或@AutoConfigure…​注解。

9.3.35. 用户配置和切片

如果您以合理的方式组织您的代码,您的@SpringBootApplication类将默认用作测试的配置。

因此,重要的是不要在应用程序的主类中填充特定于其功能区域的配置设置。

假设您正在使用Spring Data MongoDB,您依赖于其自动配置,并且已启用审计。您可以将您的@SpringBootApplication定义如下:

Java
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.mongodb.config.EnableMongoAuditing;

@SpringBootApplication
@EnableMongoAuditing
public class MyApplication {

    // ...

}
Kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.data.mongodb.config.EnableMongoAuditing

@SpringBootApplication
@EnableMongoAuditing
class MyApplication {

    // ...

}

因为这个类是测试的源配置,任何切片测试实际上都会尝试启用Mongo审计,这绝对不是您想要做的。推荐的方法是将该特定区域的配置移动到与您的应用程序相同级别的单独的@Configuration类中,如下例所示:

Java
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.config.EnableMongoAuditing;

@Configuration(proxyBeanMethods = false)
@EnableMongoAuditing
public class MyMongoConfiguration {

    // ...

}
Kotlin
import org.springframework.context.annotation.Configuration
import org.springframework.data.mongodb.config.EnableMongoAuditing

@Configuration(proxyBeanMethods = false)
@EnableMongoAuditing
class MyMongoConfiguration {

    // ...

}
根据您的应用程序复杂性,您可以为自定义设置使用单个@Configuration类,或每个领域区域使用一个类。后一种方法允许您在必要时在您的一个测试中启用它,使用@Import注解。有关何时可能需要为切片测试启用特定@Configuration类的更多详细信息,请参阅此操作指南部分

测试切片会排除@Configuration类的扫描。例如,对于@WebMvcTest,以下配置将不会在测试切片加载的应用程序上下文中包含给定的WebMvcConfigurer bean:

Java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration(proxyBeanMethods = false)
public class MyWebConfiguration {

    @Bean
    public WebMvcConfigurer testConfigurer() {
        return new WebMvcConfigurer() {
            // ...
        };
    }

}
Kotlin
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Configuration(proxyBeanMethods = false)
class MyWebConfiguration {

    @Bean
    fun testConfigurer(): WebMvcConfigurer {
        return object : WebMvcConfigurer {
            // ...
        }
    }

}

然而,下面的配置将导致自定义的WebMvcConfigurer被测试切片加载。

Java
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Component
public class MyWebMvcConfigurer implements WebMvcConfigurer {

    // ...

}
Kotlin
import org.springframework.stereotype.Component
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer

@Component
class MyWebMvcConfigurer : WebMvcConfigurer {

    // ...

}

另一个令人困惑的地方是类路径扫描。假设,虽然您以合理的方式组织了代码,但需要扫描一个额外的包。您的应用程序可能类似于以下代码:

Java
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan({ "com.example.app", "com.example.another" })
public class MyApplication {

    // ...

}
Kotlin
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.context.annotation.ComponentScan

@SpringBootApplication
@ComponentScan("com.example.app", "com.example.another")
class MyApplication {

    // ...

}

这样做实际上会覆盖默认的组件扫描指令,并扫描这两个包,而不管您选择的切片是什么。例如,@DataJpaTest似乎突然扫描应用程序的组件和用户配置。再次,将自定义指令移动到一个单独的类是解决此问题的好方法。

如果这对您不是一个选项,您可以在测试的层次结构中创建一个@SpringBootConfiguration,以便使用它。或者,您可以为您的测试指定一个源,从而禁用查找默认源的行为。

9.3.36. 使用Spock测试Spring Boot应用程序

Spock 2.2或更高版本可用于测试Spring Boot应用程序。要这样做,请在应用程序的构建中添加对Spock的spock-spring模块的-groovy-4.0版本的依赖。 spock-spring将Spring的测试框架集成到Spock中。有关更多详细信息,请参阅Spock的Spring模块文档

9.4. Testcontainers

Testcontainers库提供了一种管理在Docker容器中运行服务的方式。它与JUnit集成,允许您编写一个测试类,在任何测试运行之前启动一个容器。Testcontainers特别适用于编写与真实后端服务交互的集成测试,如MySQL、MongoDB、Cassandra等。

在Spring Boot测试中可以如下使用Testcontainers:

Java
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import org.springframework.boot.test.context.SpringBootTest;

@Testcontainers
@SpringBootTest
class MyIntegrationTests {

    @Container
    static Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:5");

    @Test
    void myTest() {
        // ...
    }

}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
import org.testcontainers.containers.Neo4jContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers

@Testcontainers
@SpringBootTest
class MyIntegrationTests {

    @Test
    fun myTest() {
        // ...
    }

    companion object {
        @Container
        val neo4j = Neo4jContainer("neo4j:5")
    }

}

这将在任何测试运行之前启动一个运行Neo4j的Docker容器(如果本地正在运行Docker)。在大多数情况下,您需要配置应用程序以连接到在容器中运行的服务。

9.4.1. 服务连接

服务连接是与任何远程服务的连接。Spring Boot的自动配置可以消费服务连接的详细信息,并将其用于建立与远程服务的连接。在这样做时,连接详细信息优先于任何与连接相关的配置属性。

使用Testcontainers时,可以通过在测试类中对容器字段进行注释来自动为在容器中运行的服务创建连接详细信息。

Java
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;

@Testcontainers
@SpringBootTest
class MyIntegrationTests {

    @Container
    @ServiceConnection
    static Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:5");

    @Test
    void myTest() {
        // ...
    }

}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.testcontainers.service.connection.ServiceConnection
import org.testcontainers.containers.Neo4jContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers

@Testcontainers
@SpringBootTest
class MyIntegrationTests {

    @Test
    fun myTest() {
        // ...
    }

    companion object {

        @Container
        @ServiceConnection
        val neo4j = Neo4jContainer("neo4j:5")

    }

}

通过@ServiceConnection,上述配置允许应用程序中与在Testcontainers管理的Docker容器中运行的Neo4j通信的相关bean。这是通过自动定义一个Neo4jConnectionDetails bean来实现的,然后被Neo4j自动配置使用,覆盖任何与连接相关的配置属性。

您需要将spring-boot-testcontainers模块作为测试依赖项添加,以便在Testcontainers中使用服务连接。

服务连接注释由注册到spring.factoriesContainerConnectionDetailsFactory类处理。一个ContainerConnectionDetailsFactory可以基于特定的Container子类或Docker镜像名称创建一个ConnectionDetails bean。

以下服务连接工厂在spring-boot-testcontainers jar中提供:

连接详细信息 匹配项

ActiveMQConnectionDetails

命名为"symptoma/activemq"的容器

CassandraConnectionDetails

类型为CassandraContainer的容器

CouchbaseConnectionDetails

类型为CouchbaseContainer的容器

ElasticsearchConnectionDetails

类型为ElasticsearchContainer的容器

FlywayConnectionDetails

类型为JdbcDatabaseContainer的容器

JdbcConnectionDetails

类型为JdbcDatabaseContainer的容器

KafkaConnectionDetails

类型为KafkaContainerRedpandaContainer的容器

LiquibaseConnectionDetails

类型为JdbcDatabaseContainer的容器

MongoConnectionDetails

类型为MongoDBContainer的容器

Neo4jConnectionDetails

类型为Neo4jContainer的容器

OtlpMetricsConnectionDetails

命名为"otel/opentelemetry-collector-contrib"的容器

OtlpTracingConnectionDetails

命名为"otel/opentelemetry-collector-contrib"的容器

PulsarConnectionDetails

类型为PulsarContainer的容器

R2dbcConnectionDetails

类型为MariaDBContainerMSSQLServerContainerMySQLContainerOracleContainerPostgreSQLContainer的容器

RabbitConnectionDetails

类型为RabbitMQContainer的容器

RedisConnectionDetails

命名为"redis"的容器

ZipkinConnectionDetails

命名为"openzipkin/zipkin"的容器

默认情况下,对于给定的Container,将创建所有适用的连接详细信息bean。例如,PostgreSQLContainer将同时创建JdbcConnectionDetailsR2dbcConnectionDetails

如果您只想创建适用类型的子集,可以使用@ServiceConnectiontype属性。

默认情况下,使用Container.getDockerImageName()来获取用于查找连接详细信息的名称。只要Spring Boot能够获取Container的实例,这就有效,这在像上面示例中使用static字段时是成立的。

如果您使用@Bean方法,Spring Boot不会调用bean方法以获取Docker镜像名称,因为这会导致急切初始化问题。相反,将使用bean方法的返回类型来查找应该使用哪个连接详细信息。只要您使用了类型化容器,例如Neo4jContainerRabbitMQContainer,这就有效。如果您使用GenericContainer,例如下面的Redis示例,这将停止工作:

Java
import org.testcontainers.containers.GenericContainer;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;

@TestConfiguration(proxyBeanMethods = false)
public class MyRedisConfiguration {

    @Bean
    @ServiceConnection(name = "redis")
    public GenericContainer<?> redisContainer() {
        return new GenericContainer<>("redis:7");
    }

}
Kotlin
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.testcontainers.service.connection.ServiceConnection
import org.springframework.context.annotation.Bean
import org.testcontainers.containers.GenericContainer

@TestConfiguration(proxyBeanMethods = false)
class MyRedisConfiguration {

    @Bean
    @ServiceConnection(name = "redis")
    fun redisContainer(): GenericContainer<*> {
        return GenericContainer("redis:7")
    }

}

Spring Boot 无法从 GenericContainer 中识别使用的容器镜像,因此必须使用 @ServiceConnection 中的 name 属性来提供提示。

您还可以使用 @ServiceConnectionname 属性来覆盖将使用哪个连接详细信息,例如在使用自定义镜像时。如果您正在使用 Docker 镜像 registry.mycompany.com/mirror/myredis,您可以使用 @ServiceConnection(name="redis") 来确保创建 RedisConnectionDetails

9.4.2. 动态属性

与服务连接相比,稍微冗长但也更灵活的替代方案是@DynamicPropertySource。静态@DynamicPropertySource方法允许向Spring环境添加动态属性值。

Java
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;

@Testcontainers
@SpringBootTest
class MyIntegrationTests {

    @Container
    static Neo4jContainer<?> neo4j = new Neo4jContainer<>("neo4j:5");

    @Test
    void myTest() {
        // ...
    }

    @DynamicPropertySource
    static void neo4jProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.neo4j.uri", neo4j::getBoltUrl);
    }

}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.testcontainers.containers.Neo4jContainer
import org.testcontainers.junit.jupiter.Container
import org.testcontainers.junit.jupiter.Testcontainers

@Testcontainers
@SpringBootTest
class MyIntegrationTests {

    @Test
    fun myTest() {
        // ...
    }

    companion object {

        @Container
        val neo4j = Neo4jContainer("neo4j:5")

        @DynamicPropertySource
        fun neo4jProperties(registry: DynamicPropertyRegistry) {
            registry.add("spring.neo4j.uri") { neo4j.boltUrl }
        }

    }

}

上述配置允许应用程序中与Testcontainers管理的Docker容器中运行的Neo4j通信的与Neo4j相关的bean。

9.5. 测试工具

一些测试实用类通常在测试应用程序时很有用,它们作为spring-boot的一部分进行打包。

9.5.1. ConfigDataApplicationContextInitializer

ConfigDataApplicationContextInitializer是一个ApplicationContextInitializer,您可以将其应用于您的测试中以加载Spring Boot的application.properties文件。您可以在不需要@SpringBootTest提供的全部功能集时使用它,如下例所示:

Java
import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer;
import org.springframework.test.context.ContextConfiguration;

@ContextConfiguration(classes = Config.class, initializers = ConfigDataApplicationContextInitializer.class)
class MyConfigFileTests {

    // ...

}
Kotlin
import org.springframework.boot.test.context.ConfigDataApplicationContextInitializer
import org.springframework.test.context.ContextConfiguration

@ContextConfiguration(classes = [Config::class], initializers = [ConfigDataApplicationContextInitializer::class])
class MyConfigFileTests {

    // ...

}
仅使用ConfigDataApplicationContextInitializer不支持@Value("${…​}")注入。它的唯一作用是确保将application.properties文件加载到Spring的Environment中。要获得@Value支持,您需要额外配置PropertySourcesPlaceholderConfigurer或使用@SpringBootTest,后者会为您自动配置一个。

9.5.2. TestPropertyValues

TestPropertyValues允许您快速向ConfigurableEnvironmentConfigurableApplicationContext添加属性。您可以使用key=value字符串调用它,如下所示:

Java
import org.junit.jupiter.api.Test;

import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.mock.env.MockEnvironment;

import static org.assertj.core.api.Assertions.assertThat;

class MyEnvironmentTests {

    @Test
    void testPropertySources() {
        MockEnvironment environment = new MockEnvironment();
        TestPropertyValues.of("org=Spring", "name=Boot").applyTo(environment);
        assertThat(environment.getProperty("name")).isEqualTo("Boot");
    }

}
Kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.boot.test.util.TestPropertyValues
import org.springframework.mock.env.MockEnvironment

class MyEnvironmentTests {

    @Test
    fun testPropertySources() {
        val environment = MockEnvironment()
        TestPropertyValues.of("org=Spring", "name=Boot").applyTo(environment)
        assertThat(environment.getProperty("name")).isEqualTo("Boot")
    }

}

9.5.3. OutputCapture

OutputCapture是一个JUnitExtension,您可以使用它来捕获System.outSystem.err输出。要使用它,请添加@ExtendWith(OutputCaptureExtension.class)并将CapturedOutput作为参数注入到您的测试类构造函数或测试方法中,如下所示:

Java
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(OutputCaptureExtension.class)
class MyOutputCaptureTests {

    @Test
    void testName(CapturedOutput output) {
        System.out.println("Hello World!");
        assertThat(output).contains("World");
    }

}
Kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
import org.springframework.boot.test.system.CapturedOutput
import org.springframework.boot.test.system.OutputCaptureExtension

@ExtendWith(OutputCaptureExtension::class)
class MyOutputCaptureTests {

    @Test
    fun testName(output: CapturedOutput?) {
        println("Hello World!")
        assertThat(output).contains("World")
    }

}

9.5.4. TestRestTemplate

TestRestTemplate是Spring的RestTemplate的一个方便替代,适用于集成测试。您可以获取一个原始模板或一个发送基本HTTP身份验证(带有用户名和密码)的模板。在任何情况下,该模板都具有容错性。这意味着它以测试友好的方式行为,不会在4xx和5xx错误时抛出异常。相反,这些错误可以通过返回的ResponseEntity及其状态代码来检测。

Spring Framework 5.0提供了一个新的WebTestClient,适用于WebFlux集成测试以及WebFlux和MVC端到端测试。它提供了一个流畅的API用于断言,不同于TestRestTemplate

建议但不强制使用Apache HTTP Client(版本5.1或更高版本)。如果您的类路径中有该客户端,TestRestTemplate会通过配置客户端来响应。如果使用Apache的HTTP客户端,将启用一些额外的测试友好功能:

  • 不会跟随重定向(因此您可以断言响应位置)。

  • 忽略Cookie(因此模板是无状态的)。

TestRestTemplate可以直接在您的集成测试中实例化,如下例所示:

Java
import org.junit.jupiter.api.Test;

import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.ResponseEntity;

import static org.assertj.core.api.Assertions.assertThat;

class MyTests {

    private final TestRestTemplate template = new TestRestTemplate();

    @Test
    void testRequest() {
        ResponseEntity<String> headers = this.template.getForEntity("https://myhost.example.com/example", String.class);
        assertThat(headers.getHeaders().getLocation()).hasHost("other.example.com");
    }

}
Kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.boot.test.web.client.TestRestTemplate

class MyTests {

    private val template = TestRestTemplate()

    @Test
    fun testRequest() {
        val headers = template.getForEntity("https://myhost.example.com/example", String::class.java)
        assertThat(headers.headers.location).hasHost("other.example.com")
    }

}

或者,如果您使用@SpringBootTest注解与WebEnvironment.RANDOM_PORTWebEnvironment.DEFINED_PORT,您可以注入一个完全配置的TestRestTemplate并开始使用它。如果需要,可以通过RestTemplateBuilder bean应用额外的自定义。任何不指定主机和端口的URL将自动连接到嵌入式服务器,如下例所示:

Java
import java.time.Duration;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpHeaders;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class MySpringBootTests {

    @Autowired
    private TestRestTemplate template;

    @Test
    void testRequest() {
        HttpHeaders headers = this.template.getForEntity("/example", String.class).getHeaders();
        assertThat(headers.getLocation()).hasHost("other.example.com");
    }

    @TestConfiguration(proxyBeanMethods = false)
    static class RestTemplateBuilderConfiguration {

        @Bean
        RestTemplateBuilder restTemplateBuilder() {
            return new RestTemplateBuilder().setConnectTimeout(Duration.ofSeconds(1))
                .setReadTimeout(Duration.ofSeconds(1));
        }

    }

}
Kotlin
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.test.web.client.TestRestTemplate
import org.springframework.boot.web.client.RestTemplateBuilder
import org.springframework.context.annotation.Bean
import java.time.Duration

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class MySpringBootTests(@Autowired val template: TestRestTemplate) {

    @Test
    fun testRequest() {
        val headers = template.getForEntity("/example", String::class.java).headers
        assertThat(headers.location).hasHost("other.example.com")
    }

    @TestConfiguration(proxyBeanMethods = false)
    internal class RestTemplateBuilderConfiguration {

        @Bean
        fun restTemplateBuilder(): RestTemplateBuilder {
            return RestTemplateBuilder().setConnectTimeout(Duration.ofSeconds(1))
                .setReadTimeout(Duration.ofSeconds(1))
        }

    }

}

10. Docker Compose 支持

Docker Compose 是一种流行的技术,可用于定义和管理应用程序所需的多个服务容器。通常会在应用程序旁边创建一个 compose.yml 文件,该文件定义和配置服务容器。

使用 Docker Compose 的典型工作流程是运行 docker compose up,与启动的服务连接一起开发应用程序,然后在完成后运行 docker compose down

可以将 spring-boot-docker-compose 模块包含在项目中,以提供对使用 Docker Compose 的容器进行操作的支持。将模块依赖项添加到构建中,如下面的 Maven 和 Gradle 示例所示:

Maven
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-docker-compose</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>
Gradle
dependencies {
    developmentOnly("org.springframework.boot:spring-boot-docker-compose")
}

当将此模块作为依赖项包含时,Spring Boot 将执行以下操作:

  • 在应用程序目录中搜索 compose.yml 和其他常见的 compose 文件名

  • 使用发现的 compose.yml 调用 docker compose up

  • 为每个受支持的容器创建服务连接 bean

  • 在应用程序关闭时调用 docker compose stop

如果在启动应用程序时 Docker Compose 服务已经在运行,则 Spring Boot 仅为每个受支持的容器创建服务连接 bean。它不会再次调用 docker compose up,也不会在应用程序关闭时调用 docker compose stop

默认情况下,在运行测试时,Spring Boot 的 Docker Compose 支持是禁用的。要启用它,请将 spring.docker.compose.skip.in-tests 设置为 false

10.1. 先决条件

您需要在路径中拥有 dockerdocker compose(或 docker-compose)CLI 应用程序。最低支持的 Docker Compose 版本为 2.2.0。

10.2. 服务连接

服务连接是与任何远程服务的连接。Spring Boot 的自动配置可以使用服务连接的详细信息,并将其用于建立与远程服务的连接。在这样做时,连接详细信息优先于任何与连接相关的配置属性。

在使用 Spring Boot 的 Docker Compose 支持时,服务连接将建立到容器映射的端口。

通常情况下,Docker Compose 的端口在容器内部映射到计算机上的临时端口。例如,Postgres 服务器可能在容器内部使用端口 5432 运行,但在本地映射到完全不同的端口。服务连接将始终发现并使用本地映射的端口。

通过使用容器的镜像名称建立服务连接。当前支持以下服务连接:

连接详细信息 匹配项

ActiveMQConnectionDetails

命名为 "symptoma/activemq" 的容器

CassandraConnectionDetails

命名为 "cassandra" 的容器

ElasticsearchConnectionDetails

命名为 "elasticsearch" 的容器

JdbcConnectionDetails

命名为 "gvenzl/oracle-free"、"gvenzl/oracle-xe"、"mariadb"、"mssql/server"、"mysql" 或 "postgres" 的容器

MongoConnectionDetails

命名为 "mongo" 的容器

Neo4jConnectionDetails

命名为 "neo4j" 的容器

OtlpMetricsConnectionDetails

命名为 "otel/opentelemetry-collector-contrib" 的容器

OtlpTracingConnectionDetails

命名为 "otel/opentelemetry-collector-contrib" 的容器

PulsarConnectionDetails

命名为 "apachepulsar/pulsar" 的容器

R2dbcConnectionDetails

命名为 "gvenzl/oracle-free"、"gvenzl/oracle-xe"、"mariadb"、"mssql/server"、"mysql" 或 "postgres" 的容器

RabbitConnectionDetails

命名为 "rabbitmq" 的容器

RedisConnectionDetails

命名为 "redis" 的容器

ZipkinConnectionDetails

命名为 "openzipkin/zipkin" 的容器

10.3. 自定义镜像

有时您可能需要使用自己版本的镜像来提供服务。只要自定义镜像的行为与标准镜像相同即可使用任何自定义镜像。具体而言,标准镜像支持的任何环境变量也必须在自定义镜像中使用。

如果您的镜像使用不同的名称,可以在您的 compose.yml 文件中使用标签,以便 Spring Boot 提供服务连接。使用名为 org.springframework.boot.service-connection 的标签提供服务名称。

例如:

services:
  redis:
    image: 'mycompany/mycustomredis:7.0'
    ports:
      - '6379'
    labels:
      org.springframework.boot.service-connection: redis

10.4. 跳过特定容器

如果在您的 compose.yml 中定义了一个容器镜像,但您不希望将其连接到应用程序,可以使用标签来忽略它。任何带有 org.springframework.boot.ignore 标签的容器都将被 Spring Boot 忽略。

例如:

services:
  redis:
    image: 'redis:7.0'
    ports:
      - '6379'
    labels:
      org.springframework.boot.ignore: true

10.5. 使用特定的 Compose 文件

如果您的 compose 文件不在与应用程序相同的目录中,或者命名不同,您可以在您的 application.propertiesapplication.yaml 中使用 spring.docker.compose.file 指向不同的文件。属性可以定义为精确路径或相对于您的应用程序的路径。

例如:

Properties
spring.docker.compose.file=../my-compose.yml
Yaml
spring:
  docker:
    compose:
      file: "../my-compose.yml"

10.6. 等待容器就绪

Docker Compose启动的容器可能需要一些时间才能完全就绪。检查就绪的推荐方式是在您的compose.yml文件中在服务定义下添加一个healthcheck部分。

由于在compose.yml文件中经常会省略healthcheck配置,Spring Boot也会直接检查服务的就绪状态。默认情况下,当可以建立到其映射端口的TCP/IP连接时,容器被视为就绪。

您可以通过在您的compose.yml文件中添加一个org.springframework.boot.readiness-check.tcp.disable标签来在每个容器上禁用此功能。

例如:

services:
  redis:
    image: 'redis:7.0'
    ports:
      - '6379'
    labels:
      org.springframework.boot.readiness-check.tcp.disable: true

您还可以在您的application.propertiesapplication.yaml文件中更改超时值:

属性
spring.docker.compose.readiness.tcp.connect-timeout=10s
spring.docker.compose.readiness.tcp.read-timeout=5s
Yaml
spring:
  docker:
    compose:
      readiness:
        tcp:
          connect-timeout: 10s
          read-timeout: 5s

可以使用spring.docker.compose.readiness.timeout来配置整体超时时间。

10.7. 控制Docker Compose生命周期

默认情况下,Spring Boot在应用程序启动时调用docker compose up,在关闭时调用docker compose stop。如果您希望有不同的生命周期管理方式,可以使用spring.docker.compose.lifecycle-management属性。

支持以下值:

  • none - 不启动或停止Docker Compose

  • start-only - 应用程序启动时启动Docker Compose并保持运行

  • start-and-stop - 应用程序启动时启动Docker Compose,在JVM退出时停止

此外,您可以使用spring.docker.compose.start.command属性来更改使用docker compose up还是docker compose startspring.docker.compose.stop.command允许您配置使用docker compose down还是docker compose stop

以下示例显示了如何配置生命周期管理:

属性
spring.docker.compose.lifecycle-management=start-and-stop
spring.docker.compose.start.command=start
spring.docker.compose.stop.command=down
spring.docker.compose.stop.timeout=1m
Yaml
spring:
  docker:
    compose:
      lifecycle-management: start-and-stop
      start:
        command: start
      stop:
        command: down
        timeout: 1m

10.8. 激活Docker Compose配置文件

Docker Compose配置文件类似于Spring配置文件,允许您根据特定环境调整Docker Compose配置。如果要激活特定的Docker Compose配置文件,可以在您的application.propertiesapplication.yaml文件中使用spring.docker.compose.profiles.active属性:

属性
spring.docker.compose.profiles.active=myprofile
Yaml
spring:
  docker:
    compose:
      profiles:
        active: "myprofile"

11. 测试容器支持

除了在集成测试中使用Testcontainers,也可以在开发阶段使用它们。接下来的章节将提供更多关于此的细节。

11.1. 在开发时使用Testcontainers

这种方法允许开发人员快速启动应用程序所依赖的服务容器,无需手动配置诸如数据库服务器之类的内容。以这种方式使用Testcontainers提供了类似Docker Compose的功能,只是您的容器配置是在Java中而不是YAML中。

要在开发时使用Testcontainers,您需要使用“test”类路径启动应用程序,而不是“main”。这将允许您访问所有声明的测试依赖项,并为您提供一个自然的地方来编写测试配置。

要创建应用程序的测试可启动版本,您应该在src/test目录中创建一个“Application”类。例如,如果您的主应用程序位于src/main/java/com/example/MyApplication.java中,则应创建src/test/java/com/example/TestMyApplication.java

TestMyApplication类可以使用SpringApplication.from(…​)方法启动真实应用程序:

Java
import org.springframework.boot.SpringApplication;

public class TestMyApplication {

    public static void main(String[] args) {
        SpringApplication.from(MyApplication::main).run(args);
    }

}
Kotlin
import org.springframework.boot.fromApplication

fun main(args: Array<String>) {
    fromApplication<MyApplication>().run(*args)
}

您还需要定义要与应用程序一起启动的Container实例。为此,您需要确保已将spring-boot-testcontainers模块添加为test依赖项。完成后,您可以创建一个@TestConfiguration类,声明要启动的容器的@Bean方法。

您还可以使用@ServiceConnection注解您的@Bean方法,以创建ConnectionDetails beans。有关受支持技术的详细信息,请参阅服务连接部分。

典型的Testcontainers配置如下:

Java
import org.testcontainers.containers.Neo4jContainer;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;

@TestConfiguration(proxyBeanMethods = false)
public class MyContainersConfiguration {

    @Bean
    @ServiceConnection
    public Neo4jContainer<?> neo4jContainer() {
        return new Neo4jContainer<>("neo4j:5");
    }

}
Kotlin
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.testcontainers.service.connection.ServiceConnection
import org.springframework.context.annotation.Bean
import org.testcontainers.containers.Neo4jContainer

@TestConfiguration(proxyBeanMethods = false)
class MyContainersConfiguration {

    @Bean
    @ServiceConnection
    fun neo4jContainer(): Neo4jContainer<*> {
        return Neo4jContainer("neo4j:5")
    }

}
Spring Boot会自动管理Container bean的生命周期。容器将自动启动和停止。
您可以使用spring.testcontainers.beans.startup属性来更改容器的启动方式。默认情况下使用sequential启动,但如果希望并行启动多个容器,则还可以选择parallel

一旦定义了测试配置,您可以使用with(…​)方法将其附加到测试启动器:

Java
import org.springframework.boot.SpringApplication;

public class TestMyApplication {

    public static void main(String[] args) {
        SpringApplication.from(MyApplication::main).with(MyContainersConfiguration.class).run(args);
    }

}
Kotlin
import org.springframework.boot.fromApplication
import org.springframework.boot.with

fun main(args: Array<String>) {
    fromApplication<MyApplication>().with(MyContainersConfiguration::class).run(*args)
}

现在,您可以像启动任何常规Java main方法应用程序一样启动TestMyApplication,以启动您的应用程序及其运行所需的容器。

您可以使用Maven目标spring-boot:test-run或Gradle任务bootTestRun从命令行执行此操作。

11.1.1. 在开发时贡献动态属性

如果您想要在开发时从Container@Bean方法中贡献动态属性,可以通过注入DynamicPropertyRegistry来实现。这与您在测试中可以使用的@DynamicPropertySource注解类似。它允许您添加一旦容器启动后将可用的属性。

典型的配置如下:

Java
import org.testcontainers.containers.MongoDBContainer;

import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.DynamicPropertyRegistry;

@TestConfiguration(proxyBeanMethods = false)
public class MyContainersConfiguration {

    @Bean
    public MongoDBContainer mongoDbContainer(DynamicPropertyRegistry properties) {
        MongoDBContainer container = new MongoDBContainer("mongo:5.0");
        properties.add("spring.data.mongodb.host", container::getHost);
        properties.add("spring.data.mongodb.port", container::getFirstMappedPort);
        return container;
    }

}
Kotlin
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
import org.springframework.test.context.DynamicPropertyRegistry
import org.testcontainers.containers.MongoDBContainer

@TestConfiguration(proxyBeanMethods = false)
class MyContainersConfiguration {

    @Bean
    fun monogDbContainer(properties: DynamicPropertyRegistry): MongoDBContainer {
        var container = MongoDBContainer("mongo:5.0")
        properties.add("spring.data.mongodb.host", container::getHost)
        properties.add("spring.data.mongodb.port", container::getFirstMappedPort)
        return container
    }

}
尽可能使用@ServiceConnection,但对于尚未支持@ServiceConnection的技术,动态属性可能是一个有用的备选方案。

11.1.2. 导入测试容器声明类

在使用Testcontainers时的常见模式是将Container实例声明为静态字段。通常这些字段直接在测试类上定义。它们也可以在父类或测试实现的接口上声明。

例如,以下MyContainers接口声明了mongoneo4j容器:

import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.junit.jupiter.Container;

import org.springframework.boot.testcontainers.service.connection.ServiceConnection;

public interface MyContainers {

    @Container
    @ServiceConnection
    MongoDBContainer mongoContainer = new MongoDBContainer("mongo:5.0");

    @Container
    @ServiceConnection
    Neo4jContainer<?> neo4jContainer = new Neo4jContainer<>("neo4j:5");

}

如果您已经以这种方式定义了容器,或者您更喜欢这种风格,您可以导入这些声明类,而不是将容器定义为@Bean方法。为此,请将@ImportTestcontainers注解添加到您的测试配置类中:

Java
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.context.ImportTestcontainers;

@TestConfiguration(proxyBeanMethods = false)
@ImportTestcontainers(MyContainers.class)
public class MyContainersConfiguration {

}
Kotlin
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.testcontainers.context.ImportTestcontainers

@TestConfiguration(proxyBeanMethods = false)
@ImportTestcontainers(MyContainers::class)
class MyContainersConfiguration
如果您不打算使用服务连接功能,但想要使用@DynamicPropertySource,请从Container字段中删除@ServiceConnection注解。您还可以将@DynamicPropertySource注解的方法添加到您的声明类中。

11.1.3. 在开发时使用DevTools与Testcontainers

在使用DevTools时,您可以使用@RestartScope为bean和bean方法添加注解。这样的bean在DevTools重新启动应用程序时不会被重新创建。这对于Testcontainer的Container bean特别有用,因为它们在应用程序重新启动时保持其状态。

Java
import org.testcontainers.containers.MongoDBContainer;

import org.springframework.boot.devtools.restart.RestartScope;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;

@TestConfiguration(proxyBeanMethods = false)
public class MyContainersConfiguration {

    @Bean
    @RestartScope
    @ServiceConnection
    public MongoDBContainer mongoDbContainer() {
        return new MongoDBContainer("mongo:5.0");
    }

}
Kotlin
import org.springframework.boot.devtools.restart.RestartScope
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.boot.testcontainers.service.connection.ServiceConnection
import org.springframework.context.annotation.Bean
import org.testcontainers.containers.MongoDBContainer

@TestConfiguration(proxyBeanMethods = false)
class MyContainersConfiguration {

    @Bean
    @RestartScope
    @ServiceConnection
    fun monogDbContainer(): MongoDBContainer {
        return MongoDBContainer("mongo:5.0")
    }

}
如果您使用Gradle并希望使用此功能,则需要将spring-boot-devtools依赖项的配置从developmentOnly更改为testAndDevelopmentOnly。使用developmentOnly的默认范围,bootTestRun任务将不会捕捉到代码中的更改,因为DevTools未激活。

12. 创建自己的自动配置

如果您在开发共享库的公司工作,或者您在开源或商业库上工作,您可能希望开发自己的自动配置。自动配置类可以打包在外部jar中,并且仍然可以被Spring Boot识别。

自动配置可以与“starter”关联,提供自动配置代码以及您将与之一起使用的典型库。我们首先介绍构建自己的自动配置所需了解的内容,然后我们转向创建自定义starter所需的典型步骤

12.1. 理解自动配置的Bean

实现自动配置的类使用@AutoConfiguration进行注释。该注释本身是使用@Configuration进行元注释的,使自动配置成为标准的@Configuration类。额外的@Conditional注释用于限制自动配置的应用时间。通常,自动配置类使用@ConditionalOnClass@ConditionalOnMissingBean注释。这确保了只有在找到相关类并且您没有声明自己的@Configuration时,自动配置才会应用。

您可以浏览spring-boot-autoconfigure的源代码spring-boot-autoconfigure,查看Spring提供的@AutoConfiguration类(请参阅META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件)。

12.2. 定位自动配置候选项

Spring Boot检查您发布的jar中是否存在META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件。该文件应列出您的配置类,每行一个类名,如下例所示:

com.mycorp.libx.autoconfigure.LibXAutoConfiguration
com.mycorp.libx.autoconfigure.LibXWebAutoConfiguration
您可以使用#字符向导入文件添加注释。
自动配置必须仅通过在导入文件中命名来加载。确保它们定义在特定的包空间中,并且永远不是组件扫描的目标。此外,自动配置类不应启用组件扫描以查找其他组件。应该使用特定的@Import注释。

如果您的配置需要按特定顺序应用,则可以在@AutoConfiguration注释或专用的@AutoConfiguration@AutoConfigureBefore@AutoConfigureAfter注释上使用beforebeforeNameafterafterName属性。例如,如果您提供特定于Web的配置,您的类可能需要在WebMvcAutoConfiguration之后应用。

如果您想要对不应直接相互了解的某些自动配置进行排序,还可以使用@AutoConfigureOrder。该注释具有与常规@Order注释相同的语义,但为自动配置类提供了专用顺序。

与标准的@Configuration类一样,自动配置类应用的顺序仅影响其bean定义的顺序。随后创建这些bean的顺序不受影响,由每个bean的依赖关系和任何@DependsOn关系确定。

12.3. 条件注解

几乎总是希望在自动配置类上包含一个或多个@Conditional注解。@ConditionalOnMissingBean注解是一个常见示例,用于允许开发人员覆盖自动配置,如果他们不满意您的默认设置。

Spring Boot包含许多@Conditional注解,您可以通过在@Configuration类或单独的@Bean方法上添加注解来重用这些注解。这些注解包括:

12.3.1. 类条件

@ConditionalOnClass@ConditionalOnMissingClass注解允许根据特定类的存在或不存在来包含@Configuration类。由于注解元数据是通过使用ASM进行解析的,您可以使用value属性来引用实际类,即使该类实际上可能不会出现在运行应用程序的类路径上。如果您更喜欢使用String值指定类名,则还可以使用name属性。

这种机制在通常情况下不适用于@Bean方法,其中通常返回类型是条件的目标:在方法上的条件应用之前,JVM将加载类并可能处理方法引用,如果类不存在,则会失败。

为了处理这种情况,可以使用单独的@Configuration类来隔离条件,如下例所示:

Java
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@AutoConfiguration
// Some conditions ...
public class MyAutoConfiguration {

    // Auto-configured beans ...

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(SomeService.class)
    public static class SomeServiceConfiguration {

        @Bean
        @ConditionalOnMissingBean
        public SomeService someService() {
            return new SomeService();
        }

    }

}
Kotlin
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration(proxyBeanMethods = false)
// Some conditions ...
class MyAutoConfiguration {

    // Auto-configured beans ...
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(SomeService::class)
    class SomeServiceConfiguration {

        @Bean
        @ConditionalOnMissingBean
        fun someService(): SomeService {
            return SomeService()
        }

    }

}
如果您将@ConditionalOnClass@ConditionalOnMissingClass作为元注解的一部分来组合自己的组合注解,那么在这种情况下,您必须使用name,因为在这种情况下,引用类不会被处理。

12.3.2. Bean条件

@ConditionalOnBean@ConditionalOnMissingBean注解允许根据特定bean的存在或不存在来包含bean。您可以使用value属性按类型指定bean,或使用name按名称指定bean。search属性允许您限制在搜索bean时应考虑的ApplicationContext层次结构。

当放置在@Bean方法上时,目标类型默认为方法的返回类型,如下例所示:

Java
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;

@AutoConfiguration
public class MyAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public SomeService someService() {
        return new SomeService();
    }

}
Kotlin
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration(proxyBeanMethods = false)
class MyAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    fun someService(): SomeService {
        return SomeService()
    }

}

在上面的示例中,如果ApplicationContext中尚未包含SomeService类型的bean,则将创建someService bean。

您需要非常小心地添加bean定义的顺序,因为这些条件是基于到目前为止已处理的内容进行评估的。因此,我们建议仅在自动配置类上使用@ConditionalOnBean@ConditionalOnMissingBean注解(因为这些注解保证在添加任何用户定义的bean定义之后加载)。
@ConditionalOnBean@ConditionalOnMissingBean不会阻止创建@Configuration类。在类级别使用这些条件和在每个包含的@Bean方法上标记注解之间的唯一区别是,前者在条件不匹配时阻止@Configuration类的注册作为bean。
在声明@Bean方法时,尽可能在方法的返回类型中提供尽可能多的类型信息。例如,如果您的bean的具体类实现了一个接口,则bean方法的返回类型应该是具体类,而不是接口。在使用bean条件时,尽可能在@Bean方法中提供尽可能多的类型信息尤为重要,因为它们的评估只能依赖于方法签名中可用的类型信息。

12.3.3. 属性条件

@ConditionalOnProperty注解允许根据Spring环境属性来包含配置。使用prefixname属性来指定应检查的属性。默认情况下,任何存在且不等于false的属性都会匹配。您还可以通过使用havingValuematchIfMissing属性创建更高级的检查。

12.3.4. 资源条件

@ConditionalOnResource注解允许仅在存在特定资源时包含配置。资源可以通过使用通常的Spring约定来指定,如下例所示:file:/home/user/test.dat

12.3.5. Web应用条件

@ConditionalOnWebApplication@ConditionalOnNotWebApplication注解允许根据应用程序是否为Web应用程序来包含配置。基于Servlet的Web应用程序是指使用Spring WebApplicationContext、定义了session范围或具有ConfigurableWebEnvironment的任何应用程序。基于响应式的Web应用程序是指使用ReactiveWebApplicationContext或具有ConfigurableReactiveWebEnvironment的任何应用程序。

@ConditionalOnWarDeployment@ConditionalOnNotWarDeployment注解允许根据应用程序是否为部署到Servlet容器的传统WAR应用程序来包含配置。此条件不适用于在嵌入式Web服务器上运行的应用程序。

12.3.6. SpEL表达式条件

@ConditionalOnExpression注解允许根据SpEL表达式的结果来包含配置。

在表达式中引用一个bean将导致该bean在上下文刷新处理过程中被非常早地初始化。因此,该bean将无法进行后处理(例如配置属性绑定),其状态可能是不完整的。

12.4. 测试您的自动配置

自动配置可能受到许多因素的影响:用户配置(@Bean定义和Environment定制)、条件评估(特定库的存在)等。具体来说,每个测试应该创建一个明确定义的ApplicationContext,代表这些定制的组合。 ApplicationContextRunner 提供了一个很好的方法来实现这一点。

ApplicationContextRunner 在本地镜像中运行测试时不起作用。

ApplicationContextRunner通常被定义为测试类的字段,用于收集基本的公共配置。以下示例确保始终调用MyServiceAutoConfiguration

Java
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
    .withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration.class));
Kotlin
val contextRunner = ApplicationContextRunner()
    .withConfiguration(AutoConfigurations.of(MyServiceAutoConfiguration::class.java))
如果必须定义多个自动配置,无需按其声明顺序排序,因为它们在运行应用程序时以完全相同的顺序调用。

每个测试都可以使用该运行器来表示特定的用例。例如,下面的示例调用用户配置(UserConfiguration)并检查自动配置是否正确地后退。调用run提供了一个可以与AssertJ一起使用的回调上下文。

Java
@Test
void defaultServiceBacksOff() {
    this.contextRunner.withUserConfiguration(UserConfiguration.class).run((context) -> {
        assertThat(context).hasSingleBean(MyService.class);
        assertThat(context).getBean("myCustomService").isSameAs(context.getBean(MyService.class));
    });
}

@Configuration(proxyBeanMethods = false)
static class UserConfiguration {

    @Bean
    MyService myCustomService() {
        return new MyService("mine");
    }

}
Kotlin
@Test
fun defaultServiceBacksOff() {
    contextRunner.withUserConfiguration(UserConfiguration::class.java)
        .run { context: AssertableApplicationContext ->
            assertThat(context).hasSingleBean(MyService::class.java)
            assertThat(context).getBean("myCustomService")
                .isSameAs(context.getBean(MyService::class.java))
        }
}

@Configuration(proxyBeanMethods = false)
internal class UserConfiguration {

    @Bean
    fun myCustomService(): MyService {
        return MyService("mine")
    }

}

还可以轻松定制Environment,如下例所示:

Java
@Test
void serviceNameCanBeConfigured() {
    this.contextRunner.withPropertyValues("user.name=test123").run((context) -> {
        assertThat(context).hasSingleBean(MyService.class);
        assertThat(context.getBean(MyService.class).getName()).isEqualTo("test123");
    });
}
Kotlin
@Test
fun serviceNameCanBeConfigured() {
    contextRunner.withPropertyValues("user.name=test123").run { context: AssertableApplicationContext ->
        assertThat(context).hasSingleBean(MyService::class.java)
        assertThat(context.getBean(MyService::class.java).name).isEqualTo("test123")
    }
}

该运行器还可以用于显示ConditionEvaluationReport。报告可以以INFODEBUG级别打印。以下示例显示了如何使用ConditionEvaluationReportLoggingListener在自动配置测试中打印报告。

Java
import org.junit.jupiter.api.Test;

import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener;
import org.springframework.boot.logging.LogLevel;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

class MyConditionEvaluationReportingTests {

    @Test
    void autoConfigTest() {
        new ApplicationContextRunner()
            .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
            .run((context) -> {
                // Test something...
            });
    }

}
Kotlin
import org.junit.jupiter.api.Test
import org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
import org.springframework.boot.logging.LogLevel
import org.springframework.boot.test.context.assertj.AssertableApplicationContext
import org.springframework.boot.test.context.runner.ApplicationContextRunner

class MyConditionEvaluationReportingTests {

    @Test
    fun autoConfigTest() {
        ApplicationContextRunner()
            .withInitializer(ConditionEvaluationReportLoggingListener.forLogLevel(LogLevel.INFO))
            .run { context: AssertableApplicationContext? -> }
    }

}

12.4.1. 模拟Web上下文

如果需要测试仅在servlet或响应式Web应用程序上下文中运行的自动配置,请分别使用WebApplicationContextRunnerReactiveWebApplicationContextRunner

12.4.2. 覆盖类路径

还可以测试当特定类和/或包在运行时不存在时会发生什么。Spring Boot提供了一个FilteredClassLoader,可以很容易地由运行器使用。在以下示例中,我们断言如果MyService不存在,则自动配置将被正确禁用:

Java
@Test
void serviceIsIgnoredIfLibraryIsNotPresent() {
    this.contextRunner.withClassLoader(new FilteredClassLoader(MyService.class))
        .run((context) -> assertThat(context).doesNotHaveBean("myService"));
}
Kotlin
@Test
fun serviceIsIgnoredIfLibraryIsNotPresent() {
    contextRunner.withClassLoader(FilteredClassLoader(MyService::class.java))
        .run { context: AssertableApplicationContext? ->
            assertThat(context).doesNotHaveBean("myService")
        }
}

12.5. 创建自定义Starter

典型的Spring Boot Starter包含用于自动配置和定制给定技术基础设施的代码,让我们称之为"acme"。为了使其易于扩展,可以将专用命名空间中的一些配置键暴露给环境。最后,提供单个"starter"依赖项,以帮助用户尽可能轻松地开始使用。

具体来说,自定义Starter可以包含以下内容:

  • 包含"acme"的自动配置代码的autoconfigure模块。

  • 提供依赖于autoconfigure模块以及"acme"和通常有用的任何其他依赖项的starter模块。简而言之,添加Starter应提供开始使用该库所需的一切。

将这两个模块分开并非必要。如果"acme"有多种风格、选项或可选功能,那么最好将自动配置分开,因为这样可以清楚地表达某些功能是可选的。此外,您可以制作一个提供关于这些可选依赖项的意见的Starter。同时,其他人可以仅依赖于autoconfigure模块,并使用不同意见制作自己的Starter。

如果自动配置相对简单且没有可选功能,则合并两个模块到Starter中绝对是一个选项。

12.5.1. 命名

确保为您的Starter提供适当的命名空间。不要以spring-boot开头命名您的模块,即使您使用不同的Maven groupId。我们将来可能会为您自动配置的内容提供官方支持。

作为一个经验法则,您应该根据Starter命名一个组合模块。例如,假设您正在为"acme"创建一个Starter,并且您将自动配置模块命名为acme-spring-boot,Starter命名为acme-spring-boot-starter。如果只有一个组合了两者的模块,请将其命名为acme-spring-boot-starter

12.5.2. 配置键

如果您的Starter提供配置键,请为它们使用唯一的命名空间。特别是,不要将您的键包含在Spring Boot使用的命名空间中(例如servermanagementspring等)。如果使用相同的命名空间,我们将来可能会以破坏您的模块的方式修改这些命名空间。作为一个经验法则,将所有键都以您拥有的命名空间前缀(例如acme)。

确保通过为每个属性添加字段javadoc来记录配置键,如下例所示:

Java
import java.time.Duration;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("acme")
public class AcmeProperties {

    /**
     * 是否检查acme资源的位置。
     */
    private boolean checkLocation = true;

    /**
     * 建立与acme服务器连接的超时时间。
     */
    private Duration loginTimeout = Duration.ofSeconds(3);

    // getters/setters ...

    public boolean isCheckLocation() {
        return this.checkLocation;
    }

    public void setCheckLocation(boolean checkLocation) {
        this.checkLocation = checkLocation;
    }

    public Duration getLoginTimeout() {
        return this.loginTimeout;
    }

    public void setLoginTimeout(Duration loginTimeout) {
        this.loginTimeout = loginTimeout;
    }

}
Kotlin
import org.springframework.boot.context.properties.ConfigurationProperties
import java.time.Duration

@ConfigurationProperties("acme")
class AcmeProperties(

    /**
     * 是否检查acme资源的位置。
     */
    var isCheckLocation: Boolean = true,

    /**
     * 建立与acme服务器连接的超时时间。
     */
    var loginTimeout:Duration = Duration.ofSeconds(3))
您应该只在@ConfigurationProperties字段Javadoc中使用纯文本,因为它们在添加到JSON之前不会被处理。

以下是我们内部遵循的一些规则,以确保描述保持一致:

  • 不要以"The"或"A"开头描述。

  • 对于boolean类型,描述应以"Whether"或"Enable"开头。

  • 对于基于集合的类型,描述应以"逗号分隔列表"开头。

  • 使用java.time.Duration而不是long,如果默认单位与毫秒不同,请描述默认单位,例如"如果未指定持续时间后缀,将使用秒"。

  • 除非必须在运行时确定,默认值不要在描述中提供。

确保触发元数据生成,以便IDE也可为您的键提供帮助。您可能需要查看生成的元数据(META-INF/spring-configuration-metadata.json),以确保您的键得到适当的文档。在兼容的IDE中使用您自己的Starter也是一个验证元数据质量的好方法。

12.5.3. “autoconfigure”模块

autoconfigure模块包含启动库所需的一切。它还可以包含配置键定义(例如@ConfigurationProperties)和任何可用于进一步自定义组件初始化方式的回调接口。

您应将对库的依赖标记为可选,以便更轻松地将autoconfigure模块包含在您的项目中。如果以这种方式执行,库将不会被提供,并且默认情况下,Spring Boot会后退。

Spring Boot使用注解处理器在元数据文件(META-INF/spring-autoconfigure-metadata.properties)中收集自动配置的条件。如果存在该文件,它将用于急切地过滤不匹配的自动配置,从而提高启动时间。

使用Maven构建时,建议在包含自动配置的模块中添加以下依赖项:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure-processor</artifactId>
    <optional>true</optional>
</dependency>

如果在您的应用程序中直接定义了自动配置,请确保配置spring-boot-maven-plugin,以防止repackage目标将依赖项添加到超级JAR中:

<project>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.springframework.boot</groupId>
                            <artifactId>spring-boot-autoconfigure-processor</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

对于Gradle,依赖项应在annotationProcessor配置中声明,如下例所示:

dependencies {
    annotationProcessor "org.springframework.boot:spring-boot-autoconfigure-processor"
}

12.5.4. 起始模块

起始模块实际上是一个空的jar包。它的唯一目的是提供与库一起工作所需的依赖关系。您可以将其视为对开始所需内容的一种主观看法。

不要对添加了您的起始器的项目做出假设。如果您正在自动配置的库通常需要其他起始器,请同时提及它们。如果可选依赖项数量很多,提供一组适当的默认依赖项可能很困难,因为您应避免包含对库的典型用法不必要的依赖项。换句话说,您不应包含可选依赖项。

无论如何,您的起始器必须直接或间接引用核心Spring Boot起始器(spring-boot-starter)(如果您的起始器依赖于另一个起始器,则无需添加它)。如果只使用您的自定义起始器创建项目,则Spring Boot的核心功能将通过核心起始器的存在得到支持。

13. Kotlin支持

Kotlin 是一种静态类型语言,面向 JVM(和其他平台),允许编写简洁优雅的代码,同时与用 Java 编写的现有库具有互操作性

Spring Boot通过利用其他Spring项目(如Spring Framework、Spring Data和Reactor)提供了对Kotlin的支持。有关更多信息,请参阅Spring Framework Kotlin支持文档

开始使用Spring Boot和Kotlin的最简单方法是按照这个全面的教程。您可以使用start.spring.io创建新的Kotlin项目。如果需要支持,请随时加入Kotlin Slack的#spring频道,或在Stack Overflow上使用springkotlin标签提问。

13.1. 要求

Spring Boot至少需要 Kotlin 1.7.x,并通过依赖管理管理适当的Kotlin版本。要使用Kotlin,必须在类路径上存在org.jetbrains.kotlin:kotlin-stdliborg.jetbrains.kotlin:kotlin-reflect。还可以使用kotlin-stdlib的变体kotlin-stdlib-jdk7kotlin-stdlib-jdk8

由于Kotlin类默认为final,您可能希望配置kotlin-spring插件,以便自动打开Spring注释的类,以便它们可以被代理。

在Kotlin中序列化/反序列化JSON数据需要Jackson的Kotlin模块。当类路径上发现时,它会自动注册。如果存在Jackson和Kotlin,但未找到Jackson Kotlin模块,则会记录警告消息。

如果在start.spring.io上启动Kotlin项目,则默认提供这些依赖项和插件。

13.2. 空安全

Kotlin的一个关键特性是空安全。它在编译时处理null值,而不是将问题推迟到运行时并遇到NullPointerException。这有助于消除常见的错误来源,而无需支付像Optional这样的包装器的成本。Kotlin还允许在具有可空值的情况下使用函数构造,如Kotlin中空安全的全面指南所述。

尽管Java不允许在其类型系统中表达空安全性,但Spring Framework、Spring Data和Reactor现在通过友好的工具注解提供其API的空安全性。默认情况下,在Kotlin中使用的Java API类型被识别为平台类型,对于这些类型,空检查是放松的。结合Kotlin对JSR 305注解的支持,以及空性注解,为Kotlin中相关的Spring API提供了空安全性。

可以通过添加-Xjsr305编译器标志来配置JSR 305检查,选项如下:-Xjsr305={strict|warn|ignore}。默认行为与-Xjsr305=warn相同。strict值要求在Kotlin类型中考虑空安全性,但应该在了解Spring API空性声明可能在次要版本之间甚至在将来的版本中发生变化的情况下使用)。

泛型类型参数、可变参数和数组元素的空性尚不受支持。有关最新信息,请参阅SPR-15942。还要注意,Spring Boot自身的API尚未进行注释

13.3. Kotlin API

13.3.1. runApplication

Spring Boot提供了一种符合惯例的方式来运行应用程序,使用runApplication<MyApplication>(*args),如下例所示:

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class MyApplication

fun main(args: Array<String>) {
    runApplication<MyApplication>(*args)
}

这是SpringApplication.run(MyApplication::class.java, *args)的一个可替换项。它还允许自定义应用程序,如下例所示:

runApplication<MyApplication>(*args) {
    setBannerMode(OFF)
}

13.3.2. 扩展

Kotlin的扩展提供了将现有类扩展为具有附加功能的能力。Spring Boot Kotlin API利用这些扩展向现有API添加新的Kotlin特定便利。

提供了TestRestTemplate扩展,类似于Spring Framework为Spring Framework中的RestOperations提供的扩展。除其他功能外,这些扩展还可以利用Kotlin的具体化类型参数。

13.4. 依赖管理

为了避免在类路径上混合不同版本的Kotlin依赖,Spring Boot导入了Kotlin BOM。

使用Maven,可以通过设置kotlin.version属性来自定义Kotlin版本,并为kotlin-maven-plugin提供插件管理。使用Gradle,Spring Boot插件会自动将kotlin.version与Kotlin插件的版本对齐。

Spring Boot还通过导入Kotlin Coroutines BOM来管理Coroutines依赖的版本。可以通过设置kotlin-coroutines.version属性来自定义版本。

如果在start.spring.io上启动具有至少一个响应式依赖项的Kotlin项目,则默认提供org.jetbrains.kotlinx:kotlinx-coroutines-reactor依赖项。

13.5. @ConfigurationProperties

@ConfigurationProperties构造函数绑定结合使用时,支持具有不可变val属性的类,如下例所示:

@ConfigurationProperties("example.kotlin")
data class KotlinExampleProperties(
        val name: String,
        val description: String,
        val myService: MyService) {

    data class MyService(
            val apiToken: String,
            val uri: URI
    )
}
要使用注解处理器生成自己的元数据,应配置kaptspring-boot-configuration-processor依赖项。请注意,由于kapt提供的模型存在限制,某些功能(如检测默认值或弃用项)由于模型kapt提供的限制而无法正常工作。

13.6. 测试

虽然可以使用JUnit 4来测试Kotlin代码,但默认提供并推荐使用JUnit 5。JUnit 5使得一个测试类可以实例化一次,并在所有类的测试中重复使用。这使得可以在非静态方法上使用@BeforeAll@AfterAll注解,这非常适合Kotlin。

要模拟Kotlin类,建议使用MockK。如果需要Mockito特定的@MockBean@SpyBean注解的MockK等效物,可以使用SpringMockK,它提供类似的@MockkBean@SpykBean注解。

13.7. 资源

13.7.2. 示例

14. SSL(安全套接层)

Spring Boot提供了配置SSL信任材料的能力,可以应用于多种类型的连接,以支持安全通信。具有前缀spring.ssl.bundle的配置属性可用于指定命名的信任材料集合及相关信息。

14.1. 使用Java KeyStore文件配置SSL

具有前缀spring.ssl.bundle.jks的配置属性可用于配置使用Java keytool实用程序创建的信任材料集合,并存储在JKS或PKCS12格式的Java KeyStore文件中。每个集合都有一个用户提供的名称,可用于引用该集合。

用于保护嵌入式Web服务器时,通常会配置一个keystore,其中包含如下示例中所示的证书和私钥:

属性
spring.ssl.bundle.jks.mybundle.key.alias=application
spring.ssl.bundle.jks.mybundle.keystore.location=classpath:application.p12
spring.ssl.bundle.jks.mybundle.keystore.password=secret
spring.ssl.bundle.jks.mybundle.keystore.type=PKCS12
Yaml
spring:
  ssl:
    bundle:
      jks:
        mybundle:
          key:
            alias: "application"
          keystore:
            location: "classpath:application.p12"
            password: "secret"
            type: "PKCS12"

用于保护客户端连接时,通常会配置一个truststore,其中包含如下示例中所示的服务器证书:

属性
spring.ssl.bundle.jks.mybundle.truststore.location=classpath:server.p12
spring.ssl.bundle.jks.mybundle.truststore.password=secret
Yaml
spring:
  ssl:
    bundle:
      jks:
        mybundle:
          truststore:
            location: "classpath:server.p12"
            password: "secret"

请参阅JksSslBundleProperties以获取支持的所有属性。

14.2. 使用PEM编码的证书配置SSL

具有前缀spring.ssl.bundle.pem的配置属性可用于配置以PEM编码文本形式的信任材料集合。每个集合都有一个用户提供的名称,可用于引用该集合。

用于保护嵌入式Web服务器时,通常会配置一个证书和私钥,如下示例中所示:

属性
spring.ssl.bundle.pem.mybundle.keystore.certificate=classpath:application.crt
spring.ssl.bundle.pem.mybundle.keystore.private-key=classpath:application.key
Yaml
spring:
  ssl:
    bundle:
      pem:
        mybundle:
          keystore:
            certificate: "classpath:application.crt"
            private-key: "classpath:application.key"

用于保护客户端连接时,通常会配置一个truststore,其中包含如下示例中所示的服务器证书:

属性
spring.ssl.bundle.pem.mybundle.truststore.certificate=classpath:server.crt
Yaml
spring:
  ssl:
    bundle:
      pem:
        mybundle:
          truststore:
            certificate: "classpath:server.crt"

PEM内容可以直接用于certificateprivate-key属性。如果属性值包含BEGINEND标记,则它们将被视为PEM内容,而不是资源位置。

以下示例显示了如何定义信任库证书:

属性
spring.ssl.bundle.pem.mybundle.truststore.certificate=\
-----BEGIN CERTIFICATE-----\n\
MIID1zCCAr+gAwIBAgIUNM5QQv8IzVQsgSmmdPQNaqyzWs4wDQYJKoZIhvcNAQEL\n\
BQAwezELMAkGA1UEBhMCWFgxEjAQBgNVBAgMCVN0YXRlTmFtZTERMA8GA1UEBwwI\n\
...\n\
V0IJjcmYjEZbTvpjFKznvaFiOUv+8L7jHQ1/Yf+9c3C8gSjdUfv88m17pqYXd+Ds\n\
HEmfmNNjht130UyjNCITmLVXyy5p35vWmdf95U3uEbJSnNVtXH8qRmN9oK9mUpDb\n\
ngX6JBJI7fw7tXoqWSLHNiBODM88fUlQSho8\n\
-----END CERTIFICATE-----\n
Yaml
spring:
  ssl:
    bundle:
      pem:
        mybundle:
          truststore:
            certificate: |
              -----BEGIN CERTIFICATE-----
              MIID1zCCAr+gAwIBAgIUNM5QQv8IzVQsgSmmdPQNaqyzWs4wDQYJKoZIhvcNAQEL
              BQAwezELMAkGA1UEBhMCWFgxEjAQBgNVBAgMCVN0YXRlTmFtZTERMA8GA1UEBwwI
              ...
              V0IJjcmYjEZbTvpjFKznvaFiOUv+8L7jHQ1/Yf+9c3C8gSjdUfv88m17pqYXd+Ds
              HEmfmNNjht130UyjNCITmLVXyy5p35vWmdf95U3uEbJSnNVtXH8qRmN9oK9mUpDb
              ngX6JBJI7fw7tXoqWSLHNiBODM88fUlQSho8
              -----END CERTIFICATE-----

请参阅PemSslBundleProperties以获取支持的所有属性。

14.3. 应用SSL集合

使用属性配置后,SSL集合可以通过名称在Spring Boot自动配置的各种连接的配置属性中引用。有关更多信息,请参阅关于嵌入式Web服务器数据技术REST客户端的部分。

14.4. 使用SSL集合

Spring Boot自动配置了一个类型为SslBundles的bean,该bean提供了对使用spring.ssl.bundle属性配置的每个命名集合的访问。

可以从自动配置的SslBundles bean中检索一个SslBundle,并用它来创建用于配置客户端库中SSL连接的对象。SslBundle提供了获取这些SSL对象的分层方法:

  • getStores()提供对密钥库和信任库java.security.KeyStore实例以及任何所需的密钥库密码的访问。

  • getManagers()提供对java.net.ssl.KeyManagerFactoryjava.net.ssl.TrustManagerFactory实例以及它们创建的java.net.ssl.KeyManagerjava.net.ssl.TrustManager数组的访问。

  • createSslContext()提供了获取新的java.net.ssl.SSLContext实例的便捷方法。

此外,SslBundle提供了有关正在使用的密钥、要使用的协议以及应用于SSL引擎的任何选项的详细信息。

以下示例显示了如何检索一个SslBundle并使用它来创建一个SSLContext

Java
import javax.net.ssl.SSLContext;

import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.stereotype.Component;

@Component
public class MyComponent {

    public MyComponent(SslBundles sslBundles) {
        SslBundle sslBundle = sslBundles.getBundle("mybundle");
        SSLContext sslContext = sslBundle.createSslContext();
        // 使用创建的sslContext执行操作
    }

}
Kotlin
import org.springframework.boot.ssl.SslBundles
import org.springframework.stereotype.Component

@Component
class MyComponent(sslBundles: SslBundles) {

    init {
        val sslBundle = sslBundles.getBundle("mybundle")
        val sslContext = sslBundle.createSslContext()
        // 使用创建的sslContext执行操作
    }

}

14.5. 重新加载SSL捆绑包

当密钥材料发生变化时,可以重新加载SSL捆绑包。消费该捆绑包的组件必须与可重新加载的SSL捆绑包兼容。目前以下组件兼容:

  • Tomcat Web服务器

  • Netty Web服务器

要启用重新加载,您需要通过配置属性选择,如下例所示:

属性
spring.ssl.bundle.pem.mybundle.reload-on-update=true
spring.ssl.bundle.pem.mybundle.keystore.certificate=file:/some/directory/application.crt
spring.ssl.bundle.pem.mybundle.keystore.private-key=file:/some/directory/application.key
Yaml
spring:
  ssl:
    bundle:
      pem:
        mybundle:
          reload-on-update: true
          keystore:
            certificate: "file:/some/directory/application.crt"
            private-key: "file:/some/directory/application.key"

然后,文件监视器会监视文件,如果文件发生更改,SSL捆绑包将被重新加载。这将触发消费组件的重新加载,例如,Tomcat会在启用SSL连接器中旋转证书。

您可以使用spring.ssl.bundle.watch.file.quiet-period属性配置文件监视器的静默期(以确保没有更改)。

15. 接下来阅读什么

如果您想了解本节讨论的任何类的更多信息,请参阅Spring Boot API文档,或者您可以直接浏览源代码。如果您有具体问题,请参阅how-to部分。

如果您对Spring Boot的核心功能感到满意,您可以继续阅读有关生产就绪功能的内容。