springboot service如何动态读取外部配置文件

在项目中, 读取外部配置文件的需求很常见

例如

  1. 这个配置文件包含些敏感内容
  2. 这个配置文件与server 环境密切相关
  3. 这个配置文件依赖于其他系统传入





静态读取外部文件

静态读取外部文件的写法有很多种
下面举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
  1. 不能动态读取, 因为只会在构造bean (springboot 程序启动)时读取1个次配置文件
  2. 只support properties 类型的配置文件, 不支持yaml读取





动态读取外部文件

首先我们列出一些方案

  1. 方案1, 每次调用配置项都去读取一次配置文件
    这种方案看起来可行, 但是如果读取配置频繁的情况下, 会导致大量IO资源浪费, 否决

  2. 方案2, 利用@RefreshScope
    这个方案也是AI 推荐的方案, 但是要引入spring cloud config 的library , 利用spring cloud config bus 功能实现配置文件获取更新
    这种方法适合于spring cloud 微服务框架, 但是如果在k8s 环境下, 使用spring cloud 框架太重了。
    否决

  3. 方案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);
        }
    }
}

值得注意的是:

  1. 虽然这个类还是基于业务考虑加上@Configuration 注解, 但是实际上不再创建child bean 项, 只是简单地让它成1个bean
  2. 简单地用1个 属性 customConfig 保存配置项
  3. 暴露Getter 方法让其他类调用这个配置项值
  4. 添加1个定时方法 @Scheduled refreshConfig
  5. 在这个方法内容忍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 的读取区别

这样, 就能简单地实现外部配置文件的动态更新读取

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nvd11

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值