模块隔离

模块隔离

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

业务分层

当这个系统承载的业务变多了之后,系统可能演化成上图中右边的这种方式。在上图的右边的部分中,一个系统承载了两个业务,一个是 Cashier(收银台),另一个是 Pay(支付),这两个业务可能会有一些依赖的关系,Cashier 需要调用 Pay 提供的能力去做支付。但是在这种模块化的方案里面,Spring 的上下文依然是同一个,类也没有任何做隔离,这就意味着,Pay Service 这个模块里面的任何的一个 Bean,都可以被 Cashier Service 这个模块所依赖。长此以往,模块和模块之间的耦合就会越来越严重,原来的模块的划分形同虚设。

当系统越来越大,最后需要做服务化拆分的时候,就需要花费非常大的精力去梳理模块和模块之间的关系。我们来区分几个常见的模块化形式:

  • 基于代码组织上的模块化:这是最常见的形式,在开发期,将不同功能的代码放在不同 Java 工程下,在编译期被打进不同 jar 包,在运行期,所有 Java 类都在一个 classpath 下,没做任何隔离;
  • 基于 Spring 上下文隔离的模块化:借用 Spring 上下文来做不同功能模块的隔离,在开发期和编译期,代码和配置也会分在不同 Java 工程中,但在运行期,不同模块间的 Spring Bean 相互不可见,DI 只在同一个上下文内部发生,但是所有的 Java 类还是在同一个 ClassLoader 下;
  • 基于 ClassLoader 隔离的模块化:借用 ClassLoader 来做隔离,每个模块都有独立的 ClassLoader,模块与模块之间的 classpath 不同,SOFAArk 就是这种模块化的实践方式。

OSGi 模块化

提到模块化,不得不提 OSGi,虽然 OSGi 没有成为 Java 官方的模块化的标准,但是由于 Java 在 Java 9 之前,一直没有官方的模块化的标准,所以 OSGi 已经是事实上的标准。OSGi 为模块化主要做了两个事情:OSGi 的类隔离、OSGi 的声明式服务。

OSGi 的类隔离

OSGi 通过扩展 Java 的 ClassLoader 机制,将模块和模块之间的类完全隔离开来,当一个模块需要引用另一个模块的类的时候,通过在模块中的 MANIFEST.MF 文件中声明类的导出和导入来解决,如下图所示:

OSGi 类隔离

通过这种方式,可以控制一个模块特定的类才可以被另一个模块所访问,达到了一定程度地模块的隔离。但是,光通过类的导出导入来解决类的引用问题还不够,还需要去解决实例的引用的问题,我们往往希望能够直接使用对方模块提供的某一个类的实例,而不是自己去 new 一个实例出来,所以 OSGi 还提供了声明式服务的方式,让一个模块可以引用到另一个模块提供的服务。

OSGi 的声明式服务

OSGi 的声明式服务正是为了解决这个实例引用的问题,我们可以在一个 OSGi 的模块(Bundle)中去添加一个 XML 配置文件去声明一个服务,如下面的代码所示:

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="ITodoService">
   <implementation class="com.example.e4.rcp.todo.service.internal.MyTodoServiceImpl"/>
   <service>
      <provide interface="com.example.e4.rcp.todo.model.ITodoService"/>
   </service>
</scr:component>

也可以同样的通过 XML 配置文件去引用一个其他的模块声明的服务:

<?xml version="1.0" encoding="UTF-8"?>
<scr:component xmlns:scr="http://www.osgi.org/xmlns/scr/v1.1.0" name="XXXService">
    <reference name="ITodoService"
            interface="com.example.e4.rcp.todo.model.ITodoService"
            bind="setITodoService" cardinality="0..1" unbind="unsetITodoService"
            policy="dynamic" />
   <implementation class="com.example.e4.rcp.todo.service.internal.XXXServiceImpl"/>
</scr:component>

通过声明式服务的方式,我们就可以直接在一个 OSGi 的 Bundle 中使用另一个 Bundle 中提供的服务实例了。

OSGi 的模块化的问题

OSGi 通过类隔离的机制解决了模块之间的类隔离的问题,然后通过声明式服务的方式解决了模块之间的服务调用的问题,看起来已经完美的解决了我们在传统的模块化中遇到的问题,通过这两个机制,模块和模块之间的边界变得清晰了起来。

但是在实践的过程中,OSGi 对开发者的技术要求比较高,并不是非常适合于业务研发。

SOFA 模块化

为了解决传统的模块化方案模块化不彻底的问题,以及 OSGi 的彻底的模块化带来的复杂性的问题,SOFA 在早期就开始引入了一种折衷的模块化的方案。

SOFA 模块化整体示意

SOFA 模块化的方案,给每一个模块都提供了一个单独的 Spring 的上下文,通过 Spring 上下文的隔离,让模块和模块之间的 Bean 的引用无法直接进行,达到模块在运行时隔离的能力。

SOFABoot 模块化示意

SOFABoot 框架定义了 SOFABoot 模块的概念,一个 SOFABoot 模块是一个包括 Java 代码、Spring 配置文件、SOFABoot 模块标识等信息的普通 Jar 包。一个 SOFABoot 应用可以包含多个 SOFABoot 模块,每个 SOFABoot 模块都含有独立的 Spring 上下文。

以 SOFABoot 模块为单元的模块化方式为开发者提供了以下功能:

  • 运行时,每个 SOFABoot 模块的 Spring 上下文是隔离的,模块间定义的 Bean 不会相互影响;
  • 每个 SOFABoot 模块是功能完备且自包含的,可以很容易在不同的 SOFABoot 应用中进行模块迁移和复用,只需将 SOFABoot 模块整个拷贝过去,调整 Maven 依赖,即可运行。

当一个模块需要调用另一个模块里面的一个 Bean 的时候,SOFA 采用了类似于 OSGi 的声明式的服务的方式,提供服务的模块可以在其配置文件(也可以通过 Annotation 的方式来声明)中声明一个 SOFA Service:

<sofa:service ref="sampleBean" interface="com.alipay.sofaboot.SampleBean"/>

使用服务的模块可以在其配置文件(也可以通过 Annotation 来使用)声明一个 SOFA Reference:

<sofa:reference id="sampleBean" interface="com.alipay.sofaboot.SampleBean"/>

通过这种方式,一个模块就可以清晰地知道它提供了哪些服务,引用了哪些服务,和其他的模块之间的关系也就非常清楚了。但是 SOFA 的模块化方案中并没有引入类隔离的方案,这也是为了避免研发的同学去处理太复杂的类加载的问题,简化研发的成本。

上一页