原创 五阳 2025-03-16 09:01 重庆
点击关注公众号,“技术干货” 及时达!
业务中台要接入很多的业务方,每个业务方并不是完全相同。很多时候无法完全复用,需要改造系统适应新的业务。
新增业务代码时,务必要保证原有业务不受影响,如果没有插件扩展点能力,就会充斥大量的 if else 。
if (biz == BizA || biz == BizB) {
//do some thing
//这部分逻辑相同
if (biz == BizA) {
//差异化处理
}
if(biz == BizB) {
//差异化逻辑
}
}
例如上面的代码,不同的业务线若有差异化逻辑,需要新增分支单独处理。想象一下,当有 10 多个业务接入了你的系统,那么一定让人抓狂……
任何一个人都无法保证对 10 多种业务完全熟悉,每个人可能只负责 1 个业务,然而如果没有代码逻辑的隔离,维护者只能在千丝万缕中,才能找到目标代码逻辑。更可怕的是,每次新增一个业务,需要在原有的屎山中继续💩,不断新增 if else。直到有一天,有一个倒霉蛋改错了代码,导致其他重要业务受影响,引发线上故障。
想象一下,当你改了几行代码以后,要求测试同学,回归10 多个业务线的全部逻辑?这显然不现实。
以上的问题和痛点可归纳为:代码隔离性和业务扩展点问题。解决这两类问题有如下手段!
「使用流程引擎,为不同的业务配置不同的流程执行链」
「使用插件扩展引擎,不同的业务实现差异化部分。」
MemberClub 中大量使用流程引擎和插件扩展引擎解决业务隔离性和扩展性 问题。
MemberClub是托管在Gitee平台的开源项目,提供了付费会员的交易解决方案,在各类购买场景下提供各类会员形态的履约及售后结算能力,具体介绍可参见 https://gitee.com/juejinwuyang/memberclub
在 程序员的保命技能——流程编排,你一定要了解!文章中,我介绍了流程引擎的设计原理,本篇文章我们分析 扩展点引擎设计。
从以下几个方面了解:扩展点接口的定义、扩展点实现类的定义、加载扩展点地图、引用和调用扩展点
定义扩展点
如下接口 PurchaseExtension 抽象了购买域 提交订单和取消订单接口,各产品线提供各自的实现类。实现类要添加 ExtensionProvider 注解,该注解声明了适用的业务线和业务场景。接口实现逻辑中共执行哪些流程。在 submit/cancel接口中 执行流程链。
扩展点接口定义
@ExtensionConfig(desc = "购买流程扩展点", type = ExtensionType.PURCHASE, must = true)
public interface PurchaseExtension extends BaseExtension {
public void submit(PurchaseSubmitContext context);// 提交订单
public void reverse(AfterSaleApplyContext context);//售后逆向
public void cancel(PurchaseCancelContext context);// 取消订单
}
ExtensionProvider 注解
该注解集成了 Service 注解,声明该注解会被加载进 Spring 上下文。同时注解信息包括业务线和业务场景值。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Service
public @interface ExtensionProvider {
public Route[] bizScenes();
public String desc();
}
扩展点实现类
加载扩展点
ExtensionManage 类在 Spring 启动阶段,从 ApplicationContext 上下文加载 有 ExtensionProvider 注解的修饰的 Bean。注解上声明了 适用的业务线和业务域,并且将以上信息 映射到 Table 中。Table 类是 guava 提供的类似于 HashMap 的工具类,和 Map 不同的是,获取 Table 中的 value 需要 key 和 subKey 两层映射。
在 Table 中,两种映射分别是业务线和 业务场景。
@Getter
private Table<BizTypeEnum, String, List<Object>> bizExtensionMeta = HashBasedTable.create();
@PostConstruct
public void init() {
String[] beanNames = context.getBeanNamesForAnnotation(ExtensionProvider.class);
for (String beanName : beanNames) {
Object bean = context.getBean(beanName);
Set<Class<?>> interfaces =
ClassUtils.getAllInterfacesForClassAsSet(bean.getClass());
ExtensionProvider extension = AnnotationUtils.findAnnotation(bean.getClass(), ExtensionProvider.class);
Route[] routes = extension.bizScenes();
for (Class<?> anInterface : interfaces) {
if (BaseExtension.class.isAssignableFrom(anInterface)) {
for (Route route : routes) {
for (SceneEnum scene : route.scenes()) {
String key = buildKey(anInterface, route.bizType().getCode(), scene.getValue());
Object value = extensionBeanMap.put(key, bean);
if (value != null) {
CommonLog.error("注册 Extension key:{}冲突", key);
throw new RuntimeException("注册 Extension 冲突");
}
CommonLog.info("注册 Extension key:{}, 接口:{}, 实现类:{}", key, anInterface.getSimpleName(), bean.getClass().getSimpleName());
List<Object> extensions = bizExtensionMeta.get(route.bizType(), anInterface.getSimpleName());
if (extensions == null) {
bizExtensionMeta.put(route.bizType(), anInterface.getSimpleName(), Lists.newArrayList(bean));
}
}
}
}
}
}
}
private String buildKey(Class<?> anInterface, int bizType, String scene) {
String key = String.format("%s_%s_%s", anInterface.getSimpleName(), bizType, scene);
return key;
}
以上代码地址在:Git 地址
引用扩展点
可通过 ExtensionManager.getExtension 方法引用扩展点。如下提单接口代码展示了 如何获取 PurchaseExtension 的实现类。
PurchaseExtension extension = extensionManager.getExtension(context.toDefaultBizScene(),PurchaseExtension.class);
extension.submit(context);
getExtension 方法中 将通过 产品线和产品域及 接口类,获取到实现类。
public <T> T getExtension(BizScene bizScene, Class<T> tClass) {
if (!tClass.isInterface()) {
throw new RuntimeException(String.format("%s 需要是一个接口", tClass.getSimpleName()));
}
if (!BaseExtension.class.isAssignableFrom(tClass)) {
throw new RuntimeException(String.format("%s 需要继承 BaseExtension 接口", tClass.getSimpleName()));
}
String key = buildKey(tClass, bizScene.getBizType(), bizScene.getScene());
T value = (T) extensionBeanMap.get(key);
if (value == null) {
key = buildKey(tClass, BizTypeEnum.DEFAULT.getCode(), SceneEnum.DEFAULT_SCENE.getValue());
value = (T) extensionBeanMap.get(key);
}
if (value == null) {
throw new RuntimeException(String.format("%s 没有找到实现类%s", tClass.getSimpleName(), bizScene.getKey()));
}
return value;
}
最后
MemberClub 中大量使用流程引擎和插件扩展引擎解决业务隔离性和扩展性 问题,以上代码均可以在 MemberClub项目中找到。代码地址:Git地址
点击关注公众号,“技术干货” 及时达!