类型安全的属性配置

doMore 556 2019-10-31
类型安全的属性配置

使用 @Value("$") 注解注入配置属性有时太麻烦,尤其是工作中需要多个属性值或者所需要的数据在环境中有层次结构。为了控制可校验应用中的属性,Spring Boot提供一种强类型 beans 操作properties。比如:

@ConfigurationProperties("acme")
public class AcmeProperties {

	private boolean enabled;

	private InetAddress remoteAddress;

	private final Security security = new Security();

	public boolean isEnabled() { ... }

	public void setEnabled(boolean enabled) { ... }

	public InetAddress getRemoteAddress() { ... }

	public void setRemoteAddress(InetAddress remoteAddress) { ... }

	public Security getSecurity() { ... }

	public static class Security {

		private String username;

		private String password;

		private List<String> roles = new ArrayList<>(Collections.singleton("USER"));

		public String getUsername() { ... }

		public void setUsername(String username) { ... }

		public String getPassword() { ... }

		public void setPassword(String password) { ... }

		public List<String> getRoles() { ... }

		public void setRoles(List<String> roles) { ... }

	}
}

上面的POJO定义了前缀为acme,属性为enabled,remoteAddress等等。还有一个security实体。

注意:

  1. Map,只要初始化就需要一个get方法,但是不一定需要set,因为绑定器可以对它进行修改
  2. 集合和数组能通过索引和使用“,”分隔的多个属性。后一种情况,请执行要求有set方法,如果想初始化一个集合,请确保它是可变的。
  3. 如果嵌套的POJO类型已经被初始化想上述security,不需要添加set方法,但是如果是 private final Security security; 在绑定属性的同事初夹杂着初始化这个实体,则需要一个set方法。

还可以通过在 @EnableConfigurationProperties 注解中直接简单的列出属性类来快捷的注册 @ConfigurationProperties 类的定义。

@Configuration
@EnableConfigurationProperties(ConnectionSettings.class)
public class MyConfiguration {
}

前面配置创建了一个常规的bean AcmeProperties。建议 @ConfigurationProperties 仅仅处理换将变量,不要再上下文中注入其他beans。请记住 @EnableConfigurationProperties 注解也会自动应用在你的项目中因此任何存在 @ConfigurationProperties 注解的类都应该从环境中获取配置。而不是让 MyConfiguration 注解 @EnableConfigurationProperties(AcmeProperties.class),应该让 AcmeProperties 成为一个bean(即被spring容器管理的类),如下例子:

@Component
@ConfigurationProperties(prefix="acme")
public class AcmeProperties {
	// ... see the preceding example
}

这样风格的配置方式和 SpringApplication外部yaml问价结合更加好:

# application.yml
acme:
	remote-address: 192.168.1.1
	security:
		username: admin
		roles:
		  - USER
		  - ADMIN
# additional configuration as required

注:使用@ConfigurationProperties能够产生可被IDEs使用的元数据文件。

第三方配置:

正如使用@ConfigurationProperties注解一个类,你也可以在@Bean方法上使用它。当你需要绑定属性到不受你控制的第三 方组件时,这种方式非常有用。 为了从Environment属性配置一个bean,将@ConfigurationProperties添加到它的bean注册过程:

@ConfigurationProperties(prefix = "foo")
@Bean
public FooComponent fooComponent() {
...
}

和上面ConnectionSettings的示例方式相同,任何以foo为前缀的属性定义都会被映射到FooComponent上。

松散绑定

Spring Boot使用一些宽松的规则用于绑定Environment属性到@ConfigurationProperties beans,所以Environment属性名和 bean属性名不需要精确匹配。常见的示例中有用的包括虚线分割(比如,context--path绑定到contextPath)和将环境属性转 为大写字母(比如,PORT绑定port)。 示例:

@ConfigurationProperties(prefix="acme.my-project.person")
public class OwnerProperties {
	private String firstName;
	public String getFirstName() {
		return this.firstName;
	}
	public void setFirstName(String firstName) {
		this.firstName = firstName;
	}
}

在上面的例子中,一下的属性名字都将被使用: 1.acme.my-project.person.first-name Kebab 情况,建议在 .properties 和 .yaml 文件中使用。 2.acme.myProject.person.firstName 标准驼峰命名写法 3.acme.my_project.person.first_name 下划线表示,这是在 .properties 和 .yaml 中另一种写法。 4.ACME_MYPROJECT_PERSON_FIRSTNAME 建议在使用系统环境变量时使用。

注意:注解中 prefix 值必须是 kebab 情况(全部小写或者使用-分隔。eg:acme.my-project.person)

松散绑定规则每个属性资源
  1. properties 驼峰、下划线、kebab 标准的list语法使用[]或者逗号分隔的值
  2. yaml 驼峰、下划线、kebab 标准的yaml语法使用[]或者逗号分隔
  3. environment 全部大写使用下划线分隔 使用双下划线包裹数字eg:MY_ACME_1_OTHER = my.acme[1].other
  4. system 驼峰、下划线、kebab 标准的list语法使用[]或者逗号分隔

提示:We recommend that, when possible, properties are stored in lower-case kebab format, such as my.property-name=acme.

当要绑定 Map 属性,如果 key 存在其他不是小写字母或者-,你需要使用括号避免值被改变。如果没有被[]包裹,任何非小写字母或者-的字符都被移除。例如:

acme:
  map:
    "[/key1]": value1
    "[/key2]": value2
    /key3: value3
map中key的值分别为 /key1,/key2和key3
合并复杂类型

当在不同的地方配置列表,通过替换整个列表进行覆盖。 例如,假设MyPojo对象的名称和描述属性默认为null。下面的例子展示了一个来自AcmeProperties的MyPojo对象列表:

@ConfigurationProperties("acme")
public class AcmeProperties {

	private final List<MyPojo> list = new ArrayList<>();
	public List<MyPojo> getList() {
		return this.list;
	}

}

参考以下配置:

acme:
  list:
    - name: my name
      description: my description
---
spring:
  profiles: dev
acme:
  list:
    - name: my another name

当dev没有被激活时list中有一个 entry 就是name为my name。description为my description。当dev被激活时list中还是只有一个,只是值为name=my anthor name。 当list被配置多个的时候,优先级高的被使用(也就是什么环境配置被激活,就使用哪个)。

注:前面的合并规则适用于来自所有属性源的属性,而不仅仅是YAML文件。

属性转换

当Spring Boot绑定到@ConfigurationProperties bean时,它尝试将外部应用程序属性强制转换为正确的类型。如果需要自定义类型转换,可以提供一个ConversionService bean(使用一个名为ConversionService的bean)或自定义属性编辑器(通过CustomEditorConfigurer bean)或自定义转换器(使用注释为@ConfigurationPropertiesBinding的bean定义)。

注意: As this bean is requested very early during the application lifecycle, make sure to limit the dependencies that your ConversionService is using. Typically, any dependency that you require may not be fully initialized at creation time. You may want to rename your custom ConversionService if it is not required for configuration keys coercion and only rely on custom converters qualified with @ConfigurationPropertiesBinding. 由于此bean在应用程序生命周期中很早就被请求,请确保限制您的ConversionService正在使用的依赖项。通常,您需要的任何依赖项可能在创建时没有完全初始化。如果配置键强制转换不需要重命名自定义转换服务,并且只依赖使用@ConfigurationPropertiesBinding限定的自定义转换器,那么您可能希望重命名自定义转换服务。

@ConfigurationProperties校验

Spring Boot将尝试校验外部的配置,默认使用JSR-303(如果在classpath路径中)。你可以轻松的为你的 @ConfigurationProperties类添加JSR-303 javax.validation约束注解:

@Component
@ConfigurationProperties(prefix="connection")
public class ConnectionSettings {
@NotNull
private InetAddress remoteAddress;
// ... getters and setters
}

小技巧:你能创建一个@Bean的方法创建一个标注了 @Validated 的类来触发校验。

尽管嵌套属性在绑定的时候也会被校验,但最好是将该属性标记上 @Valid。这样确保在没有找到嵌套属性的情况下也会触发校验。如下例子:

@ConfigurationProperties(prefix="acme")
@Validated
public class AcmeProperties {
	@NotNull
	private InetAddress remoteAddress;
	@Valid
	private final Security security = new Security();
	// ... getters and setters
	public static class Security {
		@NotEmpty
		public String username;
		// ... getters and setters
	}
}

你也可以通过创建一个叫做configurationPropertiesValidator的bean来添加自定义的Spring Validator。实例化该bean的方法应该被设置为static。配置属性的检验是在应用生命周期的最早期,声明方法为静态的可以创建bean但是不需要实例化 @Configration 类。这么做能够避免早起由于实例化引起的问题。 例子: resources

#application.yml
sample.host=192.168.0.1
sample.port=7070

启动类

@SpringBootApplication
public class SamplePropertyValidationApplication implements CommandLineRunner {

	private final SampleProperties properties;

	public SamplePropertyValidationApplication(SampleProperties properties) {
		this.properties = properties;
	}

	@Bean
	public static Validator configurationPropertiesValidator() {
		return new SamplePropertiesValidator();
	}

	@Override
	public void run(String... args) {
		System.out.println("=========================================");
		System.out.println("Sample host: " + this.properties.getHost());
		System.out.println("Sample port: " + this.properties.getPort());
		System.out.println("=========================================");
	}

	public static void main(String[] args) {
		new SpringApplicationBuilder(SamplePropertyValidationApplication.class).run(args);
	}

检验类

public class SamplePropertiesValidator implements Validator {

	final Pattern pattern = Pattern.compile("^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$");

	@Override
	public boolean supports(Class<?> type) {
		return type == SampleProperties.class;
	}

	@Override
	public void validate(Object o, Errors errors) {
		ValidationUtils.rejectIfEmpty(errors, "host", "host.empty");
		ValidationUtils.rejectIfEmpty(errors, "port", "port.empty");
		SampleProperties properties = (SampleProperties) o;
		if (properties.getHost() != null && !this.pattern.matcher(properties.getHost()).matches()) {
			errors.rejectValue("host", "Invalid host");
		}
	}

}

配置属性类

@Component
@ConfigurationProperties(prefix = "sample")
@Validated
@Data
public class SampleProperties {

	/**
	 * Sample host.
	 */
	private String host;

	/**
	 * Sample port.
	 */
	private Integer port = 8080;
}

最后说一下 @Value 是支持spel表达式的 @ConfigurationProperties 不支持,除非在使用单个自己需要注入的值,否则不建议使用 @Value。