稀土掘金技术社区 04月09日
程序员的保命技能——插件扩展点引擎,你必须要了解~
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文深入探讨了业务中台在接入多业务时,如何通过扩展点引擎解决代码隔离和业务扩展问题。文章以MemberClub项目为例,详细介绍了扩展点接口的定义、实现类的定义、加载机制以及引用和调用方式,强调了流程引擎和插件扩展引擎在解决业务隔离和扩展性问题中的关键作用。通过这种设计,系统能够更灵活地适应不同业务的需求,避免了if-else的堆积,提高了代码的可维护性和可扩展性。

💡 **扩展点接口定义:** 定义了如PurchaseExtension等抽象接口,规定了提交订单、取消订单等核心操作,为不同业务线提供了统一的入口和规范。

🧩 **扩展点实现类:** 扩展点实现类使用`ExtensionProvider`注解,声明了适用的业务线和业务场景,并注入Spring容器,实现了接口的具体业务逻辑。

⚙️ **扩展点加载机制:** 在Spring启动时,系统会扫描并加载带有`ExtensionProvider`注解的Bean,并将业务线、业务场景等信息映射到Table中,构建扩展点地图,实现扩展点的注册。

🚀 **扩展点引用和调用:** 通过`ExtensionManager.getExtension`方法引用扩展点,根据业务线、业务场景和接口类,获取对应的实现类,实现动态调用,保证了代码的灵活性和可扩展性。

✅ **核心价值:** 流程引擎和插件扩展引擎结合,解决了业务隔离性和扩展性问题,避免了大量if-else,提高了代码的可维护性,使得系统能够更好地适应不同业务需求。

原创 五阳 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)@Servicepublic @interface ExtensionProvider {
public Route[] bizScenes();

public String desc();}

扩展点实现类


加载扩展点

ExtensionManage 类在 Spring 启动阶段,从 ApplicationContext 上下文加载 有 ExtensionProvider  注解的修饰的 Bean。注解上声明了 适用的业务线和业务域,并且将以上信息 映射到  Table 中。Table 类是 guava 提供的类似于 HashMap 的工具类,和 Map 不同的是,获取 Table 中的 value 需要 key 和 subKey 两层映射。

在 Table 中,两种映射分别是业务线和 业务场景。

@Getterprivate Table<BizTypeEnum, String, List<Object>> bizExtensionMeta = HashBasedTable.create();
@PostConstructpublic 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地址

点击关注公众号,“技术干货” 及时达!

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

扩展点引擎 业务中台 代码隔离 可扩展性
相关文章