学习 Spring 实战第5版:使用配置属性

The best way to not feel hopeless is to get up and do something. Don’t wait for good things to happen to you. If you go out and make some good things happen, you will fill the world with hope, you will fill yourself with hope.
Barack Obama

这节课,我将使用IDEA来进行spring 的学习。

细粒度的自动配置

手动装配

在过去,我们使用@bean注解,在基于XML方式和基于Java的配置中,这两种类型的配置通常会在同一个地方显式声明。

基于Java的配置中,带有@Bean注解的方法会在初始化bean的同时,立即为它的属性设置值。

1
2
3
4
5
6
7
8
9
@Bean
public DataSource dataSource(){
return new EmbeddedDataSourceBuilder()
.setType(H2)
.addScript("taco_schema.sql")
.addScripts("user_data.sql","ingredient_data.sql")
.build();
}

这就是不使用spring boot时,我们配置DataSource的方法。不过现在,使用spring Boot的情况下,借助自动配置的功能,已经完全没有必要使用这种方法了

Spring的环境抽象

Spring会拉取多个属性源:

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

Spring环境会将这些属性聚合到一个源中,之后再将这个源注入到Spring应用上下文中的bean中。

所以下面几种属性配置方法是等价的:在配置文件src/main/resources/application.properties中,将server.port设置成一个不同的端口

1
server.port=9090

yaml文件的等价形式

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。

实际上,也可能解析为server-port,serverPort。这依靠Spring对上下文的推导能力。

基于上下文的推导

Spring基于上下文的推导能力十分强大,比如下文:

1
2
3
4
5
6
# 配置数据库用户名
# 配置数据库连接地址
url: jdbc:mysql://localhost/tacocloud?serverTimezone=Asia/Shanghai
username: root
# 配置数据库密码
password: 2218953481

它省略了对驱动类的配置,而Spring绝大多数情况下都可以自动推导得到正确的结果。不过你也可以显式的声明

1
2
3
4
5
6
7
8
# 配置JDBC Driver
driver-class-name: com.mysql.cj.jdbc.Driver
# 配置数据库用户名
# 配置数据库连接地址
url: jdbc:mysql://localhost/tacocloud?serverTimezone=Asia/Shanghai
username: root
# 配置数据库密码
password: 2218953481

JNDI

我们可以使用JNDI来搭建自己的数据源,这种情况下需要这样配置。

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

我们设置了这条属性后,其他的数据库连接属性无论设置与否,都会被忽略掉。

配置嵌入式服务器

我们可以将server.port设置为0

1
2
server:
port: 0

这种情况下,服务器启动时,会任选一个可用的端口。在我们运行自动化集成测试的时候,这非常有用,这样能够保证并发运行的测试不会与硬编码的端口号冲突。

底层服务器的配置并不仅限于一个端口,我们对底层容器常见的一项就是让它处理HTTPS请求。

为了实现这点,我们首先要使用JDK的keytool命令行工具生成keystore:

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

然后,我们可以使用前文提到的任何一种等价方式来配置属性,这里使用yaml作为介绍

1
2
3
4
5
6
server:
port: 8443
ssl:
key-store: file://path/to/mykey.jks
key-store-password: letmein
key-password: letmein

这里的8443端口是开发https服务器的常用端口

server.ssl.key-store要设置为我们所创建的keystore文件路径。如果你要将它打包到一个应用JAR文件中,就需要使用“classpath:”URL来引用它。

这样设置完成后,应用就会监听8443端口上的HTTPS请求。在开发过程中,浏览器可能会提示服务器无法验证其身份,这是正常的.

配置日志

默认实现中,Spring Boot会通过Logback配置日志。日志默认显示为INFO级别,它们都会写入到控制台中。

我们也可以手动编写配置文件,达到完全控制日志的要求。

在src/main/resources目录下新建一个logback.xml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<configuration>
<appender name="STDPIT" 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>

这里相比于默认设置,只改变了日志所使用的模式。

有了Spring Boot 后,我们可以使用等价的另外一种配置形式

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

此外,我们还可以将配置文件扁平化:

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

假设,我们要更改日志条目写入的位置

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

默认情况下,日志文件一旦达到10MB就会轮换

属性派生值

有生活我们需要将一个配置的值设置为引用其他配置的值,这种情况称为从其他属性派生值.我们使用${}占位符来标记

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

联系前文,在配置Spring的上下文组件时,就可以将这些配置属性注入到bean组件的属性值中.

创建自定义配置属性

使用@ConfigurationProperties注解,将它放在Spring bean上之后,Spring Boot就会为这些bean赋值。

当然,联系前文,这些值都是从Spring环境中取出的(称为Spring环境注入值),在将这些值取出后,Spring Boot将其赋值给bean中的属性。

查看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
@GetMapping
public String orderForUser(
@AuthenticationPrincipal User user, Model model) {

// 限制页面显示的订单数量为最近的20个
Pageable pageable= PageRequest.of(0,20);
model.addAttribute("orders", orderRepo.findByUserOrderByPlacedAtDesc(user,pageable));

return "orderList";

}

在这里我们硬编码了显示分页的第一页(序号0),并且限制每页数量为20.

但有时候,由于业务需要,将来可能要改变这个数值,这时,我们最好使用自定义配置的形式。

我们需要新增一个属性pageSize添加到OrderController中,并为这个类添加@ConfigurationProperties注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.zyj.tacocloud.web;

import com.zyj.tacocloud.Order;
import com.zyj.tacocloud.User;
import com.zyj.tacocloud.data.OrderRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;

import javax.validation.Valid;

/**
* @author ZYJ
*/
@Slf4j
@Controller
@RequestMapping("/orders")
@SessionAttributes("order")
@ConfigurationProperties(prefix = "taco.orders")
public class OrderController {

private OrderRepository orderRepo;

/**
* 默认值为20,前缀prefix="taco.orders"
* 所以使用taco.orders.pageSize来设置这项属性
*/
private int pageSize = 20;

public OrderController(OrderRepository orderRepo) {
this.orderRepo = orderRepo;
}

public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}

@GetMapping
public String orderForUser(
@AuthenticationPrincipal User user, Model model) {

// 限制页面显示的订单数量为最近的pageSize个
Pageable pageable = PageRequest.of(0, pageSize);
model.addAttribute("orders", orderRepo.findByUserOrderByPlacedAtDesc(user, pageable));

return "orderList";

}
}

这里使用的@ConfigurationProperties(prefix = "taco.orders")指定了这个属性可以从Spring环境池中获取,并且指定了前缀为"taco.orders".于是属性全称为taco.orders.pageSize

之后我们可以在application.yml中设置该属性,其他的等价设置都可以

1
2
3
taco:  
orders:
pageSize: 10

这里的设置并不完全,我们还要为这个源属性提供文档,称为配置属性元数据

我们应当为这些属性增加对应元数据,以方便后来人在IDE中阅读,了解这个属性有什么作用.

定义配置属性的持有者 ConfigurationPropertiesProps

@ConfigurationProperties注解实际上通常会放到一种特定类型的bean中,这种bean就是专门用来持有配置数据.

于是我们最好将上面例子中的pageSizeOrderController中抽取出来,放到单独的类OrderProps类中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.zyj.tacocloud.web;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
* 这里使用了@Component注解,Spring上下文的组件扫描会自动发现它,并将其创建为Spring 应用上下文的bean
* @author ZYJ
*/
@Slf4j
@Component
@Data
@ConfigurationProperties(prefix = "taco.orders")
public class OrderProps {

/**
* 默认值为20,前缀prefix="taco.orders"
* 所以使用taco.orders.pageSize来设置这项属性
*/
private int pageSize = 20;

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* @author ZYJ
*/
@Slf4j
@Controller
@RequestMapping("/orders")
@SessionAttributes("order")
public class OrderController {

private OrderRepository orderRepo;

private OrderProps props;

public OrderController(OrderRepository orderRepo, OrderProps props) {
this.orderRepo = orderRepo;
this.props = props;
}

@GetMapping
public String orderForUser(
@AuthenticationPrincipal User user, Model model) {

// 限制页面显示的订单数量为最近的pageSize个
Pageable pageable = PageRequest.of(0, props.getPageSize());
model.addAttribute("orders", orderRepo.findByUserOrderByPlacedAtDesc(user, pageable));

return "orderList";

}

声明配置属性元数据

你会发现application.yml文件的taco.orders.pageSize条目中,会出现警告消息,类似Unknown property 'taco',这是因为这条属性缺失注释(称之为属性元数据)

属性元数据是可选的,但我们为了后人的理解方便,还是添加上属性元数据为好.

src/main/resources/META-INF目录下添加一个文件additional-spring-configuration-metadata.json内容为:

1
2
3
4
5
6
7
8
9
{
"properties": [
{
"name": "taco.orders.page-size",
"type": "java.lang.String",
"description": "设置最大订单显示数目,显示在一个列表中."
}
]
}

这样就完成了对taco.orders.pageSize这条属性的元数据配置
Pasted image 20210204153025.png

接口的实例化实现

1
2
3
4
5
6
7
8
9
10
11
@GetMapping
public String orderForUser(
@AuthenticationPrincipal User user, Model model) {

// 限制页面显示的订单数量为最近的20个
Pageable pageable= PageRequest.of(0,20);
model.addAttribute("orders", orderRepo.findByUserOrderByPlacedAtDesc(user,pageable));

return "orderList";

}

这里使用接口Pageable作为下文的参数,并为其赋值为一个实现。这是一种很常用也很好的编写代码方式。

即使用接口作为函数参数,使用接口的某个实例化实现作为新建对象。这样未来方便我们重构或是对Pageable做出一个新的实现。

使用profile进行配置

课后作业

模仿前面要求,实际动手来完成这节课的教学任务

见:spring-in-action-5-spring起步-课后作业