5.2 创建自己的配置属性

5.2 创建自己的配置属性

正如我在前文所述,配置属性只不过是bean的属性,它们可以从Spring的环境抽象中接受配置。我还没有提及的是这些bean该如何消费这些配置。

为了支持配置属性的注入,Spring Boot提供了@ConfigurationProperties注解。将它放到Spring bean上之后,它就会为该bean中那些能够根据Spring环境注入值的属性赋值。

为了阐述@ConfigurationProperties是如何运行的,假设我们为OrderController添加了如下的方法,该方法会列出当前认证用户过去的订单:

1
2
3
4
5
6
7
@GetMapping
public String ordersForUser(
@AuthenticationPrincipal User user, Model model) {
model.addAttribute("orders",
orderRepo.findByUserOrderByPlacedAtDesc(user));
return "orderList";
}

除此之外,我们还要为OrderRepository添加必要的findByUser()方法:

1
List<Order> findByUserOrderByPlacedAtDesc(User user);

注意,这个repository方法使用了OrderByPlacedAtDesc子句。OrderBy区域指定了结果要按照什么属性来排序,在本例中,也就是placedAt属性。最后的Desc声明要按照降序进行排列。所以,返回的订单将会按照时间由近及远进行排序。

按照这种写法,如果用户只创建了少量订单,那么这个控制器方法可能会非常有用,但是,对于狂热的taco爱好者来说,这种方式就显得有些不方便了。在浏览器中显示一些订单会很有用,但是一长串没完没了的订单列表简直就是“噪声”。假设,我们希望将显示的订单数量限制为最近的20个,那么我们可以按照如下方式来修改ordersForUser():

1
2
3
4
5
6
7
8
@GetMapping
public String ordersForUser(
@AuthenticationPrincipal User user, Model model) {
Pageable pageable = PageRequest.of(0, 20);
model.addAttribute("orders",
orderRepo.findByUserOrderByPlacedAtDesc(user, pageable));
return "orderList";
}

OrderRepository也需要对应修改:

1
2
List<Order> findByUserOrderByPlacedAtDesc(
User user, Pageable pageable);

现在,我们修改了findByUserOrderByPlacedAtDesc()方法的签名,使其能够接受Pageable参数。Pageable是Spring Data根据页号和每页数量选取结果的子集的一种方法。在ordersForUser()控制器方法中,我们构建了一个PageRequest对象,该对象实现了Pageable,我们将其声明为请求第一页(序号为0)的数据,并且每页数量为20,这样我们就能获取当前用户最近的20个订单。

尽管这种方式能够很好地运行,但是我们在这里硬编码了每页的数量,这有点让人担心。如果我们以后发现展示20个订单太多,并决定将其修改为10个,那该怎么办?因为这个值是硬编码的,所以需要重新构建和重新部署应用。

我们可以将每页数量设置成一个自定义的配置属性,而不是硬编码到代码中。首先,我们需要添加一个名为pageSize的新属性到OrderController中,并为OrderController添加@ConfigurationProperties注解,如程序清单5.1所示。

程序清单5.1 在OrderController中启用配置属性功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Controller
@RequestMapping("/orders")
@SessionAttributes("order")
@ConfigurationProperties(prefix="taco.orders")
public class OrderController {
private int pageSize = 20;
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
...
@GetMapping
public String ordersForUser(
@AuthenticationPrincipal User user, Model model) {
Pageable pageable = PageRequest.of(0, pageSize);
model.addAttribute("orders",
orderRepo.findByUserOrderByPlacedAtDesc(user, pageable));
return "orderList";
}
}

程序清单5.1最重要的变更是添加了@ConfigurationProperties注解。它的prefix属性设置成了taco.orders,这意味着当设置pageSize的时候,我们需要使用名为taco.orders.pageSize的配置属性。

新的pageSize值默认为20,但是通过设置taco.orders.pageSize属性,我们可以很容易地将其修改为任意的值。例如,我们可以在application.yml中按照如下的方式设置该属性:

1
2
3
taco:
orders:
pageSize: 10

如果在生产环境中需要快速更改,我们可以将taco.orders.pageSize设置为环境变量,这样就不用重新构建和重新部署应用了:

1
$ export TACO_ORDERS_PAGESIZE=10

设置配置属性的任何方式都可以用来调整最近订单页面中每页的数量。接下来,我们看一下如何在属性持有者(property holder)中设置配置数据。

5.2.1 定义配置属性的持有者

这里并没有说@ConfigurationProperties只能用到控制器或特定类型的bean中。@ConfigurationProperties实际上通常会放到一种特定类型的bean中,这种bean的目的就是持有配置数据。这样的话,特定的配置细节就能从控制器和其他应用程序类中抽离出来,多个bean也能更容易地共享一些通用的配置。

针对OrderController中的pageSize属性,我们可以将其抽取到一个单独的类中。程序清单5.2就以这样的方式来使用OrderProps类。

程序清单5.2 将pageSize抽取到持有者类中

1
2
3
4
5
6
7
8
9
10
11
package tacos.web;
import org.springframework.boot.context.properties.
ConfigurationProperties;
import org.springframework.stereotype.Component;
import lombok.Data;
@Component
@ConfigurationProperties(prefix="taco.orders")
@Data
public class OrderProps {
private int pageSize = 20;
}

就像我们在OrderController中所做的那样,pageSize的默认值为20,OrderProps使用了@ConfigurationProperties注解并且将前缀设置成了taco.orders。这个类还用到了@Component注解,这样Spring的组件扫描功能会自动发现它并将其创建为Spring应用上下文中的bean。这是非常重要的,因为我们下一步要将OrderProps作为bean注入到OrderController中。

配置属性持有者并没有什么特别之处。它们只是将Spring环境注入到其属性中的bean。它们可以注入到任意需要这些属性的其他bean中。对于OrderController来说,我们就可以从OrderController中移除pageSize,并注入和使用OrderProps bean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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 ordersForUser(
@AuthenticationPrincipal User user, Model model) {
Pageable pageable = PageRequest.of(0, props.getPageSize());
model.addAttribute("orders",
orderRepo.findByUserOrderByPlacedAtDesc(user, pageable));
return "orderList";
}
...
}

现在,OrderController不需要负责处理自己的配置属性了。这样能够让OrderController中的代码更加整洁一些,并且能够让其他的bean重用OrderProps中的属性。除此之外,我们可以将订单相关的属性全部放到一个地方,也就是OrderProps类中。如果我们需要添加、删除、重命名或者以其他方式更改其中的属性,我们只需要在OrderProps中进行变更就可以了。

例如,假设我们在多个其他的bean中也用到了pageSize属性,现在我们决定要对这个属性的值进行一些校验,限制它的值必须要不小于5且不大于25。如果没有持有者bean,我们必须要将校验注解用到OrderController的pageSize属性上以及其他所有使用该属性的类上。但是,因为我们现在将pageSize抽取到了OrderProps中,所以只需要修改OrderProps就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package tacos.web;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import org.springframework.boot.context.properties.
ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import lombok.Data;
@Component
@ConfigurationProperties(prefix="taco.orders")
@Data
@Validated
public class OrderProps {
@Min(value=5, message="must be between 5 and 25")
@Max(value=25, message="must be between 5 and 25")
private int pageSize = 20;
}
//end::validated[]

尽管我们很容易就可以将@Validated@Min@Max注解用到OrderController(和其他可以注入OrderProps的地方),但是这样会使OrderController更加混乱。通过配置属性的持有者bean,我们将所有的配置属性收集到了一个地方,这样就能让使用这些属性的bean尽可能保持整洁。

5.2.2 声明配置属性元数据

在IDE中,你可能会发现application.yml(或application.properties)文件的taco.orders. pageSize条目上会有一条警告信息,根据IDE不同显示会有所差异,这个警告提示的内容可能是“Unknown property ‘taco’”。这个警告产生的原因在于我们刚刚创建的配置属性缺少元数据。图5.2展示了在Spring Tool Suite中,当我将鼠标悬停到taco属性时的样式。

image-20211014102516936

图5.2 缺少配置属性元数据所产生的警告

配置属性的元数据完全是可选的,它并不会妨碍配置属性的运行。但是,元数据对于为配置属性提供一个最小化的文档非常有用,在IDE中尤为如此。

举例来说,将鼠标指针悬停到security.user.password属性上时,就会看到图5.3那样的效果。尽管悬停对我们的帮助很有限,但是它足以让我们知道这个属性是做什么的以及如何使用它。

image-20211014102550678

图5.3 Spring Tool Suite中配置属性的悬停文档

为了帮助那些使用你所定义的配置属性的人(有可能就是你本人),为这些属性创建一些元数据是非常好的办法,至少它能消除IDE上那些烦人的黄色警告。

为了创建自定义配置属性的元数据,我们需要在META-INF下创建一个名为additional-spring-configuration-metadata.json的文件(比如,在项目的“src/main/resources/ META-INF”目录下)。

快速添加缺失的元数据

如果使用Spring Tool Suite,就会有一个创建缺失属性元数据的快速修正选项。将鼠标放到缺失元数据警告的那行代码上,在Mac下按CMD+1组合键或者在Windows和Linux下按Ctrl+1组合键就能打开快速修正的弹出框(见图5.4)。

image-20211014102606086

图5.4 在Spring Tool Suite中通过快速修正弹出框创建配置属性

然后,选择“Create Metadata for …”选项来为属性添加元数据(会在additional-spring- configuration-metadata.json文件中进行添加),如果文件还不存在,将会自动创建该文件。

对于taco.orders.pageSize属性来说,我们可以通过如下的JSON为其添加元数据:

1
2
3
4
5
6
7
8
9
10
{
"properties": [
{
"name": "taco.orders.page-size",
"type": "java.lang.String",
"description":
"Sets the maximum number of orders to display in a list."
}
]
}

需要注意,在元数据中引用的属性名为taco.orders.page-size。Spring Boot灵活的属性命名功能允许属性名出现不同的变种,比如taco.orders.page-size等价于taco.orders.pageSize。

元数据准备就绪之后,警告信息就会消失了。除此之外,如果将鼠标指针悬停到taco.orders. pageSize属性上,就会看到如图5.5所示的描述信息。

image-20211014102647143

图5.5 自定义配置属性的悬停帮助信息

另外,在IDE中,就像Spring本身提供的配置属性一样,我们还能具备自动补全功能,如图5.6所示。

image-20211014102704934

图5.6 配置属性的元数据能够帮助实现属性的自动补全功能

我们可以看到,配置属性对于调整自动配置的组件以及应用程序自身的bean都非常有用。但是,如果我们想要为不同的部署环境配置不同的属性又该怎么办呢?接下来,我们看一下该如何使用Spring profile搭建特定环境的配置。