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

相关文章

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

基于人工智能的图像分类系统

目录 引言项目背景环境准备 硬件要求软件安装与配置系统设计 系统架构关键技术代码示例 数据预处理模型训练模型预测应用场景结论 1. 引言 图像分类是计算机视觉中的一个重要任务,目标是自动识别图像中的对象类别。通过卷积神经网络(CNN)等深度学习技术,我们可以构建高效的图像分类系统,广泛应用于自动驾驶、医疗影像诊断、监控分析等领域。本文将介绍如何构建一个基于人工智能的图像分类系统,包括环境

水位雨量在线监测系统概述及应用介绍

在当今社会,随着科技的飞速发展,各种智能监测系统已成为保障公共安全、促进资源管理和环境保护的重要工具。其中,水位雨量在线监测系统作为自然灾害预警、水资源管理及水利工程运行的关键技术,其重要性不言而喻。 一、水位雨量在线监测系统的基本原理 水位雨量在线监测系统主要由数据采集单元、数据传输网络、数据处理中心及用户终端四大部分构成,形成了一个完整的闭环系统。 数据采集单元:这是系统的“眼睛”,

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

嵌入式QT开发:构建高效智能的嵌入式系统

摘要: 本文深入探讨了嵌入式 QT 相关的各个方面。从 QT 框架的基础架构和核心概念出发,详细阐述了其在嵌入式环境中的优势与特点。文中分析了嵌入式 QT 的开发环境搭建过程,包括交叉编译工具链的配置等关键步骤。进一步探讨了嵌入式 QT 的界面设计与开发,涵盖了从基本控件的使用到复杂界面布局的构建。同时也深入研究了信号与槽机制在嵌入式系统中的应用,以及嵌入式 QT 与硬件设备的交互,包括输入输出设

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo