0. 目录
1. 背景
为了增加软件的自适应性,我们通常会依据分析和经验来给产品增加诸多配置项。
但万事有利必有弊,当你的软件产品以本地化为主要部署场景,尤其是公司采取的是"实施兼职运维"的运行模式时,各类诸如"配置修改错了位置","未遵循配置文件格式(典型如yaml)"等等总能给你玩点新花样的状况,让研发人员陷入此类低级问题中始终无法抽身。
被动挨打不是我们的风格。本文尝试给出一个基于SpringCloud的思路方案。注:本文不会涉及过多的技术细节研究,主要是一个流程思路分享。
2. 关键技术
罗列下相关的关键技术:
- SpringCloud中的
actuator/restart功能。
1.1 这个功能并不局限在SpringCloud里,核心类RestartEndpoint没有额外依赖,将其单独拷贝出来就可以在SpringBoot项目里使用了。
1.2 该功能并非必选项。只有对于"应用端口","数据库链接"等需要重启才能生效的配置项才是必须。 - Spring中如何加载额外配置项。
- 设置额外配置项的读取优先级。参考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.

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

被折叠的 条评论
为什么被折叠?



