通过上面的文章我们已经充分了解了业务身份、扩展点的理念。
那么如果我们要设计一个框架,支持扩展点机制,应该怎么做?假设我们的扩展点方案建设在Spring框架之上。
业界已经有较多的类似实现,例如COLA框架,为了通用将业务身份抽象为(业务,用例,场景)。
但是,对于一些**对外多渠道提供服务的平台,既包含自营的前端,又可能通过API开发接口对外服务的平台,**这三个维度并不够。
1
业务身份建模
根据平台对待第三方的设计原则,我们根据上一篇文章中对于业务身份的模型定义,我们采用的业务身份模型如下:(业务,产品,客群,渠道,用例,场景)。其中业务、用例、场景是采用了COLA的设计思路。
业务身份需要在后台交易中进行识别,所以这些要素一定是能够获取到的。
-
业务(Business):
-
即垂直业务领域,阿里体系下可能是淘宝、天猫,我们的粒度可以小一点,在商业银行对公业务体系内,可以按照供应链金融、保理、现金管理等产品条线来划分,因为这些产品线内容一般也按照不同的部门来管理。
-
产品(Product):
-
即某个业务领域下的具体产品分类,例如保理业务下的一些具体业务类型。当然不同的产品实际上还可能有不同的服务模式,但是不同的服务模式,可以通过客群来进行区分,不再单独拿出来产品模式作为具体的一个业务身份要素。
-
客群(Customer):
-
与零售企业不同,企业对于对公类客户都是分层分类经营,某些产品可能需要对不同的客群不同的服务方式,这里一般是站在内部企业对客群划分的视角来定义,例如战略客群、基础客群、小微客群等。也可以具体给定某个大型企业在企业内部的编号,以作具体的分类处理。
-
渠道(Channel):
-
平台化发展到一定程度,需要与外部平台互相嵌入嵌出服务,互为生态组成部分,那么对于不同的渠道(即外部直连平台),可能有不同的控制,例如一系列外部平台的名字:找钢网等。
-
用例(Use Case):
-
描述了用户(外部平台系统)和内部系统之间的互动,每个用例提供了一个或多个场景。比如,零售领域中支付订单就是一个典型的用例。而对公业务领域中,融资申请又是一个典型业务用例。
-
场景(Scenario):
-
场景也被称为用例的实例(Instance),包括用例所有的可能情况(正常的和异常的)。比如对于零售领域“订单支付”这个用例,就有“可以使用花呗”,“支付宝余额不足”,“银行账户余额不足”等多个场景。
首先我们考虑一下设计框架需要考虑的内容,参照COLA框架的实现,适合一些对外多渠道服务多种客群的平台,既包含自营的前端,又可能通过API开发接口对外服务,从平等对待第三方的角度来看整个设计。
package com.tff.extension;
public class BizScenario {
public final static String DEFAULT_BIZ_ID = "#defaultBizId#";
public final static String DEFAULT_PRODUCT_NUM = "#defaultProductNum#";
public final static String DEFAULT_CUSTOMER = "#defaultCustomer#";
public final static String DEFAULT_CHANNEL = "#defaultChannel#";
public final static String DEFAULT_USE_CASE = "#defaultUseCase#";
public final static String DEFAULT_SCENARIO = "#defaultScenario#";
private final static String DOT_SEPARATOR = ".";
private String bizId = DEFAULT_BIZ_ID;
private String productNum = DEFAULT_PRODUCT_NUM;
private String customer = DEFAULT_CUSTOMER;
private String channel = DEFAULT_CHANNEL;
private String useCase = DEFAULT_USE_CASE;
private String scenario = DEFAULT_SCENARIO;
public static BizScenario valueOf(String bizId, String productNum, String customer, String channel, String useCase, String scenario){
BizScenario bizScenario = new BizScenario();
bizScenario.bizId = bizId;
bizScenario.productNum = productNum;
bizScenario.customer = customer;
bizScenario.channel = channel;
bizScenario.useCase = useCase;
bizScenario.scenario = scenario;
return bizScenario;
}
public String getUniqueIdentity() {
return bizId + DOT_SEPARATOR + productNum + DOT_SEPARATOR + customer + DOT_SEPARATOR + channel + DOT_SEPARATOR + useCase + DOT_SEPARATOR + scenario;
}
}
2
扩展点注解
-
扩展点注解
-
首先,我们要一个扩展点实现的注解:@Extension。用来指明某个类是某个扩展点坐标(业务身份,扩展点)的可选实现,这个接口需要实现ExtentionPoint接口。还可以实现一个@Extensions,用来支持多个扩展点坐标。
其次,需要对所有的扩展点实现进行统一的注册管理。这里我们直接使用Spring的bean管理,然后在实现@Extension的时候,直接去在注解上面添加@Component,表示是一个Spring组件。
package com.tff.extension;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.stereotype.Component;
@Component
@Inherited
@Repeatable(Extensions.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Extension {
String bizId() default BizScenario.DEFAULT_BIZ_ID;
String productNum() default BizScenario.DEFAULT_PRODUCT_NUM;
String customer() default BizScenario.DEFAULT_CUSTOMER;
String channel() default BizScenario.DEFAULT_CHANNEL;
String useCase() default BizScenario.DEFAULT_USE_CASE;
String scenario() default BizScenario.DEFAULT_SCENARIO;
}
然后是支持重复的注解:
package com.tff.extension;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.stereotype.Component;
@Component
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Extensions {
String[] bizId() default BizScenario.DEFAULT_BIZ_ID;
String[] productNum() default BizScenario.DEFAULT_PRODUCT_NUM;
String[] customer() default BizScenario.DEFAULT_CUSTOMER;
String[] channel() default BizScenario.DEFAULT_CHANNEL;
String[] useCase() default BizScenario.DEFAULT_USE_CASE;
String[] scenario() default BizScenario.DEFAULT_SCENARIO;
Extension[] value() default {};
}
3
扩展点坐标
我们在上面定义出来扩展点注解,用来声明某个扩展点可以根据业务身份去找到对应的扩展实现。
里面有两个要素(业务身份,扩展点),形成了扩展坐标,扩展坐标是可以唯一的映射到一个扩展点实现。
然后是扩展点坐标,我们直接使用COLA框架里面的实现:
package com.tff.extension;
public class ExtensionCoordinate {
private final String extensionPointName;
private final String bizScenarioUniqueIdentity;
/**
* Wrapper
*/
private Class<?> extensionPointClass;
private BizScenario bizScenario;
public Class getExtensionPointClass() {
return extensionPointClass;
}
public BizScenario getBizScenario() {
return bizScenario;
}
public static ExtensionCoordinate valueOf(Class<?> extPtClass, BizScenario bizScenario){
return new ExtensionCoordinate(extPtClass, bizScenario);
}
public ExtensionCoordinate(Class<?> extPtClass, BizScenario bizScenario){
this.extensionPointClass = extPtClass;
this.extensionPointName = extPtClass.getName();
this.bizScenario = bizScenario;
this.bizScenarioUniqueIdentity = bizScenario.getUniqueIdentity();
}
public ExtensionCoordinate(String extensionPoint, String bizScenario){
this.extensionPointName = extensionPoint;
this.bizScenarioUniqueIdentity = bizScenario;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((bizScenarioUniqueIdentity == null) ? 0 : bizScenarioUniqueIdentity.hashCode());
result = prime * result + ((extensionPointName == null) ? 0 : extensionPointName.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ExtensionCoordinate other = (ExtensionCoordinate) obj;
if (bizScenarioUniqueIdentity == null) {
if (other.bizScenarioUniqueIdentity != null) {
return false;
}
} else if (!bizScenarioUniqueIdentity.equals(other.bizScenarioUniqueIdentity)) {
return false;
}
if (extensionPointName == null) {
return other.extensionPointName == null;
} else {
return extensionPointName.equals(other.extensionPointName);
}
}
@Override
public String toString() {
return "ExtensionCoordinate [extensionPointName=" + extensionPointName + ", bizScenarioUniqueIdentity=" + bizScenarioUniqueIdentity + "]";
}
}
4
扩展坐标与扩展点实例构成的扩展点仓库
-
使用一个标记接口,做接口声明:ExtensionPoint,这个接口里面什么也不用做。
package com.tff.extension;
/**
* 标记接口,扩展点实例需要实现这个接口
*/
public interface ExtensionPoint {
}
具体的扩展点实现,可以直接实现这个接口即可。
-
然后建立一个扩展点坐标与扩展点实例的映射关系map。
首先是存储扩展点的一个一个仓库,有些也叫做ExtentionManager是一个意思,包含存储扩展点坐标与扩展点实例的Map,注册、解除注册相关方法,以及或者某个扩展点实例的方法。@Component
public class ExtensionRepository {
private static Map<ExtensionCoordinate, ExtensionPoint> extensionRepo = new HashMap<>(64);
/**
* 注册扩展点实例
*
* @param coordinate 扩展点实例坐标
* @param extension 扩展点实例对象
*/
public void registerExtPoint(ExtensionCoordinate coordinate, ExtensionPoint extension) {
extensionRepo.put(coordinate, extension);
}
/**
* 查找扩展点实例
*
* @param coordinate 扩展点实例坐标
* @return 扩展点实例
*/
public ExtensionPoint getExtention(ExtensionCoordinate coordinate) {
return extensionRepo.get(coordinate);
}
}
5
运行时自动扫描注册扩展点
我们底层应用的是Spring框架,可以直接利用Spring的特性,应用启动后自动扫描所有实现后自动注册到ExtensionRepository仓库里面。
当然,还有SPI这种自动扫描发现,模块化热部署等方式,我们暂不考虑。
package com.tff.extension.register;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import com.tff.extension.Extension;
import com.tff.extension.ExtensionPoint;
import com.tff.extension.Extensions;
import jakarta.annotation.PostConstruct;
@Component
public class ExtensionAutoRegister implements ApplicationContextAware{
@Autowired
private ExtensionRegister extensionRegister;
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@PostConstruct
public void registerAllExtensions() {
Map<String, Object> extensionBeans = applicationContext.getBeansWithAnnotation(Extension.class);
extensionBeans.values().forEach(
extension -> extensionRegister.registerExtension((ExtensionPoint) extension)
);
// handle @Extensions annotation
Map<String, Object> extensionsBeans = applicationContext.getBeansWithAnnotation(Extensions.class);
extensionsBeans.values().forEach( extension -> extensionRegister.registerExtensions((ExtensionPoint) extension));
}
}
然后是剧场的注册类,用来校验相关名称等等:
package com.tff.extension.register;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import com.tff.extension.BizScenario;
import com.tff.extension.Extension;
import com.tff.extension.ExtensionCoordinate;
import com.tff.extension.ExtensionPoint;
import com.tff.extension.Extensions;
import jakarta.annotation.Resource;
@Component
public class ExtensionRegister {
/**
* 扩展点接口名称不合法
*/
private static final String EXTENSION_INTERFACE_NAME_ILLEGAL = "extension_interface_name_illegal";
/**
* 扩展点不合法
*/
private static final String EXTENSION_ILLEGAL = "extension_illegal";
/**
* 扩展点定义重复
*/
private static final String EXTENSION_DEFINE_DUPLICATE = "extension_define_duplicate";
@Resource
private ExtensionRepository extensionRepository;
public final static String EXTENSION_EXTPT_NAMING = "ExtPt";
public void doRegistration(ExtensionPoint extensionObject) {
Class<?> extensionClz = extensionObject.getClass();
if (AopUtils.isAopProxy(extensionObject)) {
extensionClz = ClassUtils.getUserClass(extensionObject);
}
Extension extensionAnn = AnnotationUtils.findAnnotation(extensionClz, Extension.class);
BizScenario bizScenario =
BizScenario.valueOf(extensionAnn.bizId(), extensionAnn.productNum(),
extensionAnn.customer(), extensionAnn.channel(),extensionAnn.useCase(), extensionAnn.scenario());
ExtensionCoordinate extensionCoordinate = new ExtensionCoordinate(calculateExtensionPoint(extensionClz), bizScenario.getUniqueIdentity());
extensionRepository.registerExtPoint(extensionCoordinate, extensionObject);
}
public void doRegistrationExtensions(ExtensionPoint extensionObject){
Class<?> extensionClz = extensionObject.getClass();
if (AopUtils.isAopProxy(extensionObject)) {
extensionClz = ClassUtils.getUserClass(extensionObject);
}
Extensions extensionsAnnotation = AnnotationUtils.findAnnotation(extensionClz, Extensions.class);
Extension[] extensions = extensionsAnnotation.value();
if (!ObjectUtils.isEmpty(extensions)){
for (Extension extensionAnn : extensions) {
BizScenario bizScenario = BizScenario.valueOf(extensionAnn.bizId(), extensionAnn.productNum(),
extensionAnn.customer(), extensionAnn.channel(),extensionAnn.useCase(), extensionAnn.scenario());
ExtensionCoordinate extensionCoordinate = new ExtensionCoordinate(calculateExtensionPoint(extensionClz), bizScenario.getUniqueIdentity());
extensionRepository.registerExtPoint(extensionCoordinate, extensionObject);
}
}
//
String[] bizIds = extensionsAnnotation.bizId();
String[] useCases = extensionsAnnotation.useCase();
String[] scenarios = extensionsAnnotation.scenario();
for (String bizId : bizIds) {
for (String useCase : useCases) {
for (String scenario : scenarios) {
BizScenario bizScenario = BizScenario.valueOf(bizId, useCase, scenario);
ExtensionCoordinate extensionCoordinate = new ExtensionCoordinate(calculateExtensionPoint(extensionClz), bizScenario.getUniqueIdentity());
extensionRepository.registerExtPoint(extensionCoordinate, extensionObject);
}
}
}
}
/**
* @param targetClz
* @return
*/
private String calculateExtensionPoint(Class<?> targetClz) {
Class<?>[] interfaces = ClassUtils.getAllInterfacesForClass(targetClz);
if (interfaces == null || interfaces.length == 0) {
throw new ExtensionException(EXTENSION_ILLEGAL, "Please assign a extension point interface for " + targetClz);
}
for (Class intf : interfaces) {
String extensionPoint = intf.getSimpleName();
if (extensionPoint.contains(EXTENSION_EXTPT_NAMING)) {
return intf.getName();
}
}
String errMessage = "Your name of ExtensionPoint for " + targetClz +
" is not valid, must be end of " + EXTENSION_EXTPT_NAMING;
throw new ExtensionException(EXTENSION_INTERFACE_NAME_ILLEGAL, errMessage);
}
}
6
扩展点的执行
我们在上面构造了ExtensionRepository,运行时注册到大量的实例。
下面就是看运行时,如何调度执行。
package com.tff.extension;
import java.util.function.Function;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
/**
* 根据扩展点坐标找到扩展点实例,并执行对应的方法
*/
@Component
public class ExtensionExecutor {
@Resource
private ExtensionRepository extensionRepository;
private ExtensionExecutor(){
}
public <R, T> R execute(Class<T> targetClz, BizScenario bizScenario, Function<T, R> exeFunction) {
T component = locateComponent(targetClz, bizScenario);
return exeFunction.apply(component);
}
/**
* 从扩展实例仓库中获取对应的实例
* @param <T>
* @param targetClz
* @param bizScenario
* @return
*/
private <T> T locateComponent(Class<T> targetClz, BizScenario bizScenario) {
return (T) extensionRepository.getExtention(new ExtensionCoordinate(targetClz, bizScenario));
}
}
7
应用中使用
上面的代码实现了完整的处理框架,那么我们在一个业务流程中如何设定扩展点进行处理,达到如下的效果呢?
就以上图为例,在多个垂直领域,使用添加客户功能,而这个功能分为四个节点,都有可能扩展,我们假设称之为扩展点A,扩展点B,扩展点C,扩展点D。
有两个问题:
1、如何唯一标记这个扩展点?这个扩展点标记应该是唯一的,这样的话各个垂直业务领域可以自由去实现各自的扩展即可。
这个地方比较简单,扩展点的标记就是扩展点的接口。
例如我们为扩展点A定一个接口:
package com.tff.extention.consumer;
import org.springframework.stereotype.Component;
import com.tff.extension.ExtensionExecutor;
import com.tff.extension.BizScenario;
import jakarta.annotation.Resource;
@Component
public class Addcustomer {
@Resource
private ExtensionExecutor extensionExecutor;
public void add() {
// 扩展点A:参数校验,对外公共接口:CustomerInfoValidator;
BizScenario bizScenario = getBizScenario();
// 这里会去寻找对应业务身份的对CustomerInfoValidator接口的扩展实现
extensionExecutor.execute(CustomerInfoValidator.class, bizScenario, ext -> ext.validate());
// 扩展点B:保存联系人
// 扩展点C:生成新机会
// 扩展点D:添加到私海
}
/**
* 识别业务身份的实现
* @return
*/
private BizScenario getBizScenario() {
return null;
}
}
例如扩展点A,我们声明的接口CustomerInfoValidator实现了扩展点标记接口ExtensionPoint:
package com.tff.extention.consumer;
import com.tff.extension.ExtensionPoint;
public interface CustomerInfoValidator extends ExtensionPoint{
public boolean validate();
}
有多个具体的实现:
package com.tff.extention.consumer;
import com.tff.extension.Extension;
@Extension(bizId = "Taobao", productNum = "seller", customer = "123", channel = "org", useCase = "register", scenario = "null")
public class TaobaoCustomerInfoValidator implements CustomerInfoValidator{
@Override
public boolean validate() {
return false;
}
}
2、如何通过这个扩展点的定义,找到具体的扩展点实现?与我们的扩展点仓库如何关联?
在进行注册的时候,将每一个扩展类放进了ExtensionCoordinate,其中包含bizScenario信息,与class信息。
这样形成完整闭环。
8
后记
本文结合自己对于业务身份、扩展点的理解,参阅了COLA的源码,形成了对于扩展点机制的一系列理解。
建议大家参阅COLA的源码:https://github.com/alibaba/COLA
原文始发于微信公众号(架构突围):基于业务身份的平台(中台)架构多维扩展点设计与实践(下篇-具体实现代码)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/170098.html