剖析 | 详谈 SOFABoot 模块化原理

原文链接:https://juejin.im/post/5b7524d9518825611858b23a

SOFA(Scalable Open Financial Architecture)

是蚂蚁金服自主研发的金融级分布式中间件,包含了构建金融级云原生架构所需的各个组件,是在金融场景里锤炼出来的最佳实践。


SOFABoot 是蚂蚁金服中间件团队开源的基于 Spring Boot 的一个开发框架,SOFABoot 从 2.4.0 版本开始支持基于 Spring 上下文隔离的模块化开发能力,SOFABoot 模块除了包括 Java 代码外,还会包含 Spring 配置文件,每个 SOFABoot 模块都是独立的 Spring 上下文。


SOFABoot 的 Github 的地址是:

https://github.com/alipay/sofa-boot

传统模块化的陷阱

在介绍 SOFABoot 模块化之前,先让我们再回顾一遍传统模块化的弊端,这部分内容参考自鲁直(SOFA 开源负责人)发表的 蚂蚁金服的业务系统模块化 ---- 模块化隔离方案


在一个简单的 Spring/SpringBoot 的系统中,我们常常见到一个系统中的模块会按照如下的方式进行分层,如下图中的左边部分所示,一个系统就简单地分为 Web 层、Service 层、DAL 层。



当这个系统承载的业务变多了之后,系统可能演化成上图中右边的这种方式。在上图的右边的部分中,一个系统承载了两个业务,一个是 Cashier(收银台),另一个是 Pay(支付),这两个业务可能会有一些依赖的关系,Cashier 需要调用 Pay 提供的能力去做支付。

但是在这种模块化的方案里面,Spring 的上下文依然是同一个,类也没有任何做隔离,这就意味着,Pay Service 这个模块里面的任何的一个 Bean,都可以被 Cashier Service 这个模块所依赖。极端的情况下,可能会出现下面这种情况:



Cashier Service 错误地调用了 Pay Service 中的一个内部的 Bean,造成了两个模块之间的紧耦合。


这种传统的模块化的问题在于模块化地不彻底。虽然在研发的时候,通过划分模块,把特定职责的类放到特定的模块里面去,达到了类的「物理位置」的内聚。但是在运行时,由于没有做任何隔离的手段,作为一个模块的开发者,并没有办法清楚地知道对方模块提供的对外的接口到底是什么,哪些 Bean 我是可以直接注入来用的,哪些 Bean 是你的内部的 Bean,我是不能用的。长此以往,模块和模块之间的耦合就会越来越严重,原来的模块的划分形同虚设。当系统越来越大,最后需要做服务化拆分的时候,就需要花费非常大的精力去梳理模块和模块之间的关系。

SOFABoot 模块化简介

为了解决传统的模块化方案模块化不彻底的问题,SOFABoot 从 2.4.0 版本开始支持基于 Spring 上下文隔离的模块化能力,每个 SOFABoot 模块使用独立的 Spring 上下文,每个模块自包含,模块与模块之间通过 JVM Service 进行通信,避免模块间的紧耦合:

通过上面的系统架构图可以看到,SOFABoot 模块化一共包含三个基本概念,分别是 SOFABoot 模块、JVM Service 以及 Root Application Context:

  • SOFABoot 模块: SOFABoot 模块是一个包括 Java 代码、Spring 配置文件、SOFABoot 模块标识等信息的普通 Jar 包,每个 SOFABoot 模块都是一个独立的 Spring 上下文。

  • JVM Service: 上下文隔离后,模块与模块间的 Bean 无法直接注入,JVM Service 用于实现模块间通信,用于发布及引用模块服务。

  • Root Application Context: SOFABoot 应用调用 SpringApplication.run(args) 方法后产生的 Spring 上下文,是所有 SOFABoot 模块的 Parent。


本文接下来部分将分别介绍 SOFABoot 模块的查找与刷新、JVM Service 与组件管理以及 Root Application Context 的基本概念,之后会对 SOFABoot 模块的 Require-Module、Spring-Parent 以及并行启动进行简单介绍,并在最后给出进行模块化开发的实践建议。

SOFABoot 模块的查找与刷新

SOFABoot 模块查找与刷新的时序图如下:



在 SOFABoot 中,我们定义了 SofaModuleContextRefreshedListener,该类会监听 Root Application Context 发送的 ContextRefreshedEvent 事件,选择响应该事件是出于以下两方面考虑:

  1. ContextRefreshedEvent 是标准的 Spring 事件,比较通用。

  2. SOFABoot 模块刷新需要等待 Root Application Context 刷新完毕后进行,因为 SOFABoot 模块可能依赖 Root Application Context 中的 bean 定义。


SofaModuleContextRefreshedListener 监听到 ContextRefreshedEvent 事件后会创建 PipelineContext 对象,在 PipelineContext 中会添加 ModelCreatingStage、SpringContextInstallStage、ModuleLogOutputStage 三个 Stage,三个 Stage 的作用分别如下:

  • ModelCreatingStage: 在当前 ClassPath 查找所有合法的 SOFABoot 模块;

  • SpringContextInstallStage: 为每个查找到的 SOFABoot 模块新建一个 Spring Context,加载 SOFABoot 模块中的 Spring 配置文件;

  • ModuleLogOutputStage: 输出所有刷新成功和刷新失败的 SOFABoot 模块,方便用户快速定位问题。


调用 PipelineContext 的 process 方法,会触发这三个 Stage 依次执行,对当前 ClassPath 包含的所有 SOFABoot 模块都进行刷新。

JVM Service 与组件管理

上下文隔离后,模块与模块间的 Bean 无法直接注入,JVM Service 用于实现模块间通信,用于发布及引用模块服务。

为了实现跨模块的服务发现,我们在 SOFABoot 内部定义了一个组件管理接口 ComponentManager:

public interface ComponentManager {
    /**
     * register component in this manager
     *
     * @param componentInfo component that should be registered
     */
    void register(ComponentInfo componentInfo);

    /**
     * get concrete component by component name
     *
     * @param name component name
     * @return concrete component
     */
    ComponentInfo getComponentInfo(ComponentName name);
}复制代码

ComponentInfo 是一个接口,用于表示组件信息,目前包含两个具体实现,分别是 ServiceComponent 和 ReferenceComponent,分别表示服务与引用。ComponentName 是 ComponentInfo 的唯一标识,用于区分不同的 ComponentInfo 实现。

在 ComponentManager 的实现类中包含一个以 ComponentName 为 key,ComponentInfo 为 value 的 Map,用于存储所有注册过的 ComponentInfo:

public class ComponentManagerImpl implements ComponentManager {
    /** container for all components */
    protected ConcurrentMap<ComponentName, ComponentInfo> registry;
    
    // other definition

}复制代码

在发布 JVM 服务时,我们会调用 register 方法,将 JVM 服务注册到 ComponentManager 中,在发生服务调用时,我们会调用 getComponentInfo 方法,在 ComponentManager 中查找其他模块发布的服务并进行调用。

Root Application Context

SOFABoot 扩展自 Spring Boot,SOFABoot 应用是通过 SpringApplication.run(args) 方法启动的,在调用此方法后应用将产生一个 Spring 上下文,我们把它叫做 Root Application Context。Root Application Context 在 SOFABoot 模块化中是一个很特别的存在,它是每个 SOFABoot 模块上下文的 parent,这样设计的目的是为了保证开箱即用:SOFABoot 应用在每增加一个 Starter 定义时,Starter 中可能定义了一些 Bean,这些 Bean 默认只会在 Root Application Context 中生效,将Root Application Context 定义为每个 SOFABoot 模块上下文的 Parent 后,每个 SOFABoot 模块就能发现这些 Starter 新增的 Bean 定义,保证开箱即用。


除了会定义一些 Bean,Starter 还可能定义一些 BeanPostProcessor 和 BeanFactoryPostProcessor,对于这两类特殊的 Bean 定义,子上下文光能够发现 Bean 定义还不够,必须将这两类 Bean 的定义复制到当前上下文才能生效,所以我们在刷新 SOFABoot 模块对应的上下文时,会将 Root Application Context 中定义的所有 BeanPostProcessor 和 BeanFactoryPostProcessor 复制到 SOFABoot 模块的上下文中,这样一些 Starter 定义的 Processor 就可以直接在 SOFABoot 模块上下文中使用了,例如 runtime-sofa-boot-starter 中会定义 ServiceAnnotationBeanPostProcessor,该类主要用于实现注解发布服务,自动拷贝后,只要增加了 runtime-sofa-boot-starter 依赖,就支持在 SOFABoot 模块中基于注解发布服务。

Require-Module、Spring-Parent 以及并行启动模块

在刷新 SOFABoot 模块时,可能出现 A 模块中发布了一个 JVM Service,在 B 模块的某一个 Bean 的 init 方法里面需要调用这个 JVM Service,假设 B 模块在 A 模块之前启动了,那么 B 模块的 Bean 就会因为 A 模块的 JVM Service 没有发布而 init 失败,导致 Spring 上下文启动失败。此时,我们可以在 sofa-module.properties 中指定 Require-Module 来强制 A 模块在 B 模块之前启动。


在 SOFABoot 应用中,每一个 SOFABoot 模块都是一个独立的 Spring 上下文,并且这些 Spring 上下文之间是相互隔离的。虽然这样的模块化方式可以带来诸多好处,但是,在某些场景下还是会有一些不便,这个时候,你可以通过 Spring-Parent 来打通两个 SOFABoot 模块的 Spring 上下文。例如,可以将 DAL 模块作为 Service 模块的 Parent,这样 Service 模块就可以直接使用 DAL 模块定义的 DataSource 定义,无需将一个 DataSource 发布成一个 JVM Service。


SOFABoot 会根据 Require-Module 和 Spring-Parent 计算模块依赖树,例如以下依赖树表示模块B 和模块C 依赖模块A,模块E 依赖模块D,模块F 依赖模块E:

该依赖树会保证模块A 必定在模块B 和模块C 之前启动,模块D 在模块E 之前启动,模块E 在模块F 之前启动,但是依赖树没有定义模块B 与模块C,模块B、C与模块D、E、F之间的启动顺序,这几个模块之间可以串行启动,也可以并行启动。SOFABoot 默认会对模块进行并行启动,这样可以大大加快应用的启动速度。

实践建议

每个 SOFABoot 模块使用独立的 Spring 上下文,模块与模块之间通过 JVM Service 进行通信,在发布服务时,我们建议以服务维度进行服务发布,类似 RPC 的使用方法,提供一个 Facade 包,包含接口定义,然后由模块实现接口,并发布服务。


有些实现是不适合放在 SOFABoot 模块中定义的,例如 Controller 定义,Controller 定义是展示层的实现,而 SOFABoot 模块属于业务层,我们不建议也不支持在 SOFABoot 模块中定义 Controller 组件。Controller 组件的定义建议放在 Root Application Context 中,如果 Controller 组件需要调用 SOFABoot 模块发布的服务,可以直接使用注解的方式引用服务,具体的例子可以看 实操 | 基于 SOFABoot 进行模块化开发 中的例子。


有一些模块是不适合定义为 SOFABoot 模块的,例如 Util 模块或者 Facade 模块,前者主要定义一些工具类,后者主要定义一些服务接口,都不涉及服务的发布与引用,建议不要将这些模块定义为 SOFABoot 模块。

总结

本文主要介绍了 SOFABoot 模块化的实现原理,着重介绍了 SOFABoot 模块化中的三个重要概念:SOFABoot 模块的查找与刷新、JVM Service 与组件管理以及 Root Application Context 的基本概念,通过这三个重要概念的介绍能帮助用户快速理解 SOFABoot 模块化的实现原理。在原理解析之后,在文章最后我们还给出了实践建议,帮助用户快速上手 SOFABoot 模块化。


文章提到的:



长按关注,获取分布式架构干货

欢迎大家共同打造 SOFAStack https://github.com/alipay





展开阅读全文

没有更多推荐了,返回首页