创建自己的配置属性

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

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

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

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

  model.addAttribute("orders",
      orderRepo.findByUserOrderByPlacedAtDesc(user));

  return "orderList";
}

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

List<Order> findByUserOrderByPlacedAtDesc(User user);

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

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

@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也需要对应修改:

List<TacoOrder> findByUserOrderByPlacedAtDesc(
        User user, Pageable pageable);

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

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

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

程序清单6.1 在OrderController中启用配置属性功能
@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";
  }

}

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

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

taco:
  orders:
    pageSize: 10

对于在生产环境中需要快速更改的情况,我们可以将 taco.orders.pageSize 设置为环境变量,以避免重新构建和重新部署应用:

$ export TACO_ORDERS_PAGESIZE = 10

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

定义配置属性的持有者

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

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

程序清单6.2 将pageSize抽取到持有者类中
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,我们可以从中移除pageSize,并注入和使用OrderProps bean:

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中进行变更。对于测试,我们可以很容易地直接在测试专用的OrderProps中设置配置属性,并在测试之前将其传给控制器。

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

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;

}

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

声明配置属性元数据

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

image 2024 03 13 17 43 35 045
Figure 1. 图6.2 缺少配置属性元数据所产生的警告

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

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

image 2024 03 13 17 44 03 702
Figure 2. 图6.3 Spring Tool Suite中配置属性的悬停文档

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

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

快速添加缺失的元数据

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

image 2024 03 13 17 45 18 046
Figure 3. 图6.4 在Spring Tool Suite中通过快速修正弹出框创建配置属性

然后,选择 “Create Metadata for …​” 选项来为属性添加元数据。如果文件还不存在,快速修正功能会创建 META-INF/additional-spring-configuration-metadata.json 文件并为 pageSize 填充一些元数据,如下所示:

{"properties": [{
  "name": "taco.orders.page-size",
  "type": "java.lang.String",
  "description": "A description for 'taco.orders.page-size'"
}]}

需要注意,在元数据中引用的属性名为taco.orders.page-size,而application.yml中实际的属性名是pageSize。Spring Boot灵活的属性命名功能允许出现属性名出现不同的变种,比如taco.orders.page-size等价于taco.orders.pageSize,所以它对使用不会产生太大的影响。

写到additional-spring-configuration-metadata.json文件中的初始元数据是一个很好的起点,但是我们可能还想对其稍作编辑。首先,pageSize并不是java.lang.String类型,所以我们需要将其修改为java.lang.Integer。而且,description应该要更明确地描述pageSize是做什么的。如下的JSON代码样例展示了编辑之后的元数据:

{"properties": [{
  "name": "taco.orders.page-size",
  "type": "java.lang.Integer",
  "description": "Sets the maximum number of orders to display in a list."
}]}

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

image 2024 03 13 17 46 46 193
Figure 4. 图6.5 自定义配置属性的悬停帮助信息

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

image 2024 03 13 17 47 23 330
Figure 5. 图6.6 配置属性的元数据能够帮助实现属性的自动补全功能

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