模块隔离

模块隔离

在一个简单的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官方的模块化的标准,但是由于JavaJava 9之前,一直没有官方的模块化的标准,所以OSGi已经是事实上的标准。OSGi为模块化主要做了两个事情:OSGi的类隔离、OSGi的声明式服务。

OSGi的类隔离

OSGi通过扩展JavaClassLoader机制,将模块和模块之间的类完全隔离开来,当一个模块需要引用另一个模块的类的时候,通过在模块中的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>

通过声明式服务的方式,我们就可以直接在一个OSGiBundle中使用另一个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的模块化方案中并没有引入类隔离的方案,这也是为了避免研发的同学去处理太复杂的类加载的问题,简化研发的成本。

上一页