目录
专栏系列
-SaaS 电商设计 (一) 如何设计一套适应多规格的商品服务
-SaaS 电商设计 (二) 私有化部署-缓存中间件适配
-SaaS 电商设计 (三) 电商黄金流程(商详,购物车,提单)梳理,持续更新(建议收藏)
-SaaS 电商设计 (四) 谈一谈电商系统高并发多耦合上下游的系统压测怎么做
-SaaS 电商设计 (五) 私有化部署-实现 binlog 中间件适配(附源码)
-SaaS 电商设计 (六) 实现 id 生成器本地化生产 (附源码)
-SaaS 电商设计 (七) 利用 Spring 扩展点 ImportBeanDefinitionRegistrar 实现 toB 系统对接(附源码)
一.业务背景
在实际的 SaaS toB 客户交付过程中,存在一些现有客户系统的对接场景.主要分为以下这两种场景
- 场景1: 灰度期间:从客户的既有的老系统交割到新的SaaS系统的数据同步.
这时候老系统在实际进行业务流转,新系统在灰度期间承担完成将基础数据 (商品,门店,库存等基础数据) 通过老系统同步到新系统,以便于后续替换客户老系统.
如下:在这个过程中数据的流向是 客户老系统–>SaaS 系统 .这时候 SaaS 系统就需要具备接收数据同步的能力的接口.
- 场景2: 系统完成交割后.SaaS系统与原有客户其他既有系统的数据同步
如:客户在购物 SaaS 产品过程并不是完全所有产品都使用 SaaS 的服务,部分场景中使用部分原有业务系统,如会员系统仍然使用既有系统.那么在部分系统切换到 SaaS 系统后仍然存在于系统数据之间的同步,且这个同步的动作仍然是长期的事情.
如下:同步数据的方向是 SaaS 系统->客户老系统.这时候 SaaS 系统就需要具备主动数据同步的能力的接口.且一个数据需要完成多个客户的数据同步.
在以上的场景中我们发现在不断的对接过程中,有两个点是需要关注的.
数据接口的同步方向:存在正向的数据输出,逆向的数据回传.
数据接口的对接客户存在多个的场景下如何去解决一个数据同步两个客户的情况,两个客户的接口也存在差异化的场景,如何解决差异化接口同步的诉求.
二.目标
基于以上的两种场景,提出一个统一出网网关的概念.以此来解决 SaaS 产品在交付不同 B 场景多客户时带来的差异化数据同步逻辑不同的问题.
实现的关键路径:
-
领域服务一次调用解决多次数据同步问题
-
对接的差异化逻辑对于领域服务的一次调用是尽可能的透明
三.技术设计
3.1 业务域流程方案
如图所示我们在去对接各个客户 X(A,B,C) 服务时存在以下几种服务调用的场景.
如:
- 店铺服务调用客户A,客户B的两种不同客户的正向数据同步服务.(一对多的服务调用)
- 商品服务调用客户B的一种正向数据同步服务.(一对一的服务调用)
逆向场景如上类似.流程相反
那么就带来了一个问题:如上的统一网关是如何利用 router 来解决同步过程中的这些问题.
如上放大后的 统一对接网关 ,从服务的分层上来说,大体分为标准服务层,核心网关服务,不同客户的适配层.
- 标准服务层
首先是会在网关抽象一个标准服务层,这个服务层是在产品迭代过程中逐渐去丰富沉淀的标准服务,标准对齐的是内部的系统.如:商品服务,库存服务等.
什么情况下出现需要更新标准服务层呢?
这是一个好问题,因为实际过程中遇到的非常多.举个例子:在迭代过程中逐渐增加了称重品能力,增加了字段(步长,起购量,商品展示价格等),那么网关的标准服务也是同步会去做升级,保证这一层是能够持续去跟随产品能力而丰富的.那具体一点就是在同步商品的时候增加沉重品相关的字段逻辑.
- 核心网关服务层
这一层职责比较多,分为两个方面.
1.业务层面:具备通过领域服务调用标准服务层后完成多种客户群体的调用能力.
举个例子:商品领域服务通过调用标准服务层做到商品数据同步,此时可能出现两种客户 A,B 都需要进行商品数据同步,网关服务层需要做到通过一次的领域服务调用做到 A,B 的两种服务同步,因为对于领域来说其实就是商品数据同步,不用关心具体是哪些客户需要进行商品数据同步,这样就做到了解耦;第二点:调用过程中具备完成异常场景下的业务重试能力,这里又有一些异常的具体处理,比如说 超时异常 可以通过自动重试做到,业务异常需要提供可供操作的工作台来提供手动触发重试的能力.
2.技术层面
-
流量管控
-
日志记录
-
超时配置化
-
限流控制等.
-
适配层
转换标准参数到客户 X(A,B,C) 参数(正向,逆向同理)
3.2 技术实现方案
3.2.1 核心实现 ImportBeanDefinitionRegistrar
用一句话来表达:本质上是通过实现 ImportBeanDefinitionRegistrar 来完成业务接口的动态代理注入,代理实际的业务调用.以此来完成具体业务的代理实现.拿到代理之后想怎么玩就可以玩,随心所欲.
大致流程
3.2.2 扩展一下 ImportBeanDefinitionRegistrar
- step1:spring 通过AbstractApplication#refresh 来启动整体容器.进入到后置处理时
- step2:通过ConfigurationClassPostProcessor来实现 @Import 注解扫描.
- step3: 核心的 ImportBeanDefinitionRegistrar 就是通过搭配 @Import注解来实现扫描.
通过实现自定义的代理类注册到 beanDefinition 中来实现接口的动态代理 bean 注入.
类似如上的这种做法其实并不少见,多见很多开源框架中都有他的实现,譬如: mybatis 关于 mapper 接口的实现, dubbo 关于 reference api 的代理实现.都能看到基于 ImportBeanDefinitionRegistrar 的实现.有兴趣的同学可以自行研究.
3.2.3 核心代码实现
动态代理
/**
* 完成代理工厂
* step1:获取上下文中router
* step2:获取spring 上下文中动态代理对象
* step3:实现动态代理 invoke
* @author baixiu
* @date 创建时间 2023/12/7 4:47 PM
*/
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class ExtensionBeanInterceptor implements BeanClassLoaderAware, MethodInterceptor, FactoryBean<Object> {
private static final Logger log = LoggerFactory.getLogger(ExtensionBeanInterceptor.class);
/**
* 类加载器
*/
private ClassLoader classLoader;
/**
* 策略路由
*/
private SPIRouter spiRouter;
/**
* 代理bean
*/
private Object serviceProxy;
/**
* 扩展服务接口
*/
private Class<?> serviceInterface;
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
//step1:get invocation identity
String identityStr=getIdentity(methodInvocation);
//step2:push into thread local
ThreadLocalSPIRouter.pushIdentity(identityStr);
//step3:通过identity 获取router
String routerName=this.spiRouter.route(methodInvocation);
//step4:通过SPIExtensionBeanContexts 获取 bean
Object routerObject=SPIExtensionBeanContexts.BEAN_EXTENDS_MAP.get(routerName);
if(Objects.nonNull(routerObject)){
//通过声明class强转object
Object realBean=methodInvocation.getMethod ().getDeclaringClass ().cast (routerObject);
Method method=realBean.getClass().getMethod(methodInvocation.getMethod().getName(),methodInvocation.getMethod()
.getParameterTypes());
try {
return method.invoke(realBean,methodInvocation.getArguments());
} catch (Exception e) {
throw new RuntimeException (e);
} finally {
ThreadLocalSPIRouter.popIdentity();
}
}
//step5:动态代理反射调用
return null;
}
private String getIdentity(MethodInvocation methodInvocation) {
try {
Object[] objects=methodInvocation.getArguments();
if(objects!=null && objects.length>0){
Field fields=objects[0].getClass().getDeclaredField(CommonConsts.DEFAULT_IDENTITY_FIELD_NAME);
fields.setAccessible (true);
String identityStr=fields.get(objects[0]).toString()+"_"+CommonConsts.DEFAULT_SCENARIO;
log.info("getIdentity.identityStr.{}",identityStr);
return identityStr;
}
return CommonConsts.DEFAULT_IDENTITY+"1";
} catch(Exception e) {
return CommonConsts.DEFAULT_IDENTITY+"2";
}
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader=classLoader;
}
@Override
public Object getObject() throws Exception {
if (serviceProxy == null) {
Class<?> ifc = getServiceInterface();
Assert.notNull(ifc, "Property 'serviceInterface' is required");
Assert.notNull(getSpiRouter(), "Property 'spiRouter' is required");
serviceProxy = new ProxyFactory(ifc, this).getProxy(classLoader);
}
return serviceProxy;
}
@Override
public Class<?> getObjectType() {
return getServiceInterface ();
}
public SPIRouter getSpiRouter() {
return spiRouter;
}
public void setSpiRouter(SPIRouter spiRouter) {
this.spiRouter = spiRouter;
}
/**
* 获取服务接口
*
* @param serviceInterface
*/
public void setServiceInterface(Class<?> serviceInterface) {
Assert.notNull(serviceInterface, "'serviceInterface' must not be null");
Assert.isTrue(serviceInterface.isInterface(), "'serviceInterface' must be an interface");
this.serviceInterface = serviceInterface;
}
public Object getServiceProxy() {
return serviceProxy;
}
public void setServiceProxy(Object serviceProxy) {
this.serviceProxy = serviceProxy;
}
public Class<?> getServiceInterface() {
return serviceInterface;
}
}
通过ImportBeanDefinitionRegistrar 完成动态代理 注册
/**
* 用以注册spring容器之外的bean定义。
* @author baixiu
* @date 创建时间 2023/12/20 2:35 PM
*/
@Component
public class MultiBizProxyRegister implements ImportBeanDefinitionRegistrar, BeanClassLoaderAware, BeanFactoryAware, EnvironmentAware, ResourceLoaderAware {
private BeanFactory beanFactory;
private Environment envirnoment;
private ClassLoader classLoader;
private ResourceLoader resourceLoader;
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {
//step1:根据 baseScan 注解配置 获取需要扫描的basePackage 路径
Set<String> scanPackages= getScanPackages(annotationMetadata);
//step2:定义scanner
ClassPathScanningCandidateComponentProvider spiScanner=getScanner();
//遍历需要扫描的包路径,获取 beanDefinition
for (String scanPackage : scanPackages) {
//获取某一个路径下的候选bean定义
Set<BeanDefinition> beanDefinitions= spiScanner.findCandidateComponents(scanPackage);
//遍历每个 bean 注册定义,生产动态代理对象,通过 ImportBeanDefinitionRegistrar 开放的接口registerBeanDefinitions 进行
//动态代理对象的注册到 spring 容器
try {
for (BeanDefinition beanDefinition : beanDefinitions) {
//当注册对象实例为注解bean定义时 获取注解的元数据信息
AnnotationMetadata annotationMetadataItem=null;
if(beanDefinition instanceof AnnotatedBeanDefinition){
annotationMetadataItem=((AnnotatedBeanDefinition) beanDefinition).getMetadata();
}
//通过注解定义的元数据信息获取注解对应的注解属性值
Map<String,Object> spiDefineAttrs=annotationMetadataItem.getAnnotationAttributes(SPIDefine.class.getName ());
if(spiDefineAttrs ==null || spiDefineAttrs.isEmpty ()){
continue;
}
String routerBeanName=null;
if(spiDefineAttrs.get(CommonConsts.SPI_DEFINE_ATTR_NAME) instanceof String){
routerBeanName = (String) spiDefineAttrs.get(CommonConsts.SPI_DEFINE_ATTR_NAME);
}
//get router
SPIRouter spiRouter=beanFactory.getBean(routerBeanName,SPIRouter.class);
//创建beanFactory
ExtensionBeanInterceptor interceptor=this.beanFactory.getBean(ExtensionBeanInterceptor.class);
//创建动态代理
Class<?> clazz=Class.forName(beanDefinition.getBeanClassName());
interceptor.setServiceInterface(clazz);
interceptor.setSpiRouter(spiRouter);
Object spiProxyObject=interceptor.getObject();
BeanDefinitionBuilder beanDefinitionBuilder=BeanDefinitionBuilder.genericBeanDefinition(spiProxyObject.getClass());
beanDefinitionBuilder.addConstructorArgValue (Proxy.getInvocationHandler(spiProxyObject));
AbstractBeanDefinition realBeanDefinition = beanDefinitionBuilder.getBeanDefinition();
realBeanDefinition.setPrimary(true);
//注册definition到spring容器
StringBuilder sb = new StringBuilder()
.append(clazz.getSimpleName())
.append("#Proxy");
//ImportBeanDefinitionRegistrar 重写得到的registry来实现bean的动态代理注册
registry.registerBeanDefinition(sb.toString(), realBeanDefinition);
}
} catch (Exception e) {
throw new RuntimeException (e);
}
}
}
private ClassPathScanningCandidateComponentProvider getScanner() {
//不通过默认filter来实现scanner ,则后面需要增加filter
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider (false, this.envirnoment) {
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition annotatedBeanDefinition) {
//获取注解的元数据
// 1.判断是否是独立的注解 不依赖其他注解或者类
// 2.注解是否是一个接口
if (annotatedBeanDefinition.getMetadata ().isIndependent () && annotatedBeanDefinition.getMetadata ().isInterface ()) {
//获取类 判断是否存在SPI defines
try {
Class<?> target = ClassUtils.forName(annotatedBeanDefinition.getMetadata ().getClassName (),
MultiBizProxyRegister.this.classLoader);
SPIDefine[] spiDefines = target.getAnnotationsByType (SPIDefine.class);
return spiDefines.length > 0;
} catch (ClassNotFoundException e) {
throw new RuntimeException (e);
}
}
return false;
}
};
scanner.setResourceLoader(this.resourceLoader);
scanner.addIncludeFilter(new AnnotationTypeFilter (SPIDefine.class));
return scanner;
}
/**
* 获取需要扫描得到beanDefinition的 package 路径
* @param annotationMetadata 注解源数据信息 可获取spring中的注解上下文
* @return
*/
private Set<String> getScanPackages(AnnotationMetadata annotationMetadata){
AnnotationAttributes annotationAttributes=AnnotationAttributes.fromMap(annotationMetadata
.getAnnotationAttributes(RouterBaseScan.class.getName()));
Set<String> spiScanPackages=null;
assert annotationAttributes != null;
String[] paths=annotationAttributes.getStringArray("path");
if(paths.length>0){
spiScanPackages=new HashSet<>();
spiScanPackages.addAll(Arrays.asList(paths));
}
return spiScanPackages;
}
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.classLoader=classLoader;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory=beanFactory;
}
@Override
public void setEnvironment(Environment environment) {
this.envirnoment=environment;
}
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader=resourceLoader;
}
}
整体的网关代码结构
3.2.4 开箱即用
四.总结
未完待续,以上 基于 ImportBeanDefinitionRegistrar 搭配 @Import 的形式以及自定义注解的方式来实现了.toB场景中,如何使用一次服务调用做到的多次业务客户不同业务逻辑适配.后续还会继续迭代更新,做到通过数据库配置不同业务逻辑服务的形式来实现业务配置可配,流量超时配置.以及动态加载的能力.
赠人玫瑰 手有余香 我是柏修 一名持续更新的晚熟程序员
期待您的点赞,关注加收藏,加个关注不迷路,感谢
您的鼓励是我更新的最大动力
↓↓↓↓↓↓