SaaS 电商设计 (六) 利用 ImportBeanDefinitionRegistrar 如何友好实现 toB 三方系统对接(附源码)

本文主要是介绍SaaS 电商设计 (六) 利用 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;@Overridepublic Object invoke(MethodInvocation methodInvocation) throws Throwable {//step1:get invocation identityString identityStr=getIdentity(methodInvocation);//step2:push into thread localThreadLocalSPIRouter.pushIdentity(identityStr);//step3:通过identity 获取routerString routerName=this.spiRouter.route(methodInvocation);//step4:通过SPIExtensionBeanContexts 获取 beanObject routerObject=SPIExtensionBeanContexts.BEAN_EXTENDS_MAP.get(routerName);if(Objects.nonNull(routerObject)){//通过声明class强转objectObject 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";}}@Overridepublic void setBeanClassLoader(ClassLoader classLoader) {this.classLoader=classLoader;}@Overridepublic 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;}@Overridepublic 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;@Overridepublic void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {//step1:根据 baseScan 注解配置 获取需要扫描的basePackage 路径Set<String> scanPackages= getScanPackages(annotationMetadata);//step2:定义scannerClassPathScanningCandidateComponentProvider spiScanner=getScanner();//遍历需要扫描的包路径,获取 beanDefinitionfor (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 routerSPIRouter spiRouter=beanFactory.getBean(routerBeanName,SPIRouter.class);//创建beanFactoryExtensionBeanInterceptor 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 ,则后面需要增加filterClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider (false, this.envirnoment) {@Overrideprotected boolean isCandidateComponent(AnnotatedBeanDefinition annotatedBeanDefinition) {//获取注解的元数据// 1.判断是否是独立的注解 不依赖其他注解或者类// 2.注解是否是一个接口if (annotatedBeanDefinition.getMetadata ().isIndependent () && annotatedBeanDefinition.getMetadata ().isInterface ()) {//获取类 判断是否存在SPI definestry {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;}@Overridepublic void setBeanClassLoader(ClassLoader classLoader) {this.classLoader=classLoader;}@Overridepublic void setBeanFactory(BeanFactory beanFactory) throws BeansException {this.beanFactory=beanFactory;}@Overridepublic void setEnvironment(Environment environment) {this.envirnoment=environment;}@Overridepublic void setResourceLoader(ResourceLoader resourceLoader) {this.resourceLoader=resourceLoader;}
}

整体的网关代码结构

在这里插入图片描述

3.2.4 开箱即用

老规矩:https://github.com/Baixiu-code/common-open-gateway 一键直达

四.总结

未完待续,以上 基于 ImportBeanDefinitionRegistrar 搭配 @Import 的形式以及自定义注解的方式来实现了.toB场景中,如何使用一次服务调用做到的多次业务客户不同业务逻辑适配.后续还会继续迭代更新,做到通过数据库配置不同业务逻辑服务的形式来实现业务配置可配,流量超时配置.以及动态加载的能力.

赠人玫瑰 手有余香 我是柏修 一名持续更新的晚熟程序员
期待您的点赞,关注加收藏,加个关注不迷路,感谢
您的鼓励是我更新的最大动力
↓↓↓↓↓↓

这篇关于SaaS 电商设计 (六) 利用 ImportBeanDefinitionRegistrar 如何友好实现 toB 三方系统对接(附源码)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/649639

相关文章

在C#中获取端口号与系统信息的高效实践

《在C#中获取端口号与系统信息的高效实践》在现代软件开发中,尤其是系统管理、运维、监控和性能优化等场景中,了解计算机硬件和网络的状态至关重要,C#作为一种广泛应用的编程语言,提供了丰富的API来帮助开... 目录引言1. 获取端口号信息1.1 获取活动的 TCP 和 UDP 连接说明:应用场景:2. 获取硬

使用Python实现在Word中添加或删除超链接

《使用Python实现在Word中添加或删除超链接》在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能,本文将为大家介绍一下Python如何实现在Word中添加或... 在Word文档中,超链接是一种将文本或图像连接到其他文档、网页或同一文档中不同部分的功能。通过添加超

windos server2022里的DFS配置的实现

《windosserver2022里的DFS配置的实现》DFS是WindowsServer操作系统提供的一种功能,用于在多台服务器上集中管理共享文件夹和文件的分布式存储解决方案,本文就来介绍一下wi... 目录什么是DFS?优势:应用场景:DFS配置步骤什么是DFS?DFS指的是分布式文件系统(Distr

NFS实现多服务器文件的共享的方法步骤

《NFS实现多服务器文件的共享的方法步骤》NFS允许网络中的计算机之间共享资源,客户端可以透明地读写远端NFS服务器上的文件,本文就来介绍一下NFS实现多服务器文件的共享的方法步骤,感兴趣的可以了解一... 目录一、简介二、部署1、准备1、服务端和客户端:安装nfs-utils2、服务端:创建共享目录3、服

JAVA系统中Spring Boot应用程序的配置文件application.yml使用详解

《JAVA系统中SpringBoot应用程序的配置文件application.yml使用详解》:本文主要介绍JAVA系统中SpringBoot应用程序的配置文件application.yml的... 目录文件路径文件内容解释1. Server 配置2. Spring 配置3. Logging 配置4. Ma

2.1/5.1和7.1声道系统有什么区别? 音频声道的专业知识科普

《2.1/5.1和7.1声道系统有什么区别?音频声道的专业知识科普》当设置环绕声系统时,会遇到2.1、5.1、7.1、7.1.2、9.1等数字,当一遍又一遍地看到它们时,可能想知道它们是什... 想要把智能电视自带的音响升级成专业级的家庭影院系统吗?那么你将面临一个重要的选择——使用 2.1、5.1 还是

C#使用yield关键字实现提升迭代性能与效率

《C#使用yield关键字实现提升迭代性能与效率》yield关键字在C#中简化了数据迭代的方式,实现了按需生成数据,自动维护迭代状态,本文主要来聊聊如何使用yield关键字实现提升迭代性能与效率,感兴... 目录前言传统迭代和yield迭代方式对比yield延迟加载按需获取数据yield break显式示迭

Python实现高效地读写大型文件

《Python实现高效地读写大型文件》Python如何读写的是大型文件,有没有什么方法来提高效率呢,这篇文章就来和大家聊聊如何在Python中高效地读写大型文件,需要的可以了解下... 目录一、逐行读取大型文件二、分块读取大型文件三、使用 mmap 模块进行内存映射文件操作(适用于大文件)四、使用 pand

python实现pdf转word和excel的示例代码

《python实现pdf转word和excel的示例代码》本文主要介绍了python实现pdf转word和excel的示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价... 目录一、引言二、python编程1,PDF转Word2,PDF转Excel三、前端页面效果展示总结一

Python xmltodict实现简化XML数据处理

《Pythonxmltodict实现简化XML数据处理》Python社区为提供了xmltodict库,它专为简化XML与Python数据结构的转换而设计,本文主要来为大家介绍一下如何使用xmltod... 目录一、引言二、XMLtodict介绍设计理念适用场景三、功能参数与属性1、parse函数2、unpa