程序员的保命技能——插件扩展点引擎,你必须要了解~

1,118 阅读7分钟

业务中台要接入很多的业务方,每个业务方并不是完全相同。很多时候无法完全复用,需要改造系统适应新的业务。

新增业务代码时,务必要保证原有业务不受影响,如果没有插件扩展点能力,就会充斥大量的 if else 。

if (biz == BizA || biz == BizB) {
     //do some thing
     //这部分逻辑相同
     if (biz == BizA) {
         //差异化处理
     }
     
     if(biz == BizB) {
        //差异化逻辑
     }
}

例如上面的代码,不同的业务线若有差异化逻辑,需要新增分支单独处理。想象一下,当有 10 多个业务接入了你的系统,那么一定让人抓狂……

任何一个人都无法保证对 10 多种业务完全熟悉,每个人可能只负责 1 个业务,然而如果没有代码逻辑的隔离,维护者只能在千丝万缕中,才能找到目标代码逻辑。更可怕的是,每次新增一个业务,需要在原有的屎山中继续💩,不断新增 if else。直到有一天,有一个倒霉蛋改错了代码,导致其他重要业务受影响,引发线上故障。

想象一下,当你改了几行代码以后,要求测试同学,回归10 多个业务线的全部逻辑?这显然不现实。

以上的问题和痛点可归纳为:代码隔离性和业务扩展点问题。解决这两类问题有如下手段!

  1. 使用流程引擎,为不同的业务配置不同的流程执行链
  2. 使用插件扩展引擎,不同的业务实现差异化部分。

MemberClub 中大量使用流程引擎和插件扩展引擎解决业务隔离性和扩展性 问题。

MemberClub是托管在Gitee平台的开源项目,提供了付费会员的交易解决方案,在各类购买场景下提供各类会员形态的履约及售后结算能力,具体介绍可参见 gitee.com/juejinwuyan…

在 程序员的保命技能——流程编排,你一定要了解!文章中,我介绍了流程引擎的设计原理,本篇文章我们分析 扩展点引擎设计。

从以下几个方面了解:扩展点接口的定义、扩展点实现类的定义、加载扩展点地图、引用和调用扩展点

定义扩展点

如下接口 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();
}

扩展点实现类

image.png

image.png

加载扩展点

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地址

我的开源项目

最后夹带一点私货,五阳最近花了3个月的时间完成一个开源项目。

开源3周以来,已有近 200 多个关注和Fork

Gitee:gitee.com/juejinwuyan…

GitHub github.com/juejin-wuya…

开源平台上有很多在线商城系统,功能很全,很完善,关注者众多,然而实际业务场景非常复杂和多样化,开源的在线商城系统很难完全匹配实际业务,广泛的痛点是

  • 功能堆砌,大部分功能用不上,需要大量裁剪;
  • 逻辑差异点较多,需要大量修改;
  • 功能之间耦合,难以独立替换某个功能。

由于技术中间件功能诉求较为一致,使用者无需过多定制化,技术中间件开源项目以上的痛点不明显,然而电商交易等业务系统虽然通用性较多,但各行业各产品的业务差异化极大,所以导致以上痛点比较明显

所以我在思考,有没有一个开源系统,能提供电商交易的基础能力,能让开发者搭积木的方式,快速搭建一个完全契合自己业务的新系统呢?

  • 他们可以通过编排和配置选择自己需要的功能,而无需在一个现成的开源系统上进行裁剪
  • 他们可以轻松的新增扩展业务的差异化逻辑,不需要阅读然后修改原有的系统代码!
  • 他们可以轻松的替换掉他们认为垃圾的、多余的系统组件,而不需要考虑其他功能是否会收到影响

开发者们,可以择需选择需要的能力组件,组件中差异化的部分有插件扩展点能轻松扩展。或者能支持开发者快速的重新写一个完全适合自己的新组件然后编排注册到系统中?

memberclub 就是基于这样的想法而设计的。 它的定位是电商类交易系统工具箱, 以SDK方式对外提供通用的交易能力,能让开发者像搭积木方式,从0到1,快速构建一个新的电商交易系统!

image.png

具体介绍可参见

Gitee开源地址gitee.com/juejinwuyan…

GitHub开源地址 : github.com/juejin-wuya…

在这个项目中你可以学习到 SpringBoot 集成 以下框架或组件。

  1. Mybatis、Mybatis-plus 集成多数据源
  2. Sharding-jdbc 多数据源分库分表
  3. redis/redisson 缓存
  4. Apollo 分布式配置中心
  5. Spring Cloud 微服务全家桶
  6. RabbitMq 消息队列
  7. H2 内存数据库
  8. Swagger + Lombok + MapStruct

同时你也可以学习到以下组件的实现原理

  1. 流程引擎的实现原理
  2. 扩展点引擎实现原理
  3. 分布式重试组件实现原理
  4. 通用日志组件实现原理 参考:juejin.cn/post/740727…
  5. 商品库存实现原理: 参考:juejin.cn/post/731377…
  6. 分布式锁组件: 参考:
  7. Redis Lua的使用
  8. Spring 上下文工具类 参考: juejin.cn/post/746927…