7. 核心功能(Core Features)
本章节深入介绍 Spring Boot 的详细信息。在这里,你可以了解想要使用的和自定义的主要功能。如果您还没有阅读 “Getting Started” 和 “Developing with Spring Boot” 这两节内容,不放先去阅读这两节内容,以便对基础知识有一个很好的了解。
7.1. SpringApplication
SpringApplication 类为 Spring 应用程序提供了一种方便的引导方法,从 main() 方法启动。在很多情况下,你可以调用静态 SpringApplication.run 方法,如下所示:
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
当您的应用程序启动时,您应该看到类似于以下输出的内容:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.18-SNAPSHOT)
2023-11-22 15:39:20.275 INFO 4032 --- [ main] o.s.b.d.f.logexample.MyApplication : Starting MyApplication using Java 1.8.0_392 on myhost with PID 4032 (/opt/apps/myapp.jar started by myuser in /opt/apps/)
2023-11-22 15:39:20.281 INFO 4032 --- [ main] o.s.b.d.f.logexample.MyApplication : No active profile set, falling back to 1 default profile: "default"
2023-11-22 15:39:21.636 INFO 4032 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
2023-11-22 15:39:21.649 INFO 4032 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2023-11-22 15:39:21.650 INFO 4032 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.83]
2023-11-22 15:39:21.735 INFO 4032 --- [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2023-11-22 15:39:21.735 INFO 4032 --- [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1401 ms
2023-11-22 15:39:22.214 INFO 4032 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2023-11-22 15:39:22.228 INFO 4032 --- [ main] o.s.b.d.f.logexample.MyApplication : Started MyApplication in 2.437 seconds (JVM running for 2.834)
默认情况下,会显示 INFO 日志信息,包括一些相关的启动详细信息,如启动应用程序的用户。如果需要除 INFO 以外的日志级别,可以按照 Log Levels 中的说明进行设置。应用程序版本由主应用程序类包中的实现版本决定。将 spring.main.log-startup-info 设为 false,可以关闭启动信息日志记录。这也将关闭应用程序profiles配置文件的日志记录。
::: tip 提示
要在启动过程中添加额外的日志记录,可以重写 SpringApplication 子类中的logStartupInfo(boolean)方法。
:::
7.1.1 启动失败(Startup Failure)
如果应用程序启动失败,已注册的 "FailureAnalyzers "将有机会提供专门的错误消息和解决问题的具体操作。例如,如果您在端口 8080 上启动网络应用程序,而该端口已在使用中,您应该会看到与下面类似的消息:
***************************
APPLICATION FAILED TO START
***************************
Description:
Embedded servlet container failed to start. Port 8080 was already in use.
Action:
Identify and stop the process that is listening on port 8080 or configure this application to listen on another port.
::: tip 备注
Spring Boot 提供了大量 FailureAnalyzer 实现,你也可以 添加自己的实现。
:::
如果没有故障分析器能够处理异常,您仍然可以显示完整的条件报告,以便更好地了解出错的原因。为此,您需要为org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener 启用 debug 属性 或 启用 DEBUG 日志 。
例如,如果您使用 java -jar 运行应用程序,您可以按如下方法启用 debug 属性:
$ java -jar myproject-0.0.1-SNAPSHOT.jar --debug
7.1.2 懒加载(Lazy Initialization)
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
::: tip 提示
如果您想对某些 Bean 禁用懒加载,同时对应用程序的其他部分使用懒加载,您可以使用 @Lazy(false) 注解显式地将它们的 lazy 属性设置为 false。
:::
7.1.3 自定义Banner(Customizing the Banner)
启动时打印的 banner 可以通过在类路径中添加 "banner.txt "文件或将 "spring.banner.location "属性设置为该文件的位置来更改。如果文件的编码不是 UTF-8,可以设置 spring.banner.charset 属性。除文本文件外,还可以在类路径中添加 banner.gif、banner.jpg 或 banner.png图像文件,或设置 spring.banner.image.location 属性。图像会转换成 ASCII 艺术表现形式,并打印在任何文字 banner 之上。
在banner.txt文件中,您可以使用Environment中的任何可用键以及以下任何占位符:
| Variable | Description |
|---|---|
${application.version} |
在 MANIFEST.MF 中声明的应用程序版本号。例如 Implementation-Version: 1.0 打印为 1.0。 |
${application.formatted-version} |
应用程序的版本号,在 MANIFEST.MF 中声明并格式化显示(用括号包围并以 v 为前缀)。例如 (v1.0)。 |
${spring-boot.version} |
您使用的 Spring Boot 版本。例如 2.7.18-SNAPSHOT。 |
${spring-boot.formatted-version} |
您正在使用的 Spring Boot 版本,显示格式为(用括号包围,前缀为 v)。例如 (v2.7.18-SNAPSHOT)。 |
${Ansi.NAME} (or ${AnsiColor.NAME}, ${AnsiBackground.NAME}, ${AnsiStyle.NAME}) |
其中NAME是ANSI转义码的名称。详情 AnsiPropertySource请见。 |
${application.title} |
在 MANIFEST.MF 中声明的应用程序标题。例如,Implementation-Title: MyApp 打印为 MyApp。 |
::: tip 提示
如果想以编程方式生成banner,可以使用SpringApplication.setBanner(...)方法。使用 org.springframework.boot.Banner 接口并实现自己的 printBanner() 方法。
:::
您还可以使用spring.main.banner-mode属性来决定是否要将banner打印到System.out (console)、发送到配置的日志记录器 (log),或者根本不打印 (off)。
打印的banner将作为单例 Bean 注册,名称如下:springBootBanner。
::: tip 备注
application.title, application.version, 和 application.formatted-version 属性仅在使用 Spring Boot 启动程序的 java -jar 和 java -cp 命令时可用。如果运行未打包的 jar 并使用 java -cp <classpath> <mainclass> 启动,这些值将不会被解析。要使用 application. 配置,使用java -jar运行jar包或者使用 java org.springframework.boot.loader.JarLauncher运行非jar。这将在构建 classpath 和启动应用程序之前初始化 application. banner 属性。
::: tip
7.1.4 自定义SpringApplication(Customizing SpringApplication)
如果 "SpringApplication "的默认设置不符合你的需求,你可以创建一个本地实例并对其进行自定义。例如,要关闭横banner,如下所示:
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(MyApplication.class);
application.setBannerMode(Banner.Mode.OFF);
application.run(args);
}
}
::: tip 提示
传递给SpringApplication的构造函数参数是Spring Bean的配置源。大多数情况下,这些参数是对 @Configuration 类的引用,但也可能是对 @Component 类的直接引用。
::: tip
也可以使用 "application.properties "文件配置 “SpringApplication”。详情参阅 Externalized Configuration 。
有关配置选项的完整列表,请参阅 SpringApplication Javadoc。
7.1.5 Fluent Builder API
如果你需要构建一个 ApplicationContext层次结构(具有父/子关系的多个上下文),或者你更喜欢使用 "流畅 "的创建器API,那么你可以使用 SpringApplicationBuilder。
如下面的例子所示,SpringApplicationBuilder可以将多个方法调用串联起来,其中的parent和 child方法可以让你创建一个层次结构:
new SpringApplicationBuilder()
.sources(Parent.class)
.child(Application.class)
.bannerMode(Banner.Mode.OFF)
.run(args);
::: tip 备注
创建 ApplicationContext 层次结构时有一些限制。例如,Web 组件必须包含在子上下文中,父上下文和子上下文使用相同的环境。详情参阅 SpringApplicationBuilder Javadoc 。
::: tip
7.1.6 应用可用性(Application Availability)
在平台上部署应用程序时,应用程序可以使用Kubernetes Probes等基础设施向平台提供有关其可用性的信息。Spring Boot 包含对常用的 "liveness "和 "readiness "可用性状态的支持,开箱即用。如果您使用的是 Spring Boot 是 "actuator "支持,那么这些状态就会作为健康端点组暴露出来。
此外,您还可以通过实现 ApplicationAvailability 接口注入自己的bean来获取可用性状态。
运行状态(Liveness State)
应用程序的 "运行 "状态说明其内部状态是否允许其正常工作,或在当前失败的情况下自行恢复。损坏的 "运行 "状态意味着应用程序处于无法恢复的状态,基础架构应重新启动应用程序。
::: tip 备注
一般来说,“运行”状态不应该基于外部检测,例如 健康检测。如果这样做,外部系统(数据库、Web API、外部缓存)的故障就会在整个平台上引发大规模的重启和连锁故障。
:::
Spring Boot 应用程序的内部状态主要由 Spring ApplicationContext 表示。如果应用程序上下文已成功启动,Spring Boot 就认为应用程序处于有效状态。一旦上下文被刷新,应用程序即被视为已激活,详情参阅 Spring Boot 应用程序生命周期和相关应用程序事件。
就绪状态(Readiness State)
应用程序的 "就绪 "状态说明应用程序是否已准备好处理请求流量。错误的 "就绪 "状态会告诉平台暂时不应将请求转发到应用程序。这种情况通常发生在启动过程中、CommandLineRunner和 ApplicationRunner 组件正在处理过程中时,或者在应用程序认为太忙无法处理额外请求的时候。
一旦应用程序和命令行运行程序被调用,应用程序即被视为准备就绪,请参阅 Spring Boot 应用程序生命周期和相关应用程序事件。
::: tip 提示
预计在启动期间运行的任务应由 CommandLineRunner 和 ApplicationRunner 组件执行,而不是使用 Spring 组件生命周期回调(如 @PostConstruct)。
:::
管理应用可用性状态(Managing the Application Availability State)
应用程序组件可以通过注入 ApplicationAvailability 接口并调用其上的方法,随时检索当前的可用性状态。更常见的情况是,应用程序希望监听状态更新或更新应用程序的状态。
例如,我们可以将应用程序的 "就绪 "状态导出到一个文件,这样 Kubernetes 的 "执行探针 "就可以查看该文件:
@Component
public class MyReadinessStateExporter {
@EventListener
public void onStateChange(AvailabilityChangeEvent<ReadinessState> event) {
switch (event.getState()) {
case ACCEPTING_TRAFFIC:
// create file /tmp/healthy
break;
case REFUSING_TRAFFIC:
// remove file /tmp/healthy
break;
}
}
}
当应用程序发生故障而无法恢复时,我们还可以更新应用程序的状态:
@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);
}
}
}
Spring Boot 提供 利用 Actuator Health Endpoint 对 Kubernetes HTTP 进行 "Liveness "和 "Readiness "探测.。你可以在 在 Kubernetes 上部署 Spring Boot 应用程序的专门部分获取更多的指定。
7.1.7 应用程序事件和监听器(Application Events and Listeners)
除了常用的 Spring Framework 事件外,例如 ContextRefreshedEvent, SpringApplication 还会发送一些额外的应用程序事件。
::: tip 备注
有些事件实际上是在创建应用程序上下文 ApplicationContext 之前触发的,因此你不能使用 @Bean来注册这些事件的监听器。你可以使用SpringApplication.addListeners(…) 方法或者 SpringApplicationBuilder.listeners(…) 方法来进行注册。 如果你想让这些监听器自动注册,而不管应用程序是如何创建的,你可以在项目中添加一个 META-INF/spring.factories 文件,并使用 org.springframework.context.ApplicationListener 作为key来引用监听器,例如: org.springframework.context.ApplicationListener=com.example.project.MyListener
:::
应用程序运行时,应按以下顺序发送应用程序事件:
- 除了注册监听器和初始化程序之外,在运行开始时且任何处理之前发送应用程序启动事件(
ApplicationStartingEvent)。 - 当在上下文中使用的
Environment已知时,且在创建上下文之前,将发送应用环境准备事件ApplicationEnvironmentPreparedEvent。 - 当
ApplicationContext已经准备就绪且 ApplicationContextInitializers 已经被调用,但是尚未加载任何定义的 Bean 时,将发送ApplicationContextInitializedEvent。 - 在刷新开始之前,但是在加载定义的 Bean 之后,将发送
ApplicationPreparedEvent。 - 在上下文刷新之后,但是在调用任何应用程序和命令行运行程序之前,会发送
ApplicationStartedEvent。 - 紧接着会发生一个
AvailabilityChangeEvent(可用性更改事件),并带有LivenessState.CORRECT,以表示应用程序是处于运行状态的。 - 在调用任何 应用程序和命令行运行程序 之后,将发送
ApplicationReadyEvent。 - 紧接着会发送
AvailabilityChangeEvent(可用性更改事件),并带有ReadinessState.ACCEPTING_TRAFFIC,表示应用程序已经为请求提供服务做好准备。 - 如果启动时出现异常,则会发送
ApplicationFailedEvent。
上述列表只包括SpringApplication绑定的 SpringApplicationEvent。除此之外,以下事件也会在 ApplicationPreparedEvent 之后、ApplicationStartedEvent之前发布:
WebServer准备就绪后,会发送WebServerInitializedEvent。ServletWebServerInitializedEvent和ReactiveWebServerInitializedEvent分别是 servlet 和 reactive 的变体。- 当刷新
ApplicationContext时,会发送ContextRefreshedEvent。
::: tip 提示
您通常不需要使用应用程序事件,但知道它们的存在可能会很有利。在内部,Spring Boot 使用事件来处理各种任务。
:::
::: tip 备注
事件监听器不应运行潜在的冗长任务,因为它们默认在同一线程中执行。可以考虑使用 application and command-line runners 来代替。
:::
应用程序事件是通过 Spring Framework 的事件发布机制发送的。该机制的部分功能是确保向子上下文中的监听器发布的事件也会向任何祖先上下文中的监听器发布。因此,如果您的应用程序使用SpringApplication 实例的层次结构,监听器可能会接收到同一类型应用程序事件的多个实例。
为使监听器能够区分其上下文的事件和后代上下文的事件,监听器应请求注入其应用程序上下文,然后将注入的上下文与事件的上下文进行比较。可以通过实现 ApplicationContextAware 来注入上下文,如果监听器是一个 Bean,则可以通过使用 @Autowired来注入上下文。
7.1.8 Web环境(Web Environment)
SpringApplication 会尝试代表你创建正确的ApplicationContext。用于确定WebApplicationType的算法非常简单:
- 如果 Spring MVC 存在,则使用
AnnotationConfigServletWebServerApplicationContext。 - 如果 Spring MVC 不存在但是 Spring WebFlux 存在,则使用
AnnotationConfigReactiveWebServerApplicationContext。 - 否则,使用
AnnotationConfigApplicationContext。
这意味着,如果在同一应用程序中使用 Spring MVC 和 Spring WebFlux 的 WebClient,则默认使用 Spring MVC。您可以通过调用 setWebApplicationType(WebApplicationType) 方法轻松覆盖该功能。
通过调用 setApplicationContextFactory(...),还可以完全控制使用的 ApplicationContext 类型。
::: tip 提示
在 JUnit 测试中使用SpringApplication时,通常需要调用 setWebApplicationType(WebApplicationType.NONE)。
:::
7.1.9 访问应用程序参数(Accessing Application Arguments)
如果需要访问传递给SpringApplication.run(...)的应用程序参数,可以注入一个org.springframework.boot.ApplicationArguments bean。如下所示,ApplicationArguments接口既能访问原始的String[]参数,也能访问解析后的 option和 non-option参数:
@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"]
}
}
::: tip 提示
Spring Boot 还会在 Spring Environment 中注册 CommandLinePropertySource 。通过使用 @Value 注解,您还可以注入单个应用程序参数。
:::
7.1.10 使用ApplicationRunner或CommandLineRunner(Using the ApplicationRunner or CommandLineRunner)
如果你需要在 SpringApplication启动后运行一些特定的代码,你可以实现ApplicationRunner或 CommandLineRunner接口。这两个接口的工作方式相同,都提供了一个 run方法,该方法会在 SpringApplication.run(...)完成之前被调用。
::: tip 备注
这种约定非常适合在应用程序启动后、开始接受请求前运行的任务。
:::
CommandLineRunner 接口以字符串数组的形式访问应用程序参数,而 ApplicationRunner 则使用 ApplicationArguments 接口作为参数访问应用程序参数。如下所示,展示了 CommandLineRunner 的 run 方法:
@Component
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) {
// Do something...
}
}
如果定义了多个必须按特定顺序调用的 CommandLineRunner 或 ApplicationRunner Bean,您可以额外实现 org.springframework.core.Ordered 接口或使用 org.springframework.core.annotation.Order 注解。
7.1.11 应用退出(Application Exit)
每个 SpringApplication 都会向 JVM 注册一个 shutdown hook,以确保 ApplicationContext 在退出时可以正常关闭。所有的标准 Spring 生命周期回调(如DisposableBean 接口或 @PreDestroy 注解)都可以使用。
此外,此外,如果希望在调用 SpringApplication.exit() 时返回特定的退出编码,可以实现 org.springframework.boot.ExitCodeGenerator 接口。然后可将该退出编码传递给 System.exit(),使其作为状态编码返回,如下例所示:
@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)));
}
}
此外,ExitCodeGenerator接口可由exceptions实现。遇到此类exception时,Spring Boot 会返回由已实现的 getExitCode() 方法提供的退出代码。
如果有多个 ExitCodeGenerator 方法,则使用最先生成的非零退出代码。要控制生成器的调用顺序,可额外实现 org.springframework.core.Ordered 接口或使用 org.springframework.core.annotation.Order 注解。
7.1.12 管理员功能(Admin Features)
通过指定spring.application.admin.enabled 属性,可以启用应用程序的管理相关功能。将在平台 MBeanServer 上公开 SpringApplicationAdminMXBean。 您可以使用此功能远程管理 Spring Boot 应用程序。该功能对于任何服务包装器的实现也很有用。
::: tip 提示
如果想知道应用程序在哪个 HTTP 端口上运行,请获取键值为 local.server.port的属性。
:::
7.1.13 应用程序启动跟踪(Application Startup tracking)
在应用程序启动期间,SpringApplication和ApplicationContext会执行许多与应用程序生命周期、Bean生命周期甚至处理应用程序事件相关的任务。通过 ApplicationStartup,Spring Framework 允许你使用 StartupStep 对象跟踪应用程序的启动顺序。收集这些数据的目的是用于剖析,或者为了更好地理解应用程序的启动过程。
当设置 SpringApplication 实例时,你可以选择一个 ApplicationStartup 实现。例如,如果需要使用 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);
}
}
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 endpoint ,以JSON文档的形式提供信息。
7.2 外部配置(Externalized Configuration)
Spring Boot 允许您使用外部化配置,以便您可以在不同的环境中使用相同的应用程序代码。你可以使用属性文件、YAML文件、环境变量、命令行参数来外部化配置。
Property 值可以通过 @Value 注解直接注入到Bean中,通过 Spring 的 Environment 抽象访问,或者通过@ConfigurationProperties 绑定到结构化对象 中。
Spring Boot 使用一种非常特殊的 PropertySource 顺序,其目的允许对值进行合理的覆盖。后面的配置属性源可以覆盖前面的配置属性源中定义的值。配置属性源按以下顺序加载:
- 默认属性(通过设置
SpringApplication.setDefaultProperties指定)。 - 在
@Configuration类上添加@PropertySource注解。请注意,在应用程序上下文刷新之前,此类属性源不会加载到Environment中。这对于某些配置属性(例如在刷新开始之前读取的logging.*和spring.main.*)来说为时已晚。 - 配置数据(如
application.properties文件)。 RandomValuePropertySource仅在random.*类型配置中使用。- OS 环境变量。
- Java 系统属性 (
System.getProperties())。 - 来之
java:comp/env的 JNDI。 ServletContextinit 参数。ServletConfiginit 参数。- 来自
SPRING_APPLICATION_JSON的属性(嵌入在环境变量或系统变量中的内联JSON)。 - 命令行参数。
- 测试中的
properties。测试注解@SpringBootTest在 用于测试应用程序的特定片段 可用。 - 测试中的注解
@DynamicPropertySource - 测试中的注解
@TestPropertySource。 - 当 devtools 激活时,位于
$HOME/.config/spring-boot目录的Devtools 全局配置属性.
配置文件按以下顺序加载:
- 打包在 jar 中的 应用程序配置 (
application.properties和 YAML 配置文件)。 - 打包在 jar 中的 特定 Profile 的应用程序配置 (
application-{profile}.properties和 YAML配置)。 - jar之外的 应用程序配置 (
application.properties和 YAML配置)。 - jar之外的 特定 Profile 的应用程序配置(
application-{profile}.properties和 YAML配置)。
::: tip 备注
建议在整个应用程序中只使用一种格式。如果在同一位置同时使用 .properties 和 YAML 格式的配置文件,则 .properties 优先。
:::
::: tip 备注
如果使用环境变量而不是系统属性,大多数操作系统不允许使用以句点分隔的键名,但可以使用下划线代替(例如,用 SPRING_CONFIG_NAME 代替 spring.config.name )。详情参阅 Binding From Environment Variables。
:::
::: tip 备注
如果您的应用程序在 servlet 容器或应用程序服务器中运行,则可以使用 JNDI 属性(在 java:comp/env 中)或 servlet 上下文初始化参数来代替,与环境变量和系统属性一样使用。
:::
如下例所示:
@Component
public class MyBean {
@Value("${name}")
private String name;
// ...
}
在应用程序的 classpath 中(例如,在 jar 中),可以有一个 application.properties 文件,为 name 提供合理的默认属性值。在新环境中运行时,可以在 jar 之外提供一个application.properties文件来覆盖name。对于一次性测试,可以使用特定的命令行开关启动(例如,java -jar app.jar --name="Spring" )。
::: tip 提示
env 和 configprops 端点有助于确定属性具有特定值的原因。您可以使用这两个端点来诊断意外的属性值。详见"生产就绪功能 "部分。
:::
7.2.1 访问命令行属性(Accessing Command Line Properties)
默认情况下,SpringApplication 会将任何命令行参数(即以 --开头的参数,如 --server.port=9000)转换为 property,并将其添加到Spring Environment。如前所述,命令行属性优先于基于文件的数据。
如果不想将命令行属性添加到 Environment,可以使用SpringApplication.setAddCommandLineProperties(false)。
7.2.2 JSON格式应用程序属性(JSON Application Properties)
环境变量和系统属性通常有一些限制,意味着某些属性名称不能使用。为了帮助解决这个问题,Spring Boot 允许您将属性块编码为单个 JSON 结构。
当应用程序启动时,任何spring.application.json或SPRING_APPLICATION_JSON属性都将被解析并添加到 Environment。
例如,可以在UN*X 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 变量。
::: tip 备注
虽然,JSON格式中的 null 值将被添加到生成的属性中,但是 PropertySourcesPropertyResolver 将 null 属性视为缺失值。这意味着JSON无法使用 null 值覆盖优先级比较低的属性源中的属性。
:::
7.2.3 外部应用程序属性(External Application Properties)
Spring Boot 会从应用程序启动时自动从以下位置查找并加载 application.properties 和 application.yaml 文件:
- 从类路径加载
- 类路径根目录
- 类路径下的
/config包
- 从当前目录加载
- 当前目录
- 当前目录的子目录
config/ - 子目录
config/的子目录
列表按优先级排序(较低项目的值优先于较早项目的值)。加载文件中的文档作为 PropertySources 添加到 Spring Environment。
如果您不想使用 application 作为配置文件名,可以通过指定 spring.config.name 环境属性来切换到其它文件名。例如,要查找 myproject.properties 和 myproject.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
::: tip 提示
如果位置是可选的,且您不介意它们不存在,可以使用前缀 optional: 。
:::
::: warning 警告
spring.config.name, spring.config.location, 和 spring.config.additional-location很早就用于确定哪些文件需要加载。它们必须定义为环境属性(通常是操作系统环境变量、系统属性或命令行参数)。
:::
如果spring.config.location 包含目录(而不是文件),则应该以 / 结尾。在运行时,它们会在加载前被附加上 spring.config.name 配置的文件名称。在 spring.config.location 中 特定 profile 配置文件会被直接使用。
::: tip 提示
目录和文件位置值还会进行扩展,以检查 特定 profile 文件,例如,如果spring.config.location为classpath:myconfig.properties,则还会加载classpath:myconfig-<profile>.properties文件。
:::
在大多数情况下,你添加的每个 spring.config.location 项都会引用一个文件或目录。位置会按照定义的顺序进行处理,后面的位置可以覆盖前面位置的值。
如果您有复杂的位置配置,并且使用了特定profile的配置文件,您可能需要提供进一步的提示,以便 Spring Boot 知道应如何对它们进行分组。位置组是在同一级别考虑的位置的集合。例如,您可能想分组所有 classpath 位置,然后是所有外部位置。位置组内的项目应以"; "分隔。更多详情,请参阅"配置文件特定文件 "部分的示例。
使用 spring.config.location 配置的位置会取代默认位置。例如,如果 spring.config.location 的配置值为 optional:classpath:/custom-config/,optional:file:./custom-config/ ,则查找顺序为:
optional:classpath:custom-config/optional:file:./custom-config/
如果希望添加其他位置,而不是替换它们,可以使用 spring.config.additional-location 。从附加位置加载的属性可以覆盖默认位置中的属性。例如,如果 spring.config.additional-location 的配置值为optional:classpath:/custom-config/,optional:file:./custom-config/,则查找顺序为:
optional:classpath:/;optional:classpath:/config/optional:file:./;optional:file:./config/;optional:file:./config/*/optional:classpath:custom-config/optional:file:./custom-config/
这种搜索排序方法可以让你在一个配置文件中指定默认值,然后在另外一个配置文件中选择性覆盖这些值。你可以在其中一个默认位置的 application.properties(或用spring.config.name配置的文件名)中为应用程序提供默认值。这些默认值可以在运行时,使用自定义位置上的不同文件覆盖。
可选位置(Optional Locations)
默认情况下,如果指定的配置数据位置不存在,Spring Boot 将抛出 ConfigDataLocationNotFoundException 异常,应用程序将无法启动。
如果您想指定一个位置,但又不介意它可能不存在,可以使用 optional: 前缀。你可以在 spring.config.location 和 spring.config.additional-location 属性以及 spring.config.import 声明中使用该前缀。
例如,spring.config.import的值为optional:file:./myconfig.properties,如果myconfig.properties并不存在,则仍然允许应用程序启动。
如果想忽略所有 ConfigDataLocationNotFoundException 并始终继续启动应用程序,可以使用 spring.config.on-not-found 属性。使用 SpringApplication.setDefaultProperties(...)或系统/环境变量将值设为ignore。
通配符位置(Wildcard Locations)
如果配置文件位置的最后一个路径段包含*字符,则视为通配符位置。通配符会在加载配置时展开,因此直接子目录也会被检查。在 Kubernetes 等环境中,当配置属性有多个来源时,通配符位置尤其有用。
例如,如果你有一些 Redis 配置和一些 MySQL 配置,你可能希望将这两项配置分开,同时要求这两项配置都出现在一个 application.properties 文件中。这可能会导致两个单独的 application.properties 文件被挂载到不同的位置,如 /config/redis/application.properties 和 /config/mysql/application.properties。在这种情况下,如果通配符位置为 config/*/,则两个文件都会被处理。
默认情况下,Spring Boot 将 config/*/ 包含在默认搜索位置中。这意味着将搜索 jar 以外 /config 目录的所有子目录。
您可以在 spring.config.location 和 spring.config.additional-location 属性上使用通配符位置。
::: tip 备注
通配符位置必须只包含一个 *,并且在搜索目录位置时以 */ 结尾,在搜索文件位置时以 */<filename> 结尾。带有通配符的位置会根据文件名的绝对路径按字母顺序排序。
:::
::: tip 提示
通配符位置只适用于外部目录。不能在 classpath: 位置中使用通配符。
:::
特定Profile配置文件(Profile Specific Files)
除了 application 配置文件外,Spring Boot 还会尝试加载命名为 application-{profile}的特定 profile 的配置文件。例如,如果您的应用程序配置了profile 为 prod 并使用 YAML 文件,那么 application.yaml 和 application-prod.yaml 都将会被查找。
特定 profile 的配置文件和标准的 application.properties从相同位置加载,特定 profile 的配置文件总是优于非特定的配置文件。如果指定了多个配置文件,则采用last-wins策略。例如,如果通过spring.profiles.active属性指定了 prod,live 配置文件,则 application-prod.properties 中的值可能会被 application-live.properties中的值覆盖。
::: tip 备注
last-wins策略适用于 location group 级别。spring.config.location 配置的 classpath:/cfg/,classpath:/ext/和 classpath:/cfg/;classpath:/ext/的覆盖规则不同。
例如,继续上面的 prod,live 示例,我们可能有以下文件:
/cfg
application-live.properties
/ext
application-live.properties
application-prod.properties
当 spring.config.location 属性值为 classpath:/cfg/,classpath:/ext/ 时,我们会先处理 /cfg 下面的文件,然后在处理 /ext 下面的文件:
/cfg/application-live.properties/ext/application-prod.properties/ext/application-live.properties
当我们使用 classpath:/cfg/;classpath:/ext/ 时(带有 ; 分隔符),我们会在同一级别处理 /cfg 和 /ext,按照上面示例中spring.profiles.active=prod,live 处理:
-
/ext/application-prod.properties -
/cfg/application-live.properties -
/ext/application-live.properties
:::
Environment 有一组默认 profiles 的配置文件(默认为 [default]),如果没有设置激活的 profiles 配置。就会使用这些配置文件。换句话说,如果没有显式激活配置文件,则会考虑使用 application-default.properties 的属性。
::: tip 备注
属性文件只加载一次。如果已经直接 导入了某个特定 profile 配置文件吗,则不会再导入第二次。
:::
导入其他数据(Importing Additional Data)
应用程序可以使用spring.config.import属性从其它位置导入更多的配置数据。 导入文件配置在被应用程序检测到时就会被处理,并作为紧接在声明导入文件配置的文件下面插入导入文件数据。
例如,你的类路径下的 application.properties 可能有以下内容:
spring:
application:
name: "myapp"
config:
import: "optional:file:./dev.properties"
这将触发导入当前目录下的 dev.properties 文件(如果存在此类文件)。导入的 dev.properties 文件中属性值将由于声明导入配置的application.properties文件。在上例中,dev.properties将 spring.application.name重新定义为不同的值。
无论声明多少次,导入都只会被导入一次。在 properties/yaml 文件的单个文档中定义导入的顺序并不重要。例如,下面两个示例产生的结果是一样的:
spring:
config:
import: "my.properties"
my:
property: "value"
my:
property: "value"
spring:
config:
import: "my.properties"
在上述两个示例中,my.properties文件中的属性值将优先于声明导入配置的文件。
在单个spring.config.import属性下可以指定多个位置。位置将按照定义的顺序进行处理,后导入的位置优先。
::: tip 备注
适当的时候, 特定 Profile 配置文件 也可能会被导入。上述示例将同时导入 my.properties 和任何 my-<profile>.properties 。
:::
::: tip 备注
Spring Boot 包括可插拔的API,允许支持各种不同位置地址。默认情况下,您可以导入 Java 属性、YAML 和 “配置树”。第三方jar可以提供对其它技术的支持(不要求文件是本地的)。例如,您可以假定配置数据来自外部存储,例如 Consul、Apache ZooKeeper 或 Netflix Archaius。如果你想支持自己的位置地址,请参阅org.springframework.boot.context.config包中的 ConfigDataLocationResolver 类和 ConfigDataLoader 类。
:::
导入非扩展文件(Importing Extensionless Files)
某些云平台无法为挂载卷的文件添加文件扩展名。要导入这些无扩展名的文件,需要给 Spring Boot 一个提示,以便它指定如何加载这文件。为此,您可以将扩展名放到方括号里面。
例如,假设您有一个 /etc/config/myconfig 文件,希望以 yaml 格式导入。在application.properties的配置如下所示:
spring:
config:
import: "file:/etc/config/myconfig[.yaml]"
使用配置树(Using Configuration Trees)
在云平台(如 Kubernetes)上运行应用程序时,您经常需要读取平台提供的配置值。为此目的而使用环境变量的情况并不少见,但这样做也有缺点,尤其是当配置值需要保密时。
作为环境变量的替代方案,许多云平台现在都允许将配置映射到挂载的数据卷中。例如,Kubernetes 可以将可以将ConfigMaps 和 Secrets同时挂载到卷中。
有两种常见的挂载卷模式可供选择:
- 包含一整套属性的单个文件(通常写成 YAML)。
- 多个文件被写入一个目录树,文件名成为 “键”,内容成为 “值”。
对于第一种情况,您可以按照上文所述,使用 spring.config.import 直接导入YAML 或 Propertiesas 文件。对于第二中情况,你需要使用 configtree: 前缀,以便 Spring Boot 知道它需要将所有文件作为属性公开。
举个例子,假设 Kubernetes 挂载了以下卷:
etc/
config/
myapp/
username
password
username 文件的内容将会是个配置值, password 文件的内容将会是一个 secret。
要导入这些属性,可以在application.properties或application.yaml文件中添加以下内容:
spring:
config:
import: "optional:configtree:/etc/config/"
然后,你就可以从 Environment 中访问或注入 myapp.username 和 myapp.password 属性。
::: tip 提示
配置树下的文件夹和文件名构成了属性名称。在上面示例中,若要以 username 和 password 的形式访问属性,可将 spring.config.import 设置为 optional:configtree:/etc/config/myapp。
:::
::: tip 备注
使用点符号的文件名也能正确映射。例如,在上例中,/etc/config中名为myapp.username的文件将会映射到Environment中的myapp.username属性。
:::
::: tip 提示
配置树值可根据预期内容绑定到 String 和 byte[] 类型。
:::
如果要从同一父文件夹导入多个配置树,可以使用通配符。任何以/*/结尾的 configtree: 位置都会将所有的直接子文件夹导入成配置树。与非通配符导入一样,每个配置树下的文件夹和文件名构成了属性名。
例如,给定以下挂载卷:
etc/
config/
dbconfig/
db/
username
password
mqconfig/
mq/
username
password
您可以使用 configtree:/etc/config/*/ 作为导入位置:
spring:
config:
import: "optional:configtree:/etc/config/*/"
这将添加 db.username、db.password、mq.username 和 mq.password 属性。
::: tip 提示
使用通配符加载的目录按字母顺序排序。如果需要不同的顺序,则应将每个位置作为单独的导入列出
:::
配置树可以用于 Docker secrets。当 Docker swarm 服务允许获取某个 secret 时,这个 secret 就会被挂载到容器中。例如,如果一个名为 db.password 的secret 被挂载到 /run/secrets/ 中,你就可以使用以下方法将 db.password 提供给 Spring 环境:
spring:
config:
import: "optional:configtree:/run/secrets/"
属性占位符(Property Placeholders)
application.properties 和 application.yaml 中的值在使用时会根据现有 Environment 进行过滤,因此,您可以引用以前定义的值(例如,从系统属性或环境变量)。标准的 ${name} 属性占位符语法可以用于值的任何位置。属性占位符还可以指定默认值,使用 : 分隔默认值和属性名,例如 ${name:default}。
有默认值和无默认值占位符的使用方法见下例:
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.
::: tip 备注
您应始终使用其规范形式(kebab-case,仅使用小写字母)来引用占位符中的属性名称。这将允许Spring Boot 使用与 relaxed binding @ConfigurationProperties 时相同的逻辑。例如, ${demo.item-price} 将从application.properties文件中获取 demo.item-price 和 demo.itemPrice 形式的数据,并从环境变量中获取 DEMO_ITEMPRICE
。如果使用 ${demo.itemPrice} ,demo.item-price 和 DEMO_ITEMPRICE 将不会生效。
:::
::: tip 提示
您还可以使用此技术创建现有 Spring Boot 属性的 "简短 "变量。详见 Use ‘Short’ Command Line Arguments 。
:::
处理多文档文件(Working With Multi-Document Files)
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
::: tip 备注
配置文件分隔符不能有任何前导空白,必须有三个连字符。分隔符前后的行不能是相同的注释前缀。
:::
::: tip 提示
多文档配置文件通常与 spring.config.activate.on-profile 等激活属性结合使用。详见 下一节。
:::
::: warning 警告
使用 @PropertySource 或 @TestPropertySource 注解无法加载多文档配置文件。
:::
激活特性(Activation Properties)
有时,只有在满足特定条件时才激活特定的属性集是非常有用的。例如,可能有一些属性只有在特定的配置文件处于激活状态时才相关联。
你可以使用 spring.config.activate.* 有条件地激活配置属性文件。
以下激活配置可用:
| Property | Note |
|---|---|
on-profile |
配置文件表达式,必须匹配该表达式,文档才能处于活动状态。 |
on-cloud-platform |
文档激活时必须检测的 CloudPlatform 。 |
例如,下面示例中的第二个逻辑文档只有在 Kubernetes 上运行时,而且只有在spring.profiles.active或者spring.profiles.include为 "prod "或 "staging "时才会激活:
myprop:
"always-set"
---
spring:
config:
activate:
on-cloud-platform: "kubernetes"
on-profile: "prod | staging"
myotherprop: "sometimes-set"
7.2.4 加密属性(Encrypting Properties)
Spring Boot 不提供任何用于加密属性值的内置支持,但是,它提供了修改 Spring Environment 中包含的值所用的钩子点。EnvironmentPostProcessor 接口允许您在应用程序启动前操作 Environment 。详情参阅 Customize the Environment or ApplicationContext Before It Starts 。
如果您需要一种安全的方式来存储凭证和密码, Spring Cloud Vault x项目支持在 HashiCorp Vault 中存储外部化配置。
7.2.5 使用YAML(Working With YAML)
YAML 是JSON的超集,因此是指定分层配置数据的便捷格式。只要你的类路径上有 SnakeYAML, SpringApplication 类就会自动支持 YAML。
::: tip 备注
如果您使用“Starters”,则spring-boot-starter会自动提供SnakeYAML。
:::
将 YAML 映射到属性(Mapping YAML to Properties)
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
同样,YAML 列表也需要扁平化。YAML 列表被表示为带有[index]索引的属性键。例如,请看下面的 YAML:
my:
servers:
- "dev.example.com"
- "another.example.com"
前面的示例将转换为这些属性:
my.servers[0]=dev.example.com
my.servers[1]=another.example.com
::: tip 提示
使用 [index] 符号的属性可以通过 Spring Boot 的Binder类绑定到 Java List 或 Set 对象。有关详情请参阅 “Type-safe Configuration Properties” 部分。
:::
::: warning 警告
使用 @PropertySource 或 @TestPropertySource 注解无法加载 YAML 文件。因此,如果需要以这种方式加载值,则需要使用 properties 文件。
:::
直接加载 YAML(Directly Loading YAML)
Spring Framework 提供了两个方便的类,可用于加载 YAML 文档。YamlPropertiesFactoryBean 以 Properties 的形式加载 YAML,而 YamlMapFactoryBean 则以 Map 的形式加载YAML。
如果想以Spring PropertySource 的形式加载 YAML,还可以使用 YamlPropertySourceLoader 类。
7.2.6 配置随机值(Configuring Random Values)
RandomValuePropertySource 对于注入随机值(例如,secrets或测试用例)非常有用。它可以生成 integers、 longs、uuid或 string, 如以下示例所示:
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]}"
# number-in-range: "${random.int(1024,65536)}"
# number-in-range: "${random.int-1024,65536-}"
random.int* 的语法为 OPEN value (,max) CLOSE ,其中 OPEN,CLOSE 为任意字符,value,max 为整数。如果只提供 max,那么 value 是最小值,max 是最大值(不包括)。
7.2.7 配置系统环境属性(Configuring System Environment Properties)
Spring Boot 支持为环境属性设置前缀。如果具有不同配置要求的多个 Spring Boot 应用程序共享系统环境,这将非常有用。系统环境属性的前缀可以直接在 SpringApplication 类setEnvironmentPrefix方法1上设置。
例如,如果将前缀设置为 input,那么 remote.timeout 等属性也将会在系统环境中解析为 input.remote.timeout 。
7.2.8 类型安全配置属性(Type-safe Configuration Properties)
使用 @Value("${property}") 注解来注入配置属性有时候会很麻烦,尤其是在处理多个属性或数据具有层次性的情况下。Spring Boot 提供了另外一种处理属性的方法,该方法允许强类型beans管理和验证应用程序的配置。
::: tip 提示
:::
JavaBean 属性绑定(JavaBean Properties Binding)
可以绑定声明了标准 JavaBean 属性的 Bean,如下例所示:
@ConfigurationProperties("my.service")
public class MyProperties {
private boolean enabled;
private InetAddress remoteAddress;
private final Security security = new Security();
// getters / setters...
public static class Security {
private String username;
private String password;
private List<String> roles = new ArrayList<>(Collections.singleton("USER"));
// getters / setters...
}
}
前面的 POJO 定义了以下属性:
my.service.enabled,默认值为false。my.service.remote-address,其类型可以从String类型进行强制转换。my.service.security.username,带有嵌套的“Security”对象,其名称由对象属性名决定。特别要指出的是,这里根本没有使用返回类型,可能是SecurityProperties。my.service.security.password。my.service.security.roles,默认值为USER的String集合。
::: tip 备注
映射到 Spring Boot 中可用的 @ConfigurationProperties 类的属性(通过properties 文件、YAML文件、环境变量和其它机制进行配置)是公共API,但类本身的访问器(getters/setters)不能直接使用。
:::
::: tip 备注
这种安排依赖于默认的空构造函数,getter 和 setter 通常是强制性的,因为绑定是通过标准 Java Beans 属性描述符进行的,就像在 Spring MVC 中一样。在以下情况下,可以省略setter:
- Maps,只要它们被初始化,就需要一个 getter 但是不一定需要一个 setter,因为他们可以被 binder 更改。
- 可以通过索引(YAML文件)或使用单个逗号分隔值(properties文件)访问集合和数组。在后一种情况下,必须使用setter。我们建议始终为此类型添加setter。如果初始化集合,请确保它不是不可变的(如上例所示)。
- 如果嵌套的 POJO 属性被初始化(如上例中的
Security字段),则不需要 setter。如果你希望 binder 通过使用器默认构造函数动态创建实例,则需要一个 setter。
有些人使用项目插件 Lombok 自动添加 getters 和 setters。请确保 Lombok 不会为该类型生成任何特定的构造函数,因为容器会自动退使用它来实例化对象。
最后,只考虑标准的Java Bean属性,不支持绑定静态属性。
:::
构造函数绑定(Constructor Binding)
上一节的示例可以用不可变的方式重写,如下例所示:
@ConstructorBinding
@ConfigurationProperties("my.service")
public class MyProperties {
// fields...
public MyProperties(boolean enabled

最低0.47元/天 解锁文章
4万+

被折叠的 条评论
为什么被折叠?



