5.1 细粒度的自动配置

5.1 细粒度的自动配置

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

  • bean装配:声明在Spring应用上下文中创建哪些应用组件以及它们之间如何互相注入的配置。
  • 属性注入:设置Spring应用上下文中bean的值的配置。

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

1
2
3
4
5
6
7
8
@Bean
public DataSource dataSource() {
return new EmbeddedDataSourceBuilder()
.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脚本又该怎么办呢?这就是配置属性能够发挥作用的地方了。但是,在开始使用配置属性之前,我们需要理解这些属性是从哪里来的。

5.1.1 理解Spring的环境抽象

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

  • JVM系统属性;
  • 操作系统环境变量;
  • 命令行参数;
  • 应用属性配置文件。

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

image-20211014081714790

图5.1 Spring环境从各个属性源拉取属性,并让Spring应用上下文中的bean可以使用它们

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

1
server.port=9090

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

1
2
server:
port: 9090

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

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

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

1
export SERVER_PORT=9090

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

正如我前面所说,有多种配置属性的方法。当我们学习第14章的时候,你会看到另外一种设置属性的方法,那就是通过中心化的配置服务器实现。实际上,我们可以使用几百个配置属性来调整Spring bean的行为。你已经看到了其中的一部分,比如本章中已经介绍的server.port。

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

5.1.2 配置数据源

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

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

1
2
3
4
5
spring:
datasource:
url: jdbc:mysql://localhost/tacocloud
username: tacodb
password: tacopassword

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

1
2
3
4
5
6
spring:
datasource:
url: jdbc:mysql://localhost/tacocloud
username: tacodb
password: tacopassword
driver-class-name: com.mysql.jdbc.Driver

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

  • HikariCP
  • Commons DBCP 2

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

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

1
2
3
4
5
6
7
8
9
spring:
datasource:
schema:
- order-schema.sql
- ingredient-schema.sql
- taco-schema.sql
- user-schema.sql
data:
- ingredients.sql

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

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

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

5.1.3 配置嵌入式服务器

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

1
2
server:
port: 0

尽管我们将server.port属性显式设置成了0,但是服务器并不会真的在端口0上启动。相反,它会任选一个可用的端口。在我们运行自动化集成测试的时候,这会非常有用,因为这样能够保证并发运行的测试不会与硬编码的端口号冲突。在第13章中我们将会看到,如果不关心应用在哪个端口启动,那么这种配置方式也非常有用,因为此时应用将会变成通过服务注册中心来进行查找的微服务。

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

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

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

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

1
2
3
4
5
6
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提供服务时,这其实无须担心。

5.1.4 配置日志

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

默认情况下,Spring Boot通过Logback配置日志,日志会以INFO级别写入到控制台中。在运行应用或其他样例的时候,你可能已经在应用日志中发现了大量的INFO级别的条目。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
<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>

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

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

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

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

1
2
3
4
5
6
logging:
level:
root: WARN
org:
springframework:
security: DEBUG

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

1
2
3
4
logging:
level:
root: WARN
org.springframework.security: DEBUG

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

1
2
3
4
5
6
7
8
logging:
path: /var/logs/
file: TacoCloud.log
level:
root: WARN
org:
springframework:
security: DEBUG

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

5.1.5 使用特定的属性值

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

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

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

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

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

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