Spring 架构如何优雅地切换新平台?
大约 2 分钟
背景
企业级开发中,对接外部服务(如培训平台、支付网关、短信平台)是常态。
系统当前对接了“旧学习平台”,现需迁移至“新学习平台”。要求在迁移过程中:
无缝切换:不修改现有的业务逻辑调用代码。
可配置回滚:支持通过配置文件随时切回旧平台。
低耦合:新旧平台逻辑物理隔离。
设计思路
采用 代理委托模式。通过定义统一的接口,并实现一个带有 @Primary 注解的上下文代理类(Context),由代理类根据配置项动态决定调用哪个具体实现。
代码实现结构
定义统一接口:提取原有的对接接口,确保新旧平台实现一致的行为。
public interface LearningPlatformService {
CourseInfo getCourseDetail(String courseId);
void pushLearningProgress(String userId, String progress);
}旧平台:保持原逻辑,指定 Bean 名称。
@Service("oldPlatformService")
public class OldPlatformServiceImpl implements LearningPlatformService { ... }新平台:实现新接口逻辑,指定 Bean 名称。
@Service("newPlatformService")
public class NewPlatformServiceImpl implements LearningPlatformService { ... }核心代理类 (LearningPlatformContext)
利用 @Primary 让 Spring 优先注入此类。通过 Map 自动收集所有实现类。
@Component
@Primary
public class LearningPlatformContext implements LearningPlatformService {
@Value("${learning.platform.type:old}") // 默认 old
private String platformType;
@Autowired
private Map<String, LearningPlatformService> platformMap;
private LearningPlatformService getService() {
String beanName = "new".equalsIgnoreCase(platformType)
? "newPlatformService" : "oldPlatformService";
return platformMap.get(beanName);
}
@Override
public CourseInfo getCourseDetail(String courseId) {
return getService().getCourseDetail(courseId);
}
@Override
public void pushLearningProgress(String userId, String progress) {
getService().pushLearningProgress(userId, progress);
}
}配置文件 (application.yml)
learning:
platform:
type: new # 可选值: old (旧平台), new (新平台)动态切换(无需重启)
若配合数据字典,建议在 LearningPlatformContext 类中处理。
private IELearningService getService() {
QueryWrapper<SysDictItem> dictItemWrapper = new QueryWrapper<>();
dictItemWrapper.eq("item_value","trainSwitch");
SysDictItem sysDictItem = sysDictItemMapper.selectOne(dictItemWrapper);
if (null != sysDictItem && ResultConstants.DICT_ITEM_STATUS.equals(sysDictItem.getItemStatus())) {
return context.getBean("oldPlatformService", IELearningService.class);
} else {
return context.getBean("newPlatformService", IELearningService.class);
}
}若配合 Nacos 或 Apollo 配置中心,建议在 LearningPlatformContext 类上添加 @RefreshScope:
在配置中心修改 learning.platform.type 为 new。
发布配置,系统将即时生效,后续所有请求自动路由至新平台。
通用请求方法
/**
* 通用请求方法
*/
private <T> T executeRequest(String url, Method method, Object params, Class<T> responseClass) {
// 处理 GET 请求的参数拼接
if (Method.GET.equals(method) && params != null) {
if (params instanceof Map) {
url = HttpUtil.urlWithForm(url, (Map<String, Object>) params, null, false);
} else {
url = HttpUtil.urlWithForm(url, JSONUtil.parseObj(params), null, false);
}
}
// 生成 Header
Map<String, String> headers = generateHeaders();
// 创建请求对象
HttpRequest request = HttpUtil.createRequest(method, url)
.headerMap(headers, false)
.timeout(20000);
// 处理 POST/PUT 请求
if ((Method.POST.equals(method) || Method.PUT.equals(method)) && params != null) {
request.body(JSONUtil.toJsonStr(params));
}
log.info("发起请求: [{}] {}", method, url);
try (HttpResponse response = request.execute()) {
String resultBody = response.body();
if (response.getStatus() == 200) {
if (StrUtil.isBlank(resultBody)) {
log.warn("接口返回内容为空,URL: {}", url);
return null;
}
checkAndThrowBusinessError(resultBody);
if (JSONUtil.isJsonArray(resultBody) && responseClass.isArray()) {
Class<?> componentType = responseClass.getComponentType();
return (T) JSONUtil.parseArray(resultBody).toArray(componentType);
}
return JSONUtil.toBean(resultBody, responseClass);
}
// 处理异常情况
log.error("调用接口失败, URL: {}, 状态码: {}, 响应内容: {}", url, response.getStatus(), resultBody);
throw new RuntimeException("白鹭智学接口调用异常: " + resultBody);
} catch (Exception e) {
log.error("网络调用异常: {}", e.getMessage());
throw e;
}
}