细粒度地调整自动配置

在深入了解配置属性之前,我们需要知道,在Spring中有两种不同(但相关)的配置:

  • bean装配,即声明在Spring应用上下文中创建哪些应用组件(即bean)以及它们之间如何互相注入的配置;

  • 属性注入,即设置Spring应用上下文中bean的值的配置。

在Spring的XML方式和基于Java的配置中,这两种类型的配置通常会在同一个地方显式声明。在基于Java的配置中,带有@Bean注解的方法一般会同时初始化bean并立即为它的属性设置值。例如,请查看下面这个带有@Bean注解的方法,它会为嵌入式的H2数据库声明一个DataSource:

@Bean
public DataSource dataSource() {
  return new EmbeddedDatabaseBuilder()
      .setType(H2)
      .addScript("taco_schema.sql")
      .addScripts("user_data.sql", "ingredient_data.sql")
      .build();
}

在这里,addScript() 和 addScripts() 方法设置了一些String类型的属性,它们是在数据源就绪之后要用到数据库上的SQL脚本。这就是不使用Spring Boot配置DataSource bean的方法,但是借助自动配置的功能,这种方法就完全没有必要使用了。

如果能够在运行时类路径中找到H2依赖,那么Spring Boot会自动在Spring应用上下文中创建对应的DataSource bean,这个bean会运行名为schema.sql和data.sql脚本。

但是,如果我们想要给SQL脚本使用其他的名称,该怎么办呢?如果我们想要指定两个以上的SQL脚本,又该怎么办呢?这就是配置属性能够发挥作用的地方了。但是,在开始使用配置属性之前,我们需要理解这些属性是从哪里来的。

理解Spring的环境抽象

Spring的环境抽象为各种配置属性提供了一站式服务。它会抽取原始的属性,这样需要这些属性的bean就可以从Spring本身中获取了。Spring环境会拉取多个属性源,包括:

  • JVM系统属性;

  • 操作系统环境变量;

  • 命令行参数;

  • 应用属性配置文件。

它会将这些属性聚合到一个源中,这个源可以注入到Spring的bean中。图6.1阐述了来自各个属性源的属性是如何流经Spring的环境抽象进入Spring bean的。

image 2024 03 13 17 19 47 811
Figure 1. 图6.1 Spring环境从各个属性源拉取属性,并允许Spring应用上下文中的bean使用它们

Spring Boot自动配置的bean都可以通过Spring环境提取的属性进行配置。举个简单的例子,假设我们希望应用底层的Servlet容器使用另外一个端口监听请求,而不再使用8080,则可以在“src/main/resources/application.properties”中将server.port设置成一个不同的端口,如下所示:

server.port = 9090

在设置属性的时候,我个人更喜欢使用 YAML。所以,我通常不会使用 application.properties,而是在 “src/main/resources/application.yml” 中设置 server.port 的值,如下所示:

server:
  port: 9090

如果你喜欢在外部配置该属性,可以在使用命令行参数启动应用的时候指定端口:

$ java -jar tacocloud-0.0.5-SNAPSHOT.jar --server.port = 9090

如果你希望应用始终在一个特定的端口启动,可以通过操作系统的环境变量进行一次性的设置:

$ export SERVER_PORT = 9090

需要注意,在将属性设置为环境变量的时候,命名风格略有不同,这样做是为了适应操作系统对环境变量名称的限制。不过,没有关系,Spring 能够将其挑选出来,并将 SERVER_PORT 解析为 server.port。

正如我前面所说,有多种配置属性的方法。实际上,可以使用几百个配置属性来调整 Spring bean 的行为。我们已经看到了其中的一部分,比如本章中介绍的 server.port 和前文提到的 spring.datasource.name 和 spring.thymeleaf.cache。

本章不可能介绍所有可用的配置属性。尽管如此,我们还是可以了解一些可能会经常遇到的最有用的配置属性。首先看一下能够调整自动配置的数据源的一些属性。

配置数据源

此时,Taco Cloud 应用尚未完成,在该应用准备部署之前,我们还会用几个章节的篇幅来完善它。因此,使用嵌入式的 H2 数据库作为数据源非常适合我们的需求,至少就目前来看是这样的。但是,一旦要将应用部署到生产环境中,就可能需要考虑使用更加持久的数据库解决方案。

尽管可以显式地配置自己的DataSource,但通常没有必要这样做。相反,通过配置属性为数据库设置URL和凭证信息会更加简单。例如,如果想要使用MySQL数据库,可以添加如下的配置属性到application.yml中:

spring:
  datasource:
    url: jdbc:mysql://localhost/tacocloud
    username: tacouser
    password: tacopassword

我们尽管需要将对应的JDBC驱动添加到构建文件中,但是不需要指定JDBC驱动类,Spring Boot会根据数据库URL的结构推算出来。然而,如果这样做有问题,我们依然可以通过spring.datasource.driver-class-name属性来进行设置:

spring:
  datasource:
    url: jdbc:mysql://localhost/tacocloud
    username: tacouser
    password: tacopassword
    driver-class-name: com.mysql.jdbc.Driver

Spring Boot 在自动化配置 DataSource bean 时,将会使用该连接。如果在类路径中存在 HikariCP 连接池,DataSource 将使用该连接池。否则,Spring Boot 会在类路径下尝试查找并使用如下的连接池实现:

  • Tomcat JDBC 连接池;

  • Apache Commons DBCP2。

自动配置所能支持的连接池可选方案仅有这些,但是随时欢迎显式配置DataSource bean,这样一来,就可以使用任意我们喜欢的连接池实现。

在前文,我建议要有一种方式声明应用启动的时候要执行的数据库初始化脚本。在这种情况下,spring.datasource.schema和spring.datasource.data属性就非常有用了:

spring:
  datasource:
    schema:
    - order-schema.sql
    - ingredient-schema.sql
    - taco-schema.sql
    - user-schema.sql
    data:
    - ingredients.sql

有的读者可能无法使用显式配置数据源的方式,而是更加倾向于在 JNDI(代表Java Naming and Directory Interface)中配置数据源并让 Spring 去那里进行查找。在这种情况下,可以使用 spring.datasource.jndi-name 搭建自己的数据源:

spring:
  datasource:
    jndi-name: java:/comp/env/jdbc/tacoCloudDS

如果设置了 spring.datasource.jndi-name 属性,其他的数据库连接属性(如果已经设置了)就会被忽略掉。

配置嵌入式服务器

我们已经见过如何使用server.port属性来配置Servlet容器的端口。但是,我还没有展示将server.port设置为0将会出现什么状况:

server:
  port: 0

尽管 server.port 属性显式设置成了 0,但是服务器并不会真的在端口 0 上启动。相反,它会任选一个可用的端口。在运行自动化集成测试时,这会非常有用,因为这样能够保证并发运行的测试不会与硬编码的端口号冲突。

但是,底层服务器的配置并不仅仅局限于端口,对底层容器常见的一项设置就是让它处理 HTTPS 请求。为了实现这一点,首先要使用JDK的keytool命令行工具生成keystore:

$ keytool -keystore mykeys.jks -genkey -alias tomcat -keyalg RSA

在这个过程中,程序会询问我们一些关于名称和组织机构相关的问题,大多数问题都无关紧要。但是,当提示输入密码的时候,需要我们记住所输入的密码。本例使用 letmein 作为密码。

接下来,我们需要设置一些属性,以便在嵌入式服务器中启用 HTTPS。我们可以在命令行中进行配置,但是这种方式非常不方便,相反,你可能更愿意通过 application. properties 或 application.yml 文件来声明配置。在 application.yml 中,配置属性如下所示:

server:
  port: 8443
  ssl:
    key-store: file:///path/to/mykeys.jks
    key-store-password: letmein
    key-password: letmein

在这里,我们将server.port设置为8443,在开发阶段,这是HTTPS服务器的常用选择。server.ssl.key-store属性应该设置为我们所创建的keystore文件的路径。在这里,它使用了file://URL,因此会在文件系统中加载,但是,如果需要将它打包到一个应用JAR文件中,就需要使用“classpath:”URL来引用它。server.ssl.key-store-password和server.ssl.key-password属性都设置成了创建keystore时所设置的密码。

这些属性准备就绪之后,应用就会监听8443端口上的HTTPS请求。因为浏览器之间有所差异,所以我们可能会遇到服务器无法验证其身份的警告。在开发阶段,通过localhost提供服务时,这其实无须担心。

配置日志

大多数的应用都会提供某种形式的日志。即便应用本身不直接打印任何日志,应用所使用的库也会以日志的形式记录它们的活动。

默认情况下,Spring Boot 通过 Logback 配置日志,日志会以 INFO 级别写入控制台。在运行应用或其他样例的时候,你可能已经在应用日志中发现了大量的INFO级别的条目。作为提醒,如下的日志样例展示了默认的日志格式(为了适应页面宽度进行了折行)

2021-07-29 17:24:24.187 INFO 52240 --- [nio-8080-exec-1]
➥com.example.demo.Hello                 Here's a log entry.
2021-07-29 17:24:24.187 INFO 52240 --- [nio-8080-exec-1]
➥com.example.demo.Hello                 Here's another log entry.
2021-07-29 17:24:24.187 INFO 52240 --- [nio-8080-exec-1]
➥com.example.demo.Hello                 And here's one more.

为了完全控制日志的配置,可以在类路径的根目录下(在 “src/main/resources” 中)创建一个 logback.xml 文件。如下是一个简单 logback.xml 文件的样例:

<configuration>
  <appender name = "STDOUT" class = "ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>
        %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
      </pattern>
    </encoder>
  </appender>
  <logger name="root" level="INFO"/>
  <root level="INFO">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

借助这个新的配置,与前面相同的样例日志条目将会如下所示(为了适应页面宽度进行了折行):

17:25:09.088 [http-nio-8080-exec-1] INFO com.example.demo.Hello -
                                         Here's a log entry.
17:25:09.088 [http-nio-8080-exec-1] INFO com.example.demo.Hello -
                                         Here's another log entry.
17:25:09.088 [http-nio-8080-exec-1] INFO com.example.demo.Hello -
                                         And here's one more.

除了日志所使用的模式之外,这个 Logback配置和没有logback.xml文件时的默认行为几乎是相同的。但是,通过编辑logback.xml文件,可以完全控制应用的日志文件。

注意:logback.xml文件中都可以声明哪些内容超出了本书的范围。可以参考Logback的文档来了解更多信息。

在日志配置方面,你可能遇到的常见变更就是修改日志级别和指定日志写入到哪个文件中。借助Spring Boot的配置属性功能,不用创建logback.xml文件就能完成这些变更。

要设置日志级别,我们可以创建这样的属性:以logging.level为前缀,随后紧跟着想要设置日志级别的logger。假设我们想要将root logging设置为WARN级别,但是希望将Spring Security的日志级别设置为DEBUG。那么,在application.yml添加如下的条目就能实现我们的要求:

logging:
  level:
    root: WARN
    org:
      springframework:
        security: DEBUG

我们还可以将 Spring Security 的包名扁平化到一行中,使其更易于阅读:

logging:
  level:
    root: WARN
    org.springframework.security: DEBUG

现在,假设我们想要将日志条目写入 “/var/logs/” 中的 TacoCloud.log 文件。logging.path 和 logging.file 文件可以按照如下形式进行设置:

logging:
  file:
    path: /var/logs/
    file: TacoCloud.log
  level:
    root: WARN
    org:
      springframework:
        security: DEBUG

如果应用具有“/var/logs/”目录的写入权限,那么日志条目会写入“/var/logs/TacoCloud.log”文件。默认情况下,日志文件一旦达到10 MB就会轮换。

使用特定的属性值

在设置属性的时候,并非必须要将它们的值设置为硬编码的String或数值。其实,我们还可以从其他的配置属性派生值。

例如,假设(不管基于什么原因)我们想要设置一个名为greeting.welcome的属性,它的值来源于名为spring.application.name的另一个属性。为了实现该功能,在设置greeting.welcome的时候,我们可以使用 ${} 占位符标记:

greeting:
  welcome: ${spring.application.name}

我们甚至可以将占位符嵌入其他文本:

greeting:
  welcome: You are using ${spring.application.name}.

可以看到,在配置Spring自己的组件时,使用配置属性可以很容易地将值注入这些组件的属性,并且可以细粒度调整自动配置功能。配置属性并不专属于Spring创建的bean。我们稍微下点功夫就可以在自己的bean中使用配置属性功能。接下来,让我们看一下如何实现。