Skip to main content

14小节:基于模板方法的多配置中心抽象层设计

作者:程序员马丁

在线博客:https://nageoffer.com

note

热门项目实战社群,收获国内众多知名公司面试青睐,近千名同学面试成功!助力你在校招或社招上拿个offer。

基于模板方法的多配置中心抽象层设计,元数据信息:

©版权所有 - 拿个offer-开源&项目实战星球专属学习项目,依据《中华人民共和国著作权法实施条例》《知识星球产权保护》,严禁未经本项目原作者明确书面授权擅自分享至 GitHub、Gitee 等任何开放平台。违者将面临法律追究。


内容摘要:本文通过对 Nacos、Apollo 两种配置中心刷新实现的对比剖析,引出其在多配置中心扩展中的设计短板,继而引入模板方法设计模式对刷新逻辑进行抽象重构。

课程目录如下所示:

  • 前言
  • 多配置中心设计短板
  • 什么是模板方法设计模式?
  • 使用模板方法重构刷新事件
  • 文末总结

前言

在前面的章节中,为了帮助大家快速理解动态线程池的配置刷新机制,我们使用了一些“临时代码”作为演示。这些代码虽然简洁直观,但并不是我们真正落地时采用的方式。

在 oneThread 的真实实现中,我们为了支持多种配置中心并保持良好的可维护性,采用了模板方法模式来封装公共流程、抽象差异行为,从而实现可扩展、高内聚的动态配置刷新能力。

本文将带你完整梳理线程池配置动态化的设计演进,重点聚焦在多配置中心支持的设计短板为何需要引入模板方法模式

多配置中心设计短板

目前我们已经支持 Nacos 和 Apollo 两种主流配置中心,下面是它们各自的监听实现方式。

1. Nacos 配置刷新事件实现

@Slf4j
@RequiredArgsConstructor
public class NacosCloudRefresherHandlerV1 implements ApplicationRunner {

private final ConfigService configService;
private final BootstrapConfigProperties properties;

@Override
public void run(ApplicationArguments args) throws Exception {
BootstrapConfigProperties.NacosConfig nacosConfig = properties.getNacos();
configService.addListener(
nacosConfig.getDataId(),
nacosConfig.getGroup(),
new Listener() {

@Override
public Executor getExecutor() {
return xxx;
}

@Override
public void receiveConfigInfo(String configInfo) {
refreshThreadPoolProperties(content); // 刷新动态线程池配置
}
});

log.info("Dynamic thread pool refresher, add nacos cloud listener success. data-id: {}, group: {}", nacosConfig.getDataId(), nacosConfig.getGroup());
}

public void refreshThreadPoolProperties(String configInfo) throws IOException {
Map<Object, Object> configInfoMap = ConfigParserHandler.getInstance().parseConfig(configInfo, properties.getConfigFileType());
ConfigurationPropertySource sources = new MapConfigurationPropertySource(configInfoMap);
Binder binder = new Binder(sources);

BootstrapConfigProperties refresherProperties = binder.bind(BootstrapConfigProperties.PREFIX, Bindable.ofInstance(properties)).get();
log.info("Latest updated configuration: \n{}", configInfo);
log.info("Java configuration object binding: \n{}", JSON.toJSONString(refresherProperties));

// ...... 检测线程池参数是否变更,如果已变更则进行更新
}
}

2. Apollo 配置刷新事件实现

@Slf4j
@RequiredArgsConstructor
public class ApolloRefresherHandlerV1 implements ApplicationRunner {

private final BootstrapConfigProperties properties;

@Override
public void run(ApplicationArguments args) throws Exception {
BootstrapConfigProperties.ApolloConfig apolloConfig = properties.getApollo();
String[] apolloNamespaces = apolloConfig.getNamespace().split(",");

String namespace = apolloNamespaces[0];
String configFileType = properties.getConfigFileType().getValue();
Config config = ConfigService.getConfig(String.format("%s.%s", namespace, properties.getConfigFileType().getValue()));

ConfigChangeListener configChangeListener = createConfigChangeListener(namespace, configFileType);
config.addChangeListener(configChangeListener);

log.info("Dynamic thread pool refresher, add apollo listener success. namespace: {}", namespace);
}

private ConfigChangeListener createConfigChangeListener(String namespace, String configFileType) {
return configChangeEvent -> {
// 如果 Apollo 配置文件变更,会触发该方法进行回调
String namespaceItem = namespace.replace("." + configFileType, "");
ConfigFileFormat configFileFormat = ConfigFileFormat.fromString(configFileType);
ConfigFile configFile = ConfigService.getConfigFile(namespaceItem, configFileFormat);
String content = configFile.getContent(); // 变更后的最新内容
refreshThreadPoolProperties(content); // 刷新动态线程池配置
};
}

public void refreshThreadPoolProperties(String configInfo) throws IOException {
Map<Object, Object> configInfoMap = ConfigParserHandler.getInstance().parseConfig(configInfo, properties.getConfigFileType());
ConfigurationPropertySource sources = new MapConfigurationPropertySource(configInfoMap);
Binder binder = new Binder(sources);

BootstrapConfigProperties refresherProperties = binder.bind(BootstrapConfigProperties.PREFIX, Bindable.ofInstance(properties)).get();
log.info("Latest updated configuration: \n{}", configInfo);
log.info("Java configuration object binding: \n{}", JSON.toJSONString(refresherProperties));

// ...... 检测线程池参数是否变更,如果已变更则进行更新
}
}

3. 存在的主要问题

随着对配置中心的支持增加,上述实现会暴露出以下设计短板:

3.1 代码重复

  • refreshThreadPoolProperties() 的逻辑在每个类中重复出现

  • ApplicationRunner 接口需要在每个类中重复实现

  • 每增加一种配置中心,都需复制粘贴一份逻辑,不利于维护与扩展。

3.2 行为无法约束

  • 每个配置中心的监听实现类触发时机都是自由发挥,缺乏统一约束规范

  • 容易出现调用时机不一致、监听失败无兜底、线程池变更未触发等问题;

  • 难以统一升级或增强公共能力(例如统一异常处理、线程池重载逻辑等)。

接下来的章节将介绍我们是如何借助模板方法模式设计一个统一的刷新抽象类,并支持以最小成本扩展更多配置中心的。

什么是模板方法设计模式?

1. 模板方法定义

模板方法设计模式是一种行为设计模式,它在一个方法中定义了一个操作的框架,而将一些步骤的实现延迟到子类中。通过这种方式,模板方法允许子类在不改变算法结构的情况下重新定义算法中的某些步骤。

通俗来讲 : 定义一个抽象类 AbstractTemplate,并定义一个或若干抽象方法 abstractMethod

由子类去继承抽象类的同时实现抽象方法, 在抽象类的公共方法中调用抽象方法,最终调用的就是不同子类实现的方法逻辑。

2. 用户登录举例

**背景:**假设我们有一套用户登录逻辑,其总体流程如下:

  1. 获取用户输入;
  2. 校验身份(用户名密码、验证码、OAuth token 等);
  3. 登录成功后写入日志或发通知。

这个流程对于不同登录方式(账号密码登录、微信扫码登录、钉钉免登登录等)是一致的,只有“校验方式”不同,典型适合使用模板方法模式抽象出公共流程。

2.1 抽象模板类

public abstract class LoginTemplate {

/**
* 模板方法:定义固定登录流程
*/
public final void login(String loginId) {
// 1. 获取用户输入
String input = getLoginInput(loginId);
// 2. 执行认证逻辑(由子类决定怎么认证)
boolean success = authenticate(input);
if (success) {
// 3. 登录成功后的处理
postLogin(loginId);
} else {
System.out.println("[LoginTemplate] 登录失败");
}
}

/**
* 获取登录输入(可选步骤,默认实现)
*/
protected String getLoginInput(String loginId) {
return loginId; // 默认直接返回用户名等
}

/**
* 认证逻辑(抽象方法)
*/
protected abstract boolean authenticate(String input);

/**
* 登录成功后的操作(钩子方法)
*/
protected void postLogin(String loginId) {
System.out.println("[LoginTemplate] 登录成功,用户:" + loginId);
}
}

2.2 子类一:账号密码登录

public class UsernamePasswordLogin extends LoginTemplate {

@Override
protected boolean authenticate(String input) {
System.out.println("[UsernamePasswordLogin] 正在校验用户名和密码...");
return "admin".equals(input); // 模拟判断用户名等于 admin 才能登录
}

@Override
protected void postLogin(String loginId) {
System.out.println("[UsernamePasswordLogin] 登录成功,记录登录日志:" + loginId);
}
}

2.3 子类二:微信扫码登录

public class WeChatScanLogin extends LoginTemplate {

@Override
protected boolean authenticate(String input) {
System.out.println("[WeChatScanLogin] 正在校验微信二维码...");
return input.startsWith("wx_"); // 模拟微信二维码前缀
}

@Override
protected void postLogin(String loginId) {
System.out.println("[WeChatScanLogin] 登录成功,推送欢迎消息:" + loginId);
}
}

2.4 测试运行

public class LoginTest {
public static void main(String[] args) {
LoginTemplate login1 = new UsernamePasswordLogin();
login1.login("admin");

System.out.println("-----");

LoginTemplate login2 = new WeChatScanLogin();
login2.login("wx_123456");
}
}

[UsernamePasswordLogin] 正在校验用户名和密码...
[UsernamePasswordLogin] 登录成功,记录登录日志:admin
-----
[WeChatScanLogin] 正在校验微信二维码...
[WeChatScanLogin] 登录成功,推送欢迎消息:wx_123456

这里拿用户名密码登录方式举例,时序图如下所示:

3. 模式优点

  • 登录流程完全由模板类控制,流程统一;

  • 子类只关心 认证方式后处理动作

  • 方便扩展钉钉登录、验证码登录等,无需改动已有逻辑

使用模板方法重构刷新事件

动态线程池在多配置中心支持的场景下,监听器注册与配置刷新存在高度相似的处理流程。为了统一逻辑、增强可扩展性,我们引入模板方法模式进行重构,形成一套结构清晰、职责分明的刷新机制。

1. 类图

动态刷新场景逻辑如下:

解锁付费内容,👉 戳