扩展点设计

02_扩展点设计

一、业务举例

供应链的业务中,一个业务流程涉及到多个节点,并且每个节点的实现逻辑不同,如下图所示

image.png

每一个节点都可能存在不同的实现,有时候需要从多个实现中选择一个(互斥),有时候需要选择多个(组合)。如果不对各种实现进行良好的管理,带来的问题是:

  • 代码圈复杂度高。if-else,switch分支多,影响代码主干流程。阅读性差,新人学习成本高

  • 分支之间没有做隔离,改了一个地方可能影响其他分支

  • 随着时间推移,需求增多,代码越来越复杂,慢慢形成祖传代码,之前看到的一张图,就比较好的形容这种祖传代码

image.png

二、场景收集&分析

image.png
  1. 节点管理:节点管理本质上就是代码隔离,即将一个节点的不同实现分散到不同的类里面。

  2. 互斥:不同分支实现相互隔离,根据条件选择唯一的实现

  3. 组合:一个节点的多个实现同时执行

  4. 优先级管理:在组合模式下,调用节点的多个实现,但是实现有优先级顺序

  5. 中断策略:在组合模式下,调用节点的多个实现,根据节点返回结果判断是否继续向下执行

三、方案调研

(一) Java SPI调研

针对于上一节中提到的节点多种实现的问题,Java的SPI可以解决我们的问题。

image.png

Java SPI使用约定:

1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;

2、接口实现类所在的jar包放在主程序的classpath中;

3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;

4、SPI的实现类必须携带一个不带参数的构造方法;

(二) Cola 框架 & Halo框架调研

扩展点(ExtensionPoint)必须通过接口申明,扩展实现(Extension)是通过Annotation的方式标注的,Extension里面使用BizCode和TenantId两个属性用来标识身份,

框架的Bootstrap类会在Spring启动的时候做类扫描,进行Extension注册,在Runtime的时候,通过TenantContext来选择要使用的Extension。TenantContext是通过Interceptor在调用业务逻辑之前进行初始化的。整个过程如下图所示:

image.png

扩展点实现路由

image.png

比如在一个CRM系统里,客户要添加联系人Contact是一个,但是在添加联系人之前,我们要判断这个Contact是不是已经存在了,如果存在那么就不能添加了。不过在一个支持多业务的系统里面,可能每个业务的冲突检查都不一样,这是一个典型的可以扩展的场景。

那么在SOFA框架中,我们可以这样去做。

public interface ContactConflictRuleExtPt extends RuleI, ExtensionPointI {
   /**
    * 查询联系人冲突
    *
    * @param contact 冲突条件,不同业务会有不同的判断规则
    * @return 冲突结果
    */
   public boolean queryContactConflict(ContactE contact);
}

2、实现业务的扩展实现

@Extension(bizCode = BizCode.ICBU)
public class IcbuContactConflictRuleExt implements ContactConflictRuleExtPt {

    @Autowired
    private RepeatCheckServiceI repeatCheckService;
    @Autowired
    private MemberMappingQueryTunnel memberMappingQueryTunnel;
    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * 查询联系人冲突
     *
     * @param contact 冲突条件,不同业务会有不同的判断规则
     * @return 冲突结果
     */
    @Override
    public boolean queryContactConflict(ContactE contact) {

        Set<String> emails = contact.getEmail();

        //具体的业务逻辑
        
        return false;
    }

3、在领域实体中调用扩展实现

@ToString
@Getter
@Setter
public class CustomerE extends Entity {
    /**
     * 公司ID
     */
    private String companyId;
    /**
     * 公司(客户)名字
     */
    private String companyName;
    /**
     * 公司(客户)英文名字
     */
    private String companyNameEn;
          /**
     * 给客户添加联系人
     * @param contact
     */
    public void addContact(ContactE contact,boolean checkConflict){
        // 业务检查
        if (checkConflict) {
            ruleExecutor.execute(ContactConflictRuleExtPt.class, p -> p.queryContactConflict(contact));
        }
        contact.setCustomerId(this.getId());
        contactRepository.create(contact);
    }
}

(三) 我们对于扩展点的需求

image.png

cola扩展点的缺陷:

  • cola扩展点不支持组合场景

  • cola框架的Bootstrap类会在Spring启动的时候做类扫描,进行Extension注册,在Runtime的时候,通过TenantContext(身份标识信息)来选择要使用的Extension。TenantContext是通过Interceptor在调用业务逻辑之前进行初始化的,在供应链场景中,现在无法抽象出身份标识信息;或者执行扩展点的时候传参包含身份标识信息,如果业务场景比较复杂,构造身份标识信息会比较麻烦,因此考虑把扩展点的路由交个具体实现类处理,通过调用扩展点实现类的condition方案,判断是否执行该扩展点,扩展点实现:02_扩展点设计

三、业务扩展点使用

1、xml配置

<context:component-scan base-package="com.sankuai.sjst"/>

2、扩展点接口定义

扩展点必须以ExtPt结尾,通过ExtPt明显标识这是一个扩展点,扩展点实现类以Ext结尾

3、扩展点互斥场景实现

  • 定义业务扩展点接口
public interface AgreementGoodsBOBuilderExtPt extends ExtensionPointI<ScmIntelligentQueryGoodsContext, List<ScmPurchaseGoodsWithSuppliersBO>> {
}
  • 扩展点实现类-1
@Extension(name = "通过查询主数据es索引构建GoodsUnitBO")
public class AgreementGoodsBOBuilderByESQueryExt implements AgreementGoodsBOBuilderExtPt {
    @Resource
    private RemoteMainDataQueryService remoteMainDataQueryService;

    @Override
    public boolean condition(ScmIntelligentQueryGoodsContext context) {

        ScmIntelligentQueryGoodsConditionTO queryGoodsConditionTO = context.getQueryGoodsConditionTO();
        // GoodsUnitTO为空 且goodsIds不存在
        return queryGoodsConditionTO.getGoodsUnitTO() == null && queryGoodsConditionTO.getGoodsIdsSize() == 0;
    }

    @Override
    public List<ScmPurchaseGoodsWithSuppliersBO> invoke(ScmIntelligentQueryGoodsContext context) {
         // 业务逻辑
    }
}
  • 扩展点实现类-2
@Extension(name = "通过goodsIds参数构建GoodsUnitBO")
public class AgreementGoodsBOBuilderByGoodsIdsExt implements AgreementGoodsBOBuilderExtPt {
    // spring 依赖注入
    @Resource
    private RemoteBaseService remoteBaseService;

    @Override
    public boolean condition(ScmIntelligentQueryGoodsContext context) {
        List<Long> goodsIds = context.getQueryGoodsConditionTO().getGoodsIds();
        return CollectionUtils.isNotEmpty(goodsIds);
    }

    @Override
    public List<ScmPurchaseGoodsWithSuppliersBO> invoke(ScmIntelligentQueryGoodsContext context) {
        // 业务逻辑
    }
}
  • 调用扩展点
List<ScmPurchaseGoodsWithSuppliersBO> purchaseAgreementGoodsBOs = extensionExecutor.execute(AgreementGoodsBOBuilderExtPt.class, context);

4、扩展点组合+优先级管理 + 中断策略实现

  • 扩展点接口定义
/**
 * 智能采购物品协议校验扩展点
 */
public interface IntelligentPurchaseGoodsAgreementCheckExtPt extends ExtensionPointI<ScmIntelligentPurchaseCheckContext, ErrorItemAndStatus> {
}
  • 扩展点实现-1
@Order(1)
@Extension(name = "智能采购-配送中心配送物品校验")
public class DistributionGoodsAgreementCheckExtPt implements IntelligentPurchaseGoodsAgreementCheckExtPt {
    @Resource
    private ScmSupplierCheckService scmSupplierCheckService;

    @Override
    public boolean condition(ScmIntelligentPurchaseCheckContext context) {
        return CollectionUtils.isNotEmpty(context.getGoodsAndDistributionOrgBOs());
    }

    @Override
    public ErrorItemAndStatus invoke(ScmIntelligentPurchaseCheckContext checkContext) {
        // 业务逻辑
        return new ErrorItemAndStatus();
    }
}
  • 扩展点实现-2
@Order(2)
@Extension(name = "智能采购-供应商采购物品校验")
public class SupplierGoodsAgreementCheckExtPt implements IntelligentPurchaseGoodsAgreementCheckExtPt {
    @Override
    public boolean condition(ScmIntelligentPurchaseCheckContext context) {
        return CollectionUtils.isNotEmpty(context.getGoodsAndSupplierBOs());
    }

    @Override
    public ErrorItemAndStatus invoke(ScmIntelligentPurchaseCheckContext context) {
        // 业务逻辑
        return new ErrorItemAndStatus();
    }
}

  • 扩展点执行
// 智能采购-配送物品协议校验
List<ErrorItemAndStatus> errorItemAndStatuses =
        extensionExecutor.multiExecute(
                IntelligentPurchaseGoodsAgreementCheckExtPt.class,//扩展点接口
                intelligentPurchaseCheckContext,// 参数
                errorItemAndStatus -> ThriftStatusHelper.iserrorItemAndStatus.getStatus()));// 中断策略

四、业务扩展点原理

(一)、原理

image.png
  • spring在容器在启动的时候,会调用getBean方法实例化&初始化对象
public void refresh() throws BeansException, IllegalStateException {

        synchronized (this.startupShutdownMonitor) {

            // Prepare this context for refreshing.

            prepareRefresh();

            // Tell the subclass to refresh the internal bean factory.

            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

            // Prepare the bean factory for use in this context.

            prepareBeanFactory(beanFactory);

            try {

                // Allows post-processing of the bean factory in context subclasses.

                postProcessBeanFactory(beanFactory);

                // 调用 factory processors registered as beans in the context.

                invokeBeanFactoryPostProcessors(beanFactory);

                // Register bean processors that intercept bean creation.

                registerBeanPostProcessors(beanFactory);

                // Initialize message source for this context.

                initMessageSource();

                // Initialize event multicaster for this context.

                initApplicationEventMulticaster();

                // Initialize other special beans in specific context subclasses.

                onRefresh();

                // Check for listener beans and register them.

                registerListeners();

                // 初始化所有单例对象

                finishBeanFactoryInitialization(beanFactory);

                // Last step: publish corresponding event.

                finishRefresh();

            }catch (BeansException ex) {

                if (logger.isWarnEnabled()) {

                    logger.warn("Exception encountered during context initialization - " +

                            "cancelling refresh attempt: " + ex);

                }

                // Destroy already created singletons to avoid dangling resources.

                destroyBeans();

                // Reset 'active' flag.

                cancelRefresh(ex);

                // Propagate exception to caller.

                throw ex;

            }finally {

                // Reset common introspection caches in Spring's core, since we

                // might not ever need metadata for singleton beans anymore...

                resetCommonCaches();

            }

        }

    }
  • 初始化过程中会执行spring开发出来的扩展点,我们的业务扩展点框架实现了BeanPostProcessor接口,判断对象的class是否有Extension注解,如果存在组件,将对象添加到ExtensionRepository中,其内部接口是Map<String, List<ExtensionPointI>>结果,key是扩展点接口的类名称,value是实现类列表

  • 当要执行扩展点时,通过调用ExtensionExecutor.execute方法,实现选择一个扩展点实现类,来进行调用;调用ExtensionExecutor.multiExecute方法,按扩展点实现类的优先级先后进行调用,如果设置了中断策略,在执行下一个扩展点实现类之前会先判断是否中断

(二)、核心模型

1、扩展点接口:

/**
 * ExtensionPointI is the parent interface of all ExtensionPoints
 * 扩展点表示一块逻辑在不同的业务有不同的实现,使用扩展点做接口申明,然后用Extension(扩展)去实现扩展点。
 *
 * @author heyong04
 */
public interface ExtensionPointI<T, R> {

    /**
     * 是否执行当前实现的条件
     *
     * @param context 调用上下文
     * @return 是否满足条件
     */
    boolean condition(T context);

    /**
     * 扩展点实现的具体操作
     *
     * @param context 调用上下文
     * @return 执行结果
     */
    R invoke(T context);
}

2、扩展点注解

用在扩展点实现类上,使用该注解,会将实现类注入到spring容器中

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Component
public @interface Extension {
    String name() default "";
}

3、Spring BeanPostProcessor扩展点实现

package com.sankuai.sjst.scm.extension.register;

import com.sankuai.sjst.scm.constant.ExtensionConstant;
import com.sankuai.sjst.scm.exception.ExtensionException;
import com.sankuai.sjst.scm.extension.Extension;
import com.sankuai.sjst.scm.extension.ExtensionPointI;
import com.sankuai.sjst.scm.extension.RegisterI;
import com.sankuai.sjst.scm.extension.repository.ExtensionRepository;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;

import java.util.Objects;
import java.util.concurrent.ConcurrentSkipListSet;

/**
 * ExtensionRegister
 *
 * @author heyong
 */
@Component
public class ExtensionRegister implements RegisterI, BeanPostProcessor {
    // 防止bean重复添加到ExtensionRepository
    private static final ConcurrentSkipListSet<String> EXTENSION_BEAN_NAME_SET = new ConcurrentSkipListSet<>();

    @Autowired
    private ExtensionRepository extensionRepository;

    @Override
    public void doRegistration(Class<?> clazz, ExtensionPointI extensionPointI) {
        Class<? extends ExtensionPointI> extPtClass = calculateExtensionPoint(clazz);
        extensionRepository.put(extPtClass, extensionPointI);
    }

    /**
     * @param targetClz 子类
     * @return
     */
    private Class<? extends ExtensionPointI> calculateExtensionPoint(Class<?> targetClz) {

        Class[] interfaces = targetClz.getInterfaces();
        if (ArrayUtils.isEmpty(interfaces)) {
            throw new ExtensionException("Please assign a extension point interface for " + targetClz);
        }

        for (Class iface : interfaces) {
            String extensionPoint = iface.getSimpleName();
            if (StringUtils.contains(extensionPoint, ExtensionConstant.EXTENSION_EXTPT_NAMING)) {
                return iface;
            }
        }
        throw new ExtensionException("Your name of ExtensionPoint for " + targetClz + " is not valid, must be end of " + ExtensionConstant.EXTENSION_EXTPT_NAMING);
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        // 已经处理过的扩展点类,不需要处理
        if (EXTENSION_BEAN_NAME_SET.contains(beanName)) {
            return bean;
        }

        Class<?> targetClass = AopUtils.getTargetClass(bean);
        Extension extension = AnnotationUtils.findAnnotation(targetClass, Extension.class);
        if (Objects.nonNull(extension)) {
            EXTENSION_BEAN_NAME_SET.add(beanName);
            doRegistration(targetClass, (ExtensionPointI) bean);
        }
        return bean;
    }

}

4、扩展点执行器

/**
 * <p>扩展点抽象执行器</p>
 *
 * @author heyong04@meituan.com
 * @version AbstractComponentExecutor.class 2020-09-14 上午11:33
 * @since 1.0.0
 **/
public abstract class AbstractComponentExecutor {

    /**
     * Execute extension with Response
     *
     * @param targetClz 扩展点接口定义
     * @param context   扩展点上下文信息
     * @param <R>       扩展点接口入参类型
     * @param <T>       扩展点接口出参类型
     * @return 执行结果
     */
    public <R, T> R execute(Class<? extends ExtensionPointI<T, R>> targetClz, T context) {
        ExtensionPointI extensionPointI = locateComponent(targetClz, context);
        return (R) extensionPointI.invoke(context);
    }

    /**
     * Multi Execute extension with Response
     *
     * @param targetClz 扩展点接口
     * @param context   扩展点上下文信息
     * @param <R>       扩展点接口入参类型
     * @param <T>       扩展点接口出参类型
     * @return 执行结果, 使用list包装了每个扩展点实现的返回值
     */
    public <R, T> List<R> multiExecute(Class<? extends ExtensionPointI<T, R>> targetClz, T context) {
        return multiExecute(targetClz, context, new DefaultInterruptionStrategy<>());
    }

    /**
     * Multi Execute extension with Response
     *
     * @param targetClz            扩展点接口
     * @param context              扩展点上下文信息
     * @param <R>                  扩展点接口入参类型
     * @param <T>                  扩展点接口出参类型
     * @param interruptionStrategy 中断策略
     * @return 执行结果, 使用list包装了每个扩展点实现的返回值
     */
    public <R, T> List<R> multiExecute(Class<? extends ExtensionPointI<T, R>> targetClz, T context, InterruptionStrategy<R> interruptionStrategy) {
        List<ExtensionPointI> extensionPointIs = locateComponents(targetClz, context);

        List<R> combinationResult = Lists.newArrayListWithExpectedSize(extensionPointIs.size());
        for (ExtensionPointI extensionPointI : extensionPointIs) {
            R result = (R) extensionPointI.invoke(context);
            combinationResult.add(result);
            if (interruptionStrategy.interrupt(result)) {
                return combinationResult;
            }
        }

        return combinationResult;
    }

    /**
     * 加载扩展实现
     *
     * @param targetClz 扩展点接口
     * @param context   扩展点上下文信息
     * @param <T>       扩展点接口入参类型
     * @param <R>       扩展点接口出参类型
     * @return 扩展点实现
     */
    abstract <T, R> ExtensionPointI locateComponent(Class<? extends ExtensionPointI<T, R>> targetClz, T context);

    /**
     * 加载多个扩展点实现
     *
     * @param <T>       扩展点接口入参类型
     * @param <R>       扩展点接口出参类型
     * @param targetClz 扩展点接口
     * @param context   扩展点接口入参
     * @return 扩展点实现列表
     */
    abstract <T, R> List<ExtensionPointI> locateComponents(Class<? extends ExtensionPointI<T, R>> targetClz, T context);
}

5、中断策略

/**
 * <p>扩展点执行中断策略接口</p>
 *
 * @author heyong04@meituan.com
 * @version InterruptionStrategy.class 2020-09-14 上午11:33
 * @since 1.0.0
 **/
public interface InterruptionStrategy<R> {
    /**
     * 是否中断执行
     *
     * @param extensionPointResult 扩展点执行返回结果
     * @return
     */
    boolean interrupt(R extensionPointResult);
}

五、使用规范

image.png

六、扩展点相对于策略模式优势

1、基于Strategy Pattern的扩展,没有找到一个很好的固化到框架中的方法

2、使用Strategy Pattern,没有规范的限制,编码相对随意

七、参考文档

https://blog.csdn.net/significantfrank/article/details/85785565

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270