在项目中, 读取外部配置文件的需求很常见
例如
- 这个配置文件包含些敏感内容
- 这个配置文件与server 环境密切相关
- 这个配置文件依赖于其他系统传入
…
静态读取外部文件
静态读取外部文件的写法有很多种
下面举1个常用的例子
例子
@Configuration
@PropertySource(value = "file:${external-custom-config1-path}", ignoreResourceNotFound = true)
public class ExternalConfig {
@Bean("customConfig1")
public String customConfig() {
return getCustomConfig();
}
@Value("${external.custom.config1:not defined}")
private String customConfig;
private String getCustomConfig() {
return this.customConfig;
}
}
上面创建了1个 Configuration 的bean, 里面再定义1个 名字是customConfig1的 bean, 它就是配置项
@PropertySource 作用是引入1个配置文件, 其中配置文件的路径我们写在了application.yaml
external-custom-config1-path: /app/config/external-config.properties
实际上的文件位置在 /app/config/external-config.properties
让程序容忍读取配置文件失败
容忍配置文件不存在:
在@PropertySource 里加上 ignoreResourceNotFound = true
容忍配置项读取失败:
@Value(“${external.custom.config1:not defined}”) 这里加上:和默认值, 如果读取失败, 下面的变量就获取到默认值
如何使用这个配置项
既然我们把配置项定义为了1个bean, 那么在其他bean里直接用 @Autowire 引用就得
下面是把配置项 作为actuator/info 接口输出项的例子:
@Component
@Slf4j
public class AppVersionInfo implements InfoContributor {
@Value("${pom.version}") // https://stackoverflow.com/questions/3697449/retrieve-version-from-maven-pom-xml-in-code
private String appVersion;
@Autowired
private String hostname;
@Autowired
private InfoService infoservice;
@Value("${spring.datasource.url}")
private String dbUrl;
@Value("${spring.profiles.active}")
private String appEnvProfile;
@Autowired
private String customConfig1;
@Override
// https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-endpoints-info
public void contribute(Info.Builder builder) {
log.info("AppVersionInfo: contribute ...");
builder.withDetail("app", "Cloud Order Service")
.withDetail("appEnvProfile", appEnvProfile)
.withDetail("version", appVersion)
.withDetail("hostname",hostname)
.withDetail("dbUrl", dbUrl)
.withDetail("description", "This is a simple Spring Boot application to for cloud order...")
.withDetail("customConfig1", customConfig1)
.withDetail("SystemVariables", infoservice.getSystemVariables());
}
}
注意上customConfig1 这个bean的引用
至于actuator框架的用法请参考:
利用SpringBoot Actuator 来构造/health /info 等监控接口
测试
gateman@MoreFine-S500:~/projects/coding/sql_backup$ curl 127.0.0.1:8080/actuator/info | jq .
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 2356 0 2356 0 0 553k 0 --:--:-- --:--:-- --:--:-- 575k
{
"app": "Cloud Order Service",
"appEnvProfile": "dev",
"version": "1.0.2",
"hostname": "MoreFine-S500",
"dbUrl": "jdbc:mysql://34.39.2.90:6033/demo_cloud_order?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowPublicKeyRetrieval=true",
"description": "This is a simple Spring Boot application to for cloud order...",
"customConfig1": "value of config1",
}
}
gateman@MoreFine-S500:~/projects/coding/sql_backup$ cat /app/config/external-config.properties
external.custom.config1=value of config1
能正确读取
这个方法的一些limitation
- 不能动态读取, 因为只会在构造bean (springboot 程序启动)时读取1个次配置文件
- 只support properties 类型的配置文件, 不支持yaml读取
动态读取外部文件
首先我们列出一些方案
-
方案1, 每次调用配置项都去读取一次配置文件
这种方案看起来可行, 但是如果读取配置频繁的情况下, 会导致大量IO资源浪费, 否决 -
方案2, 利用@RefreshScope
这个方案也是AI 推荐的方案, 但是要引入spring cloud config 的library , 利用spring cloud config bus 功能实现配置文件获取更新
这种方法适合于spring cloud 微服务框架, 但是如果在k8s 环境下, 使用spring cloud 框架太重了。
否决 -
方案3, 不再依赖于bean 来保存配置项, 构建1个定时机制去刷新配项的方法
这个方案由于使用了定时器, 所以更新时间上会有gap, 但是几秒or 1分钟内的gap 很多场景下是可以容忍的, 下面例子就是基于这个方案
例子:
添加1个配置文件在/app/config
gateman@MoreFine-S500:/app/config$ pwd
/app/config
gateman@MoreFine-S500:/app/config$ ls
external-config2.properties external-config.properties external-config.yml
gateman@MoreFine-S500:/app/config$ cat external-config2.properties
external.custom.config2=value of config2222
gateman@MoreFine-S500:/app/config$
构建1个DynamicExternalConfig类
@Configuration("dynamicExternalConfig")
@PropertySource(value = "file:${external-custom-config2-path}", ignoreResourceNotFound = true)
@Slf4j
public class DynamicExternalConfig {
private Properties properties = new Properties();
// could not add @Bean otherwise the call from AppVersionInfo will not make a refresh call
@Getter
@Value("${external.custom.config2:not defined}")
private String customConfig;
@Value("${external-custom-config2-path}")
private String externalConfigPath;
@Scheduled(fixedRate = 6000) // Refresh every 6 seconds
public void refreshConfig() {
try {
log.info("trying to refresh configuration customConfig2");
FileInputStream fis = new FileInputStream(externalConfigPath);
properties.clear();
properties.load(fis);
fis.close();
customConfig = properties.getProperty("external.custom.config2", "not defined");
} catch (IOException e) {
log.error("failed to refresh configuration customConfig2", e);
//throw new RuntimeException(e);
}catch (Exception e) {
log.error("failed to refresh configuration customConfig2, and it's not an IO exception", e);
throw new RuntimeException(e);
}
}
}
值得注意的是:
- 虽然这个类还是基于业务考虑加上@Configuration 注解, 但是实际上不再创建child bean 项, 只是简单地让它成1个bean
- 简单地用1个 属性 customConfig 保存配置项
- 暴露Getter 方法让其他类调用这个配置项值
- 添加1个定时方法 @Scheduled refreshConfig
- 在这个方法内容忍IO Exception 但是不容忍其他Exception
但是在调用这个配置项的类中, 调用方法与本文静态读取配置的例子也有区别, 不能直接使用@Autowired 来引入 customConfig了, 必须用getter方法
@Component
@Slf4j
public class AppVersionInfo implements InfoContributor {
@Value("${pom.version}") // https://stackoverflow.com/questions/3697449/retrieve-version-from-maven-pom-xml-in-code
private String appVersion;
@Autowired
private String hostname;
@Autowired
private InfoService infoservice;
@Value("${spring.datasource.url}")
private String dbUrl;
@Value("${spring.profiles.active}")
private String appEnvProfile;
@Autowired
private String customConfig1;
@Autowired
private DynamicExternalConfig dynamicExternalConfig;
@Override
// https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-endpoints-info
public void contribute(Info.Builder builder) {
log.info("AppVersionInfo: contribute ...");
builder.withDetail("app", "Cloud Order Service")
.withDetail("appEnvProfile", appEnvProfile)
.withDetail("version", appVersion)
.withDetail("hostname",hostname)
.withDetail("dbUrl", dbUrl)
.withDetail("description", "This is a simple Spring Boot application to for cloud order...")
.withDetail("customConfig1", customConfig1)
.withDetail("customConfig2", dynamicExternalConfig.getCustomConfig())
.withDetail("SystemVariables", infoservice.getSystemVariables());
}
}
注意customConfig1 和 customConfig2 的读取区别
这样, 就能简单地实现外部配置文件的动态更新读取