【思路】基于Spring实现配置的界面化修改

本文为增加软件自适应性,提出基于SpringCloud的思路方案。介绍了关键技术,包括SpringCloud功能及加载额外配置项方法。以多子域端口配置为例阐述实现细节,还指出重启应用可能出现的问题及解决方案,最后给出GPT建议和Spring执行顺序相关内容。
摘要由CSDN通过智能技术生成

1. 背景

为了增加软件的自适应性,我们通常会依据分析和经验来给产品增加诸多配置项。

但万事有利必有弊,当你的软件产品以本地化为主要部署场景,尤其是公司采取的是"实施兼职运维"的运行模式时,各类诸如"配置修改错了位置","未遵循配置文件格式(典型如yaml)"等等总能给你玩点新花样的状况,让研发人员陷入此类低级问题中始终无法抽身。

被动挨打不是我们的风格。本文尝试给出一个基于SpringCloud的思路方案。注:本文不会涉及过多的技术细节研究,主要是一个流程思路分享。

2. 关键技术

罗列下相关的关键技术:

  1. SpringCloud中的actuator/restart功能。
    1.1 这个功能并不局限在SpringCloud里,核心类RestartEndpoint没有额外依赖,将其单独拷贝出来就可以在SpringBoot项目里使用了。
    1.2 该功能并非必选项。只有对于"应用端口","数据库链接"等需要重启才能生效的配置项才是必须。
  2. Spring中如何加载额外配置项。
  3. 设置额外配置项的读取优先级。参考Grafana等软件,对于所提供的配置项不进行原地修改,而是采用提供诸如custom.yaml的方式来覆盖默认配置项。

3. 实现

接下来我们以一个实际的场景需求"多子域端口配置"为脉络,来介绍相关的实现细节。

3.1 实现细节 - 加载额外配置项

这里直接贴出代码进行讲解。

/**
 * <p>
 * 各模块自定义配置文件配置项加载配置
 * 
 * <p>
 * 思路:
 * <p>
 * 对于系统中提供的配置项, 如果使用者有额外的调整需求, 有一个比较好的实践是:不进行原地修改,而是提供一个诸如custom.yaml的约定配置文件, 用其中的配置项设置覆盖原始的.
 * <p>
 * 这样的思路遵循了古老的"开闭原则", 在诸如grafana等成熟开源软件中得到广泛应用.
 * <p>
 * 如此好的实践我们没道理不进行学习借鉴.
 **/
@Slf4j
@Configuration
public class CustomPropertiesConfig {
   

    /**
     * <p>
     * 这里注入的配置只能使用{@code @Value(${})} 读取到相应的属性配置项
     *
     * @description 加载属性配置
     */
    @Bean
    public static PropertySourcesPlaceholderConfigurer customPropertySourcesPlaceholderConfig()
        throws IOException {
   

        final PropertySourcesPlaceholderConfigurer config = new PropertySourcesPlaceholderConfigurer();
        // 推荐其中只放:需要重启才能生效的配置。其中用注释打上样例。
        // 动态生效的配置放到consul里
        final Resource customConfigFileResource = ModuleDetector.getCustomConfigFileResource();
        //
        log.warn("### the customconfig file path in module [ {} ] is [ {} ]", ModuleDetector.getCurrentModuleName(),
            ModuleDetector.getCustomConfigFileFullPath());

        config.setLocations(new Resource[] {
   customConfigFileResource});
        // 关键 - 设置额外配置项的读取优先级
        // 此项配置使得我们的自定义配置文件拥有了覆盖默认配置项的能力
        config.setLocalOverride(true);
        return config;
    }
}

// ================================================ 辅助类:ModuleDetector 

public class ModuleDetector {
   
    public static String getCurrentModuleName() {
   
        // 省略
        return "xxxxx";
    }

    public static File getCustomConfigFileFullPath() {
   
        try {
   
            return getCustomConfigFileResource().getFile();
        } catch (IOException e) {
   
            throw new RuntimeException(e);
        }
    }

    public static Resource getCustomConfigFileResource() {
   
        final String currentModuleName = getCurrentModuleName();

        final String customConfigFile =
            StrUtil.format("file:./_customconfig/{}-application-customconfig.properties", currentModuleName);
        // TODO 这里可以优化下
        // \_customconfig\xxxx-application-customconfig.properties
        // 这里最终选择了properties文件是为了方便更新, 规避掉yaml的强格式要求
        try {
   
            final DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
            Resource resource = resourceLoader.getResource(customConfigFile);
            //
            makesureFileExist(resource.getFile());
            return resource;
        } catch (IOException e) {
   
            throw new RuntimeException(e);
        }
    }

    private static void makesureFileExist(File fileMayoutExist) {
   
        if (FileUtil.exist(fileMayoutExist)) {
   
            return;
        }

        FileUtil.writeUtf8String(StrUtil.EMPTY, fileMayoutExist);
    }

    /**
     * @param key 该key默认已经存在, 只是被注释
     * @param val
     */
    public static void modifyKV(final String key, final String val) {
   
        // 相关: zheng中的PropertiesFileUtil.java
        final File customConfigFileFullPath = getCustomConfigFileFullPath();
        
        // 1. 先备份
        final String destFilename = StrUtil.format("{}-{}", customConfigFileFullPath.getName(),
            DateUtil.format(DateUtil.date(), DatePattern.PURE_DATETIME_PATTERN));
        FileUtil.copyContent(customConfigFileFullPath, FileUtil.file(StrUtil
            .replace(customConfigFileFullPath.getAbsolutePath(), customConfigFileFullPath.getName(), destFilename)),
            true);

        // 2. 再写入
        final List<String> finalFileContents = CollUtil.newArrayList();
        FileUtil.readUtf8Lines(customConfigFileFullPath, new LineHandler() {
   

            @Override
            public void handle(String line) {
   
                if (StrUtil.startWith(line, key) || //
                (StrUtil.startWith(line, "#") && StrUtil.contains(line, key))) {
   
                    final String newLineContent = StrUtil.format("{}={}", key, val);
                    finalFileContents.add(newLineContent);
                } else {
   
                    finalFileContents.add(line);
                }
            }
        });

        FileUtil.writeUtf8Lines(finalFileContents, customConfigFileFullPath);
    }

    /**
     * @return 备份文件数量, 其实也就是针对配置文件的操作次数
     */
    public static List<String> getCustomConfigFileBackupFilenames() {
   
        final File customConfigFolderPath = getCustomConfigFolderPath();
        return FileUtil.listFileNames(customConfigFolderPath.getAbsolutePath()).stream()
            .filter(fileName -> FileUtil.extName(fileName).contains("properties-")).collect(Collectors.toList());
    }
}

3.2 实现细节 - 重启应用

相较于直接暴露 /actuator/restart 接口,更推荐后端做一层封装:

@Slf4j
@ApiIgnore  // 减小被发现的可能
@RestController
@RequestMapping("/svcmanager")
public class ManagerController {
   
    private RestartEndpoint restartEndpoint;
    private ShutdownEndpoint shutdownEndpoint;
    private RefreshEndpoint refreshEndpoint;

    public ManagerController(ShutdownEndpoint shutdownEndpoint, RestartEndpoint restartEndpoint,
        RefreshEndpoint refreshEndpoint) {
   
        this.shutdownEndpoint = shutdownEndpoint;
        this.restartEndpoint = restartEndpoint;
        this.refreshEndpoint = refreshEndpoint;
    }

    @PostMapping("/restart")
    public String restart() {
   
        // TODO 多种鉴权机制确保被安全使用.
        log.warn("### restart current application");

        stopwatch(() -> {
   
            restartEndpoint.restart();
        }, sw -> {
   
            log.warn("### [ {} ]restart耗时: [ {} ]s", ModuleDetector.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值