我有一个以标准方式配置的 Spring Boot 应用程序:加载时,它从
application.yaml
文件中获取属性,并将它们注入到多个配置 bean 中,并用 @Component
和 @ConfigurationProperties
进行注释。
我需要自定义计算一些属性。这些属性与网络配置有关:子网掩码和地址、广播IP等。它们的数量相当多,它们因服务器而异,而且我们有很多服务器。我宁愿不因为这个原因而增加属性文件的数量。
我有一个公开网络属性的
LocalNetwork
bean,以及构建它的服务。现在我希望这些属性可以用作 application.yaml
: 中定义的其他属性中的占位符
hosts:
my-first-host: ${local-network.local.ip}
my-second-host: ${local-network.default-loopback.ip}
my-third-host: ${local-network.local.broacast-ip}
somewhere:
else:
broadcast-to: ${hosts.my-third-host}
# I could do without this double indirection,
# but it would be so nice to have it.
我可以使用
LocalNetwork
bean 作为注入到其他 bean 的依赖项,但这会使这部分配置变得相当模糊。我更喜欢 application.yaml
显式传递一些可读的 ${local-network.local.ip}
变量作为其他配置属性的值。
我想出了一种让它部分工作的方法:
@Configuration
@ComponentScan
public class ServicesConfiguration {
@Bean
@Primary
public LocalNetwork networkInterface() {
return LocalNetworkBuilder.build();
}
// ...
// Lots of other things here
// ...
@Bean
public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(LocalNetwork localNetwork) {
Properties properties = new Properties();
properties.putIfAbsent("local-network.default-loopback.ip",
localNetwork.getLoopbackDefault().ip(0).getHostAddress());
properties.putIfAbsent("local-network.local.ip",
localNetwork.getNetwork().ip(0).getHostAddress());
properties.putIfAbsent("local-network.local.broadcast-ip",
localNetwork.getNetwork().broadcastIp().getHostAddress());
PropertySourcesPlaceholderConfigurer p = new PropertySourcesPlaceholderConfigurer();
p.setProperties(properties);
return p;
}
}
但我不喜欢它,有两个原因:
broadcast-to: ${my-third-host}
无法解析,出现错误 无法解析值“${my-third-host}”中的占位符“my-third-host”。不过它确实解决了my-third-host: ${local-network.local.broacast-ip}
。实现这种占位符机制的正统方法是什么?
注意这不适用于服务器应用程序。这是一个测试应用程序,将由操作员安装在服务器上。操作员可能会根据自己的需要编辑外部
application.yaml
。这就是为什么保持属性文件可读非常重要。
N.B.2 我不喜欢直接使用
${local-network.local.broacast-ip}
,而是使用它来初始化 my-third-host
,然后在应用程序文件的其余部分中使用 ${my-third-host}
,原因是为了可读性:所有操作员都熟悉 的名称整个公司使用的主机,因此他们可以更轻松地通过这种方式理解设置。
事实证明,通过用
PropertySourcesPlaceholderConfigurer
替换默认构造函数来直接将属性添加到 @Bean
中可以以某种方式工作,但并不完全:
application.yaml
使用,并且可以使用 ${...}
例如,考虑以下配置:
hosts:
my-first-host: ${local-network.local.ip}
...
somewhere:
else:
broadcast-to: ${hosts.my-third-host}
如果
local-network.local.ip = 167.1.1.1
,则 hosts.my-first-host
正确设置为 167.1.1.1
,但 broadcast-to
设置为 local-network.local-ip
,无需进一步解析。
一种有效的方法是添加一个
ApplicationContextInitializer
,将属性添加为 PropertySource
:
public class NetworkPropertiesContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
private final LocalNetwork localNetwork;
public NetworkPropertiesContextInitializer() {
this.localNetwork = LocalNetworkBuilder.build();
}
@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
ConfigurableEnvironment environment = configurableApplicationContext.getEnvironment();
// Create map for properties and add first (important)
Map<String, Object> networkProperties = new HashMap<>();
networkProperties.put("local-network.default-loopback.ip",
localNetwork.getLoopbackDefault().ip(0).getHostAddress());
networkProperties.put("local-network.local.ip",
localNetwork.getNetwork().ip(0).getHostAddress());
networkProperties.put("local-network.local.broadcast-ip",
localNetwork.getNetwork().broadcastIp().getHostAddress());
// Add properties as a new property source:
environment.getPropertySources().addFirst(
new MapPropertySource("networkProperties",
networkProperties));
}
}
然后您需要从应用程序的入口点引用上下文初始值设定项。
@SpringBootApplication(scanBasePackageClasses = {ServicesConfiguration.class})
public class Application {
public static void main(String[] args) {
new SpringApplicationBuilder(Application.class)
.web(WebApplicationType.NONE)
.initializers(new NetworkPropertiesContextInitializer())
.run(args);
}
}
从测试弹簧配置来看,它会是这样的:
@SpringBootTest(classes = ServicesConfiguration.class)
@ContextConfiguration(initializers = NetworkPropertiesContextInitializer.class)
@EnableAutoConfiguration
public class SpringIntegrationConfig {
}
参考资料:
@SpringBootTest
链接到上下文初始化程序:Spring boot 测试:为每个测试加载上下文?