springboot service如何动态读取外部配置文件
在项目中, 读取外部配置文件的需求很常见
例如
- 这个配置文件包含些敏感内容
- 这个配置文件与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-codeprivate String appVersion;@Autowiredprivate String hostname;@Autowiredprivate InfoService infoservice;@Value("${spring.datasource.url}")private String dbUrl;@Value("${spring.profiles.active}")private String appEnvProfile;@Autowiredprivate String customConfig1;@Override// https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-endpoints-infopublic 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 CurrentDload 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 secondspublic 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-codeprivate String appVersion;@Autowiredprivate String hostname;@Autowiredprivate InfoService infoservice;@Value("${spring.datasource.url}")private String dbUrl;@Value("${spring.profiles.active}")private String appEnvProfile;@Autowiredprivate String customConfig1;@Autowiredprivate DynamicExternalConfig dynamicExternalConfig;@Override// https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-features.html#production-ready-endpoints-infopublic 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 的读取区别
这样, 就能简单地实现外部配置文件的动态更新读取
