基于业务身份的平台(中台)架构多维扩展点设计与实践(下篇-具体实现代码)

通过上面的文章我们已经充分了解了业务身份、扩展点的理念。

那么如果我们要设计一个框架,支持扩展点机制,应该怎么做?假设我们的扩展点方案建设在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

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!