SOFABoot 是基于 Spring Boot 的一套研发框架。
在完全兼容 Spring Boot 的基础上,SOFABoot 还提供了启动期监控检查,上下文隔离,模块化开发,类隔离,日志空间隔离等等能力。
SOFABoot 地址:
https://github.com/alipay/sofa-boot
本文工程案例:
春节小长假还没感觉就过去了,对于“热爱工作”的我,也早早的回到了工作岗位;感受下假期中的我和上班时的我。
后面拿枪的就是"逼着"我写文章的五花肉,上次 SOFATracer 采样用的是刀,这次用了枪!
模块化与扩展点
言归正传,节前 SOFABoot 发布了2.6.x 系列版本,新特性也是相当给力,这里简单罗列下新特性:
1、支持扩展和扩展点
2、在刷新上下文期间支持 spring bean 的并行初始化
3、支持使用注解方式发布 JVM 服务
之前的文章中有 @玄北 写过的模块化的文章( 传送门 :剖析 | 详谈 SOFABoot 模块化原理 & 实操 | 基于 SOFABoot 进行模块化开发),这两篇文章中介绍了模块化的几种实现方案(PS:当然主要还是为了宣传一下 SOFABoot 提供的基于 Spring 上下文隔离的模块化能力)。SOFABoot 的模块隔离方案是为了解决传统的模块化方案模块化不彻底的问题,从 2.4.0 版本开始支持基于 Spring 上下文隔离的模块化能力,每个 SOFABoot 模块使用独立的 Spring 上下文,每个模块自包含,模块与模块之间通过 JVM Service 进行通信,从而避免模块间的紧耦合。
在 Spring 上下文隔离的情况下,各个上下文之间的 bean 是互不可见;SOFABoot 中通过发布 JVM 服务的方式使得不同模块 bean 之间的访问得以实现。但是同时又带来了另外一个问题,如果一个模块以独立 jar 的方式对外提供 api ,那么对于其他依赖此模块的模块来说,就无法去改变这个模块中的 bean 实例行为。
在实际的使用场景中,一个模块中的 bean 有时候需要开放一些入口,供另外一个模块扩展。SOFABoot 借鉴和使用了 Nuxeo Runtime项目 以及nuxeo项目,并在上面扩展,与 Spring 融合,提供了扩展点的能力。
本篇将针对 SOFABoot 2.6.x 版本中提供的扩展点进行简单尝试,结合官方文档提供的示例,一步一步实现我们自定义的一个扩展点功能(本文过于简单,可能会引起极度舒适,请备好被子和热水袋)。
案例背景
这里先抛出一个例子,现在有一个三方 jar ,它定义了获取数据源接口的顶层抽象;不同的业务方如果依赖这个 jar,则需要自己提供一个数据源的实现,当然其本身提供了默认实现(假设是 mysql)。基于此我们大概能够想到的方式就是基于 SPI 来提供这种扩展能力,但是对于在 Spring 上下文隔离的情况下,业务方的 Spring 上下文是无法与引入 jar 的上下文共享 bean 的,这样自然也就无法实现对原有数据源实现的扩展。
那么这里我们就可以选择使用 SOFABoot 扩展点来实现,这里有两个比较重要的概念,也是扩展点区别于 SPI 的主要特性:
1、可以在基于 Spring 上下文隔离的情况下实现扩展
2、扩展的是 Spring Bean
下面基于这两个点,来完成自定义扩展点的一个案例。在实现上述案例之前我们需要先构建一个基于 Spring 上下文隔离的模块化工程,然后再简单介绍下扩展点的基本使用方式。
构建模块化工程
SOFABoot 开源版本中并没有给出扩展点相关的案例工程,只是在测试用例中进行了详细的测试,有兴趣的同学可以看下相关测试用例代码。实际上测试用例中也没有涉及到在模块化的场景下使用扩展点,测试用例都是基于同一个Spring 上下文来完成的。本篇文章将先搭建一个简单的模块化工程,然后基于此工程来实现扩展点的能力。
本工程主要包括 4 个模块:
1、glmapper-sofa-facade //JVM 服务发布与引用的 API 包
2、glmapper-sofa-provider //Annotation 方式发布 JVM 服务
3、glmapper-sofa-consumer //Annotation 方式引用 JVM 服务
4、glmapper-sofa-web //启动包含 SOFABoot 模块的 SOFA Boot 应用
官方文档及案例中给的比较复杂,包含了多种使用服务发布和引用的方式,这里我使用了最新提供的基于注解的方式来实现;获取本文工程案例。
扩展点基本使用
在 SOFABoot 中使用扩展点能力,需要以下三个步骤:
1、定义提供扩展能力的 bean
2、定义扩展点
3、定义扩展并使用
这三步中前两步都是由服务提供方来完成,最后一步由具体的业务使用方式来定义。
定义提供扩展能力的 bean
本案例工程中,是将 glmapper-sofa-provider 作为服务提供方,因此也在此模块下定义一个具有扩展能力的 bean 。
定义这个接口的实现:
在模块的 Spring配置文件 resources/META-INF/service-provider.xml 中,我们把这个 bean 给配置起来:
定义扩展点
在上面的 bean 中有一个字段 word ,在实际中,我们希望这个字段能够被其他的模块自定义进行覆盖,这里我们将其以扩展点的形式暴露出来。这里先定义一个类去描述这个扩展点:

然后在模块的 Spring 配置文件 resources/META-INF/service-provider.xml 中定义扩展点:
name :为扩展点的名字
ref :为扩展点所作用在的 bean
object :为扩展点的贡献点具体的描述,这个描述是通过 XMap 的方式来进行的(XMap 的作用是将 Java 对象和 XML 文件进行映射,这里建议通过在网上搜索下 XMap 的文档来了解 XMap)
至此服务提供端已经暴露出了扩展点,那么在服务使用端,也就是需要扩展这个 bean 的使用方就可以扩展这个bean 了。
定义扩展
上述已经将扩展点定义好了,此时我们就可以对这个 bean 进行扩展了。扩展是具体业务使用方来做的事,在本案例中,glmapper-sofa-web 模块作为使用服务使用方,因此在 resources/META-INF/spring/web-home.xml 下进行扩展定义:
bean :为扩展所作用在的 bean
point :为扩展点的名字
content 里面的内容为扩展的定义,会通过 XMap 将内容解析为:扩展点的贡献点具体的描述对象,在这里即为 com.glmapper.bridge.extension.ExtensionDescriptor 对象
需要注意一点,glmapper-sofa-web 模块不是一个 SOFABoot 模块,这里留坑。
编写一个 TestController 类,这里最先参考的是 SOFABoot 测试用例中的写法,如下:
启动运行,结果抛了一个错:

没有找到 extension 这个 bean ,但是实际上我们在前面 定义提供扩展能力的 bean 小结中已经将 extension 配置成一个 bean 了。
原因在于,glmapper-sofa-provider 是一个 SOFABoot 模块,它有自己独立的 Spring 上下文环境,web 模块这里作为根上下文无法感知到这个 bean 的存在,所以这里还需要将extension 这个发布成一个 JVM 服务,然后才能正常启动。具体就是在 IExtensionImpl 类上加上 @SofaService 注解。然后在 TestController 中,将@Autowired 改成 @SofaReference 。
另外,因为 glmapper-sofa-web 不是一个 SOFABoot 模块(这里特指的是 isle 模块),在resources/META-INF/spring/web-home.xml 定义的扩展无法直接被 spring 扫到,因此还要在启动类上使用 @ImportResource 来指定当前 web 模块的 xml 文件位置,否则工程可以正常运行,但是基于此工程扩展点扩展的能力是无效的。
registerExtension
细心的同学可以注意到了一个点,就是前面扩展点实现 IExtensionImpl 这个类中有一个特殊的方法,在整个案例演示中其实都是没有用到的。
最开始对这个方法我也很迷糊,因为我并没有用到。既然自己没用到,那一定是框架自己用到了。有兴趣的同学可以自己断点下这段逻辑。实际上 SOFABoot 在解析到贡献点时,会调用被扩展 bean 的 registerExtension 方法,其中包含了用户定义的贡献点处理逻辑。在上述的例子中,获取用户定义的 value 值,并将其设置到 word 字段中覆盖 bean 中原始定义的值,最后调用 extension.say() 方法,可以看到返回扩展中定义的值: newValue 。
XMap 支持和扩展
上述的例子中只是一个很简单的扩展,其实 XMap 包含了非常丰富的描述能力,包括List,Map等,这些可以通过查看XMap 的文档来了解。在 SOFABoot 中,除了 XMap 原生的支持以外,还扩展了跟 Spring 集成的能力:
1、通过 XNode 扩展出了 XNodeSpring
2、通过 XNodeList 扩展出了 XNodeListSpring
3、通过 XNodeMap 扩展出了 XNodeMapSpring
这部分的扩展能力,让扩展点的能力更加丰富,描述对象中可以直接指向一个 SpringBean (用户配置 bean 的名字,SOFABoot 会根据名字从 Spring 上下文中获取到 bean)。
Datasource 扩展点案例
基于前小结对于 XMAP 的扩展的介绍以及开篇的案例, 这里举一个使用 XNodeSpring 的例子,来实现 Spring 上下文隔离场景对于数据源 bean 的扩展。依然是前文描述的三个步骤:
辅助接口和类
1、定义一个 DatasourceBean ,并且提供一个 getDatasource 方法,用于获取 数据源实例。

2、定义一个 DefaultDataSourceBean ,作为 DatasourceBean 接口的默认实现。

定义提供扩展能力的 DatasourceExtension Bean
新建 DatasourceExtension 接口

新建 DatasourceExtensionImpl 实现类,并且实现 DatasourceExtension 中的 getDatasourceBean 方法,且里面通过 datasourceBean 去执行获取数据源实例。

定义扩展点
定义并且暴露扩展点,这里还需要一个扩展点描述。

下面在 xml 文件中将此扩展点通过 xml 暴露出去,并配置相关默认实现。
以上几步在此案例工程包括定义提供扩展能力的 bean、包括扩展点等均在 glmapper-sofa-provider 中完成,作为扩展点的提供方。
定义扩展
这部分实现是需要由业务方来完成,这里就需要对于 provider 中提供的扩展点进行扩展,以改变其本身提供的 bean 实例的行为。
扩展扩展点,这里我们希望能够扩展对于 oracle 数据源的支持,那么对于 provider 中提供的默认对 mysql 的支持的 bean 实例就需要被“扩展”,此处的扩展本身上就是 bean 实例的替换。
首先定义一个 OracleDatasourceBean ,同样实现 DatasourceBean 这个接口,getDatasource 方法中返回 oracle 的数据源实例:

然后在业务模块(本案例在 glmapper-sofa-web 模块下)的 resources/META-INF/spring/web-home.xml 中配置扩展的 bean 并且对扩展点进行扩展。如下:

详细代码见:glmapper-sofa-extension。
下面开始启动项目工程,首先将扩展部分注释掉,执行 http://localhost:8080/extension ,查看控制台打印结果如下:

打开扩展部分注释,重新启动,刷新地址,查看控制台打印结果如下:

那么这里可以看到,provider 中提供的数据源 bean 被自定义的 数据源 bean 替换了。实现了在 Spring 上下文隔离情况下,替换 bean 的操作。
小结
扩展点的存在很好的解决了这样一个问题:需要在另一个模块中对依赖的模块中定义的组件进行定制化。在模块化的场景下,如果能够允许改变另外一个模块中 bean 的行为,无疑会解决很多棘手的问题。
本文通过一个简单的 Demo 对 SOFABoot 中扩展点进行了演示,本篇基于 SOFABoot 官方文档,补充了一些使用上的细节以及需要注意的一些坑,希望通过本篇文章可以帮助大家对 SOFABoot 扩展点的能力及使用有初步了解。
未出正月都是年,这里给大家拜个晚年,祝大家新年快乐、升职加薪!
文中相关链接
本文工程案例:https://github.com/glmapper/glmapper-sofa-extension
SOFABoot 2.6.x 系列版本:https://github.com/alipay/sofa-boot/releases
剖析 | 详谈 SOFABoot 模块化原理:https://mp.weixin.qq.com/s/7WAClC-f9mM7-_WIa10M_g
实操 | 基于 SOFABoot 进行模块化开发:https://mp.weixin.qq.com/s/-7_wXRcvWpt6JwsBIOkQjA
SOFABoot 官方文档及案例:https://www.sofastack.tech/sofa-boot/docs/extension
nuxeo 项目:https://github.com/nuxeo/nuxeo
公众号:金融级分布式架构(Antfin_SOFA)
网友评论