02_擴展點設(shè)計
一晚树、業(yè)務(wù)舉例
供應(yīng)鏈的業(yè)務(wù)中译秦,一個業(yè)務(wù)流程涉及到多個節(jié)點两残,并且每個節(jié)點的實現(xiàn)邏輯不同裤纹,如下圖所示
每一個節(jié)點都可能存在不同的實現(xiàn)委刘,有時候需要從多個實現(xiàn)中選擇一個(互斥),有時候需要選擇多個(組合)鹰椒。如果不對各種實現(xiàn)進行良好的管理锡移,帶來的問題是:
代碼圈復(fù)雜度高。if-else漆际,switch分支多罩抗,影響代碼主干流程。閱讀性差灿椅,新人學(xué)習(xí)成本高
分支之間沒有做隔離套蒂,改了一個地方可能影響其他分支
隨著時間推移,需求增多茫蛹,代碼越來越復(fù)雜操刀,慢慢形成祖?zhèn)鞔a,之前看到的一張圖婴洼,就比較好的形容這種祖?zhèn)鞔a
二骨坑、場景收集&分析
節(jié)點管理:節(jié)點管理本質(zhì)上就是代碼隔離,即將一個節(jié)點的不同實現(xiàn)分散到不同的類里面柬采。
互斥:不同分支實現(xiàn)相互隔離欢唾,根據(jù)條件選擇唯一的實現(xiàn)
組合:一個節(jié)點的多個實現(xiàn)同時執(zhí)行
優(yōu)先級管理:在組合模式下,調(diào)用節(jié)點的多個實現(xiàn)粉捻,但是實現(xiàn)有優(yōu)先級順序
中斷策略:在組合模式下礁遣,調(diào)用節(jié)點的多個實現(xiàn),根據(jù)節(jié)點返回結(jié)果判斷是否繼續(xù)向下執(zhí)行
三肩刃、方案調(diào)研
(一) Java SPI調(diào)研
針對于上一節(jié)中提到的節(jié)點多種實現(xiàn)的問題祟霍,Java的SPI可以解決我們的問題。
Java SPI使用約定:
1盈包、當(dāng)服務(wù)提供者提供了接口的一種具體實現(xiàn)后沸呐,在jar包的META-INF/services目錄下創(chuàng)建一個以“接口全限定名”為命名的文件,內(nèi)容為實現(xiàn)類的全限定名呢燥;
2崭添、接口實現(xiàn)類所在的jar包放在主程序的classpath中;
3叛氨、主程序通過java.util.ServiceLoder動態(tài)裝載實現(xiàn)模塊呼渣,它通過掃描META-INF/services目錄下的配置文件找到實現(xiàn)類的全限定名根暑,把類加載到JVM;
4徙邻、SPI的實現(xiàn)類必須攜帶一個不帶參數(shù)的構(gòu)造方法排嫌;
(二) Cola 框架 & Halo框架調(diào)研
擴展點(ExtensionPoint)必須通過接口申明,擴展實現(xiàn)(Extension)是通過Annotation的方式標(biāo)注的缰犁,Extension里面使用BizCode和TenantId兩個屬性用來標(biāo)識身份淳地,
框架的Bootstrap類會在Spring啟動的時候做類掃描,進行Extension注冊帅容,在Runtime的時候颇象,通過TenantContext來選擇要使用的Extension。TenantContext是通過Interceptor在調(diào)用業(yè)務(wù)邏輯之前進行初始化的并徘。整個過程如下圖所示:
擴展點實現(xiàn)路由
比如在一個CRM系統(tǒng)里遣钳,客戶要添加聯(lián)系人Contact是一個,但是在添加聯(lián)系人之前麦乞,我們要判斷這個Contact是不是已經(jīng)存在了蕴茴,如果存在那么就不能添加了。不過在一個支持多業(yè)務(wù)的系統(tǒng)里面姐直,可能每個業(yè)務(wù)的沖突檢查都不一樣倦淀,這是一個典型的可以擴展的場景。
那么在SOFA框架中声畏,我們可以這樣去做撞叽。
public interface ContactConflictRuleExtPt extends RuleI, ExtensionPointI {
/**
* 查詢聯(lián)系人沖突
*
* @param contact 沖突條件,不同業(yè)務(wù)會有不同的判斷規(guī)則
* @return 沖突結(jié)果
*/
public boolean queryContactConflict(ContactE contact);
}
2插龄、實現(xiàn)業(yè)務(wù)的擴展實現(xiàn)
@Extension(bizCode = BizCode.ICBU)
public class IcbuContactConflictRuleExt implements ContactConflictRuleExtPt {
@Autowired
private RepeatCheckServiceI repeatCheckService;
@Autowired
private MemberMappingQueryTunnel memberMappingQueryTunnel;
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 查詢聯(lián)系人沖突
*
* @param contact 沖突條件愿棋,不同業(yè)務(wù)會有不同的判斷規(guī)則
* @return 沖突結(jié)果
*/
@Override
public boolean queryContactConflict(ContactE contact) {
Set<String> emails = contact.getEmail();
//具體的業(yè)務(wù)邏輯
return false;
}
3、在領(lǐng)域?qū)嶓w中調(diào)用擴展實現(xiàn)
@ToString
@Getter
@Setter
public class CustomerE extends Entity {
/**
* 公司ID
*/
private String companyId;
/**
* 公司(客戶)名字
*/
private String companyName;
/**
* 公司(客戶)英文名字
*/
private String companyNameEn;
/**
* 給客戶添加聯(lián)系人
* @param contact
*/
public void addContact(ContactE contact,boolean checkConflict){
// 業(yè)務(wù)檢查
if (checkConflict) {
ruleExecutor.execute(ContactConflictRuleExtPt.class, p -> p.queryContactConflict(contact));
}
contact.setCustomerId(this.getId());
contactRepository.create(contact);
}
}
(三) 我們對于擴展點的需求
cola擴展點的缺陷:
cola擴展點不支持組合場景
cola框架的Bootstrap類會在Spring啟動的時候做類掃描均牢,進行Extension注冊糠雨,在Runtime的時候,通過TenantContext(身份標(biāo)識信息)來選擇要使用的Extension膨处。TenantContext是通過Interceptor在調(diào)用業(yè)務(wù)邏輯之前進行初始化的见秤,在供應(yīng)鏈場景中,現(xiàn)在無法抽象出身份標(biāo)識信息真椿;或者執(zhí)行擴展點的時候傳參包含身份標(biāo)識信息,如果業(yè)務(wù)場景比較復(fù)雜乎澄,構(gòu)造身份標(biāo)識信息會比較麻煩突硝,因此考慮把擴展點的路由交個具體實現(xiàn)類處理,通過調(diào)用擴展點實現(xiàn)類的condition方案置济,判斷是否執(zhí)行該擴展點解恰,擴展點實現(xiàn):02_擴展點設(shè)計
三锋八、業(yè)務(wù)擴展點使用
1、xml配置
<context:component-scan base-package="com.sankuai.sjst"/>
2护盈、擴展點接口定義
擴展點必須以ExtPt結(jié)尾挟纱,通過ExtPt明顯標(biāo)識這是一個擴展點,擴展點實現(xiàn)類以Ext結(jié)尾
3、擴展點互斥場景實現(xiàn)
- 定義業(yè)務(wù)擴展點接口
public interface AgreementGoodsBOBuilderExtPt extends ExtensionPointI<ScmIntelligentQueryGoodsContext, List<ScmPurchaseGoodsWithSuppliersBO>> {
}
- 擴展點實現(xiàn)類-1
@Extension(name = "通過查詢主數(shù)據(jù)es索引構(gòu)建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) {
// 業(yè)務(wù)邏輯
}
}
- 擴展點實現(xiàn)類-2
@Extension(name = "通過goodsIds參數(shù)構(gòu)建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) {
// 業(yè)務(wù)邏輯
}
}
- 調(diào)用擴展點
List<ScmPurchaseGoodsWithSuppliersBO> purchaseAgreementGoodsBOs = extensionExecutor.execute(AgreementGoodsBOBuilderExtPt.class, context);
4腐宋、擴展點組合+優(yōu)先級管理 + 中斷策略實現(xiàn)
- 擴展點接口定義
/**
* 智能采購物品協(xié)議校驗擴展點
*/
public interface IntelligentPurchaseGoodsAgreementCheckExtPt extends ExtensionPointI<ScmIntelligentPurchaseCheckContext, ErrorItemAndStatus> {
}
- 擴展點實現(xiàn)-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) {
// 業(yè)務(wù)邏輯
return new ErrorItemAndStatus();
}
}
- 擴展點實現(xiàn)-2
@Order(2)
@Extension(name = "智能采購-供應(yīng)商采購物品校驗")
public class SupplierGoodsAgreementCheckExtPt implements IntelligentPurchaseGoodsAgreementCheckExtPt {
@Override
public boolean condition(ScmIntelligentPurchaseCheckContext context) {
return CollectionUtils.isNotEmpty(context.getGoodsAndSupplierBOs());
}
@Override
public ErrorItemAndStatus invoke(ScmIntelligentPurchaseCheckContext context) {
// 業(yè)務(wù)邏輯
return new ErrorItemAndStatus();
}
}
- 擴展點執(zhí)行
// 智能采購-配送物品協(xié)議校驗
List<ErrorItemAndStatus> errorItemAndStatuses =
extensionExecutor.multiExecute(
IntelligentPurchaseGoodsAgreementCheckExtPt.class,//擴展點接口
intelligentPurchaseCheckContext,// 參數(shù)
errorItemAndStatus -> ThriftStatusHelper.iserrorItemAndStatus.getStatus()));// 中斷策略
四紊服、業(yè)務(wù)擴展點原理
(一)、原理
- spring在容器在啟動的時候胸竞,會調(diào)用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);
// 調(diào)用 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();
}
}
}
初始化過程中會執(zhí)行spring開發(fā)出來的擴展點欺嗤,我們的業(yè)務(wù)擴展點框架實現(xiàn)了BeanPostProcessor接口,判斷對象的class是否有Extension注解卫枝,如果存在組件煎饼,將對象添加到ExtensionRepository中,其內(nèi)部接口是Map<String, List<ExtensionPointI>>結(jié)果校赤,key是擴展點接口的類名稱吆玖,value是實現(xiàn)類列表
當(dāng)要執(zhí)行擴展點時,通過調(diào)用ExtensionExecutor.execute方法马篮,實現(xiàn)選擇一個擴展點實現(xiàn)類衰伯,來進行調(diào)用;調(diào)用ExtensionExecutor.multiExecute方法积蔚,按擴展點實現(xiàn)類的優(yōu)先級先后進行調(diào)用意鲸,如果設(shè)置了中斷策略,在執(zhí)行下一個擴展點實現(xiàn)類之前會先判斷是否中斷
(二)尽爆、核心模型
1怎顾、擴展點接口:
/**
* ExtensionPointI is the parent interface of all ExtensionPoints
* 擴展點表示一塊邏輯在不同的業(yè)務(wù)有不同的實現(xiàn),使用擴展點做接口申明漱贱,然后用Extension(擴展)去實現(xiàn)擴展點槐雾。
*
* @author heyong04
*/
public interface ExtensionPointI<T, R> {
/**
* 是否執(zhí)行當(dāng)前實現(xiàn)的條件
*
* @param context 調(diào)用上下文
* @return 是否滿足條件
*/
boolean condition(T context);
/**
* 擴展點實現(xiàn)的具體操作
*
* @param context 調(diào)用上下文
* @return 執(zhí)行結(jié)果
*/
R invoke(T context);
}
2、擴展點注解
用在擴展點實現(xiàn)類上幅狮,使用該注解募强,會將實現(xiàn)類注入到spring容器中
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Component
public @interface Extension {
String name() default "";
}
3、Spring BeanPostProcessor擴展點實現(xiàn)
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重復(fù)添加到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 {
// 已經(jīng)處理過的擴展點類崇摄,不需要處理
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擎值、擴展點執(zhí)行器
/**
* <p>擴展點抽象執(zhí)行器</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> 擴展點接口入?yún)㈩愋? * @param <T> 擴展點接口出參類型
* @return 執(zhí)行結(jié)果
*/
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> 擴展點接口入?yún)㈩愋? * @param <T> 擴展點接口出參類型
* @return 執(zhí)行結(jié)果, 使用list包裝了每個擴展點實現(xiàn)的返回值
*/
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> 擴展點接口入?yún)㈩愋? * @param <T> 擴展點接口出參類型
* @param interruptionStrategy 中斷策略
* @return 執(zhí)行結(jié)果, 使用list包裝了每個擴展點實現(xiàn)的返回值
*/
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;
}
/**
* 加載擴展實現(xiàn)
*
* @param targetClz 擴展點接口
* @param context 擴展點上下文信息
* @param <T> 擴展點接口入?yún)㈩愋? * @param <R> 擴展點接口出參類型
* @return 擴展點實現(xiàn)
*/
abstract <T, R> ExtensionPointI locateComponent(Class<? extends ExtensionPointI<T, R>> targetClz, T context);
/**
* 加載多個擴展點實現(xiàn)
*
* @param <T> 擴展點接口入?yún)㈩愋? * @param <R> 擴展點接口出參類型
* @param targetClz 擴展點接口
* @param context 擴展點接口入?yún)? * @return 擴展點實現(xiàn)列表
*/
abstract <T, R> List<ExtensionPointI> locateComponents(Class<? extends ExtensionPointI<T, R>> targetClz, T context);
}
5、中斷策略
/**
* <p>擴展點執(zhí)行中斷策略接口</p>
*
* @author heyong04@meituan.com
* @version InterruptionStrategy.class 2020-09-14 上午11:33
* @since 1.0.0
**/
public interface InterruptionStrategy<R> {
/**
* 是否中斷執(zhí)行
*
* @param extensionPointResult 擴展點執(zhí)行返回結(jié)果
* @return
*/
boolean interrupt(R extensionPointResult);
}
五逐抑、使用規(guī)范
六鸠儿、擴展點相對于策略模式優(yōu)勢
1、基于Strategy Pattern的擴展,沒有找到一個很好的固化到框架中的方法
2进每、使用Strategy Pattern汹粤,沒有規(guī)范的限制,編碼相對隨意
七田晚、參考文檔
https://blog.csdn.net/significantfrank/article/details/85785565