Java应用架构设计:模块化模式与OSGi
上QQ阅读APP看书,第一时间看更新

3.4 模块化:被忽视的部分

架构定义的两个关键因素是组件和组合(参见1.1节)。但是对于组件[1](实际上,这使我想起了架构也是如此)这个词还没有一个标准的和公认的定义。大多数的地方都使用这个词代表“一块代码”(a chunk of code)。但那是不合适的,在OSGi环境中,模块显然是一个软件组件。开发具备适应性、灵活性以及可维护性架构的系统需要模块化,这是因为我们必须设计灵活的系统,以便在开发过程中面临变化时,能够根据当时的情况做出决定。模块化一直是被忽略的一个部分,它能够让我们更容易地适应变化并且关注系统中那些需要灵活性的特定领域,如图3.5所示。改变封装在一个模块中的设计要比改变分散在多个模块中的设计更容易一些。

它是真正的封装吗

在标准的Java中,没有办法强制将设计细节封装在模块中,因为Java没有提供将包或类定义为模块作用域的方法。于是,一个模块中的类总是能够访问另一个模块的实现细节。这就是像OSGi这样的模块化框架,能够发挥作用的地方了,因为它能够在清单文件头中显式声明导入包和导出包,进而强制将实现细节封装在模块中。即便是包中公开的类,只要这个包没有显式声明为导出,其他的模块也不能进行访问。区别很细小,但是很重要。在贯穿本书的模式中,我们将会看到几个这样的例子,当这些例子出现时,我会明确指出。现在,先看一个简单的例子(关于在没有运行时模块系统环境下的模块化,参见2.3节)。

图3.5 封装性设计

1.标准Java:没有封装

图3.6展现了Client类要依赖Inter接口,Impl提供了实现。Client类打包在client.jar模块中,Inter和Impl打包在provider.jar模块中。这是一个关于模块化系统的好例子,但它展现了在标准Java中,我们是无法封装实现细节的,因为没有办法阻止对Impl的访问。provider.jar模块外的类依然可以接触到Impl类,并且能够直接实例化和使用它。

实际上,Impl是作为包作用域的类进行定义的,如程序清单3.1所示。但是部署在client.jar模块中的Spring XML配置文件AppContext.xml依然可以在运行时创建Impl实例并将其注入Client中。AppContext.xml和Client类分别如程序清单3.2和程序清单3.3所示。关键问题在于AppContext.xml部署在client.jar模块中,而它创建的Impl类部署在provider.jar模块中。如程序清单3.2所示,部署在client.jar模块中的AppContext.xml违反了封装性,因为它引用了provider.jar模块的实现细节。因为Spring的配置是全局配置,所以结果就是违反了封装性。

图3.6 标准Java不能封装模块中的设计细节

程序清单3.1 类Impl

程序清单3.2 Spring配置文件AppContext.xml

程序清单3.3 类Client

2.OSGi与封装

现在,来看看相同的例子用OSGi(参见第13章)如何实现。这里,provider.jar模块中的Impl类严格进行了封装,Impl对其他模块中的类是不可见的。Impl类和Inter接口与前面的例子中是一样的,不需要任何变化。我们将已存在的应用放在了OSGi框架中,它会强制封装模块的实现细节并提供模块间进行交流的一种机制。

图3.7展示了这种新的结构。实际上,它是抽象化模块模式(Abstract Modules pattern,参见11.1节)的一个样例。这里将Spring XML配置文件拆分成了4个不同的文件。本来我可以只使用两个配置文件,但是对于每个模块我希望将标准Java和OSGi框架的配置分离开。provider.jar模块负责对自己进行配置并在安装时暴露它的服务功能。在描述这种方式之前,这里对每个配置文件进行简单的介绍。

图3.7 使用OSGi实现的封装性设计

·client.xml:标准的Spring配置文件,它描述OSGi该如何启动应用。

·client-osgi.xml:Spring配置文件,它允许Client类使用OSGi μService。

·provider.xml:Spring配置,包含provider.jar模块中的bean定义。

·provider-osgi.xml:Spring配置,它将provider.xml中的bean定义为OSGi μService。

在了解这两个模块如何装配在一起之前,先看一下provider.jar模块,它包含了Inter接口、Impl实现以及两个配置文件。重复一遍,Inter和Impl与前面的样例是相同的,所以我们只看一下配置文件。图3.7中的provider.xml文件定义了标准的Spring bean配置,也就是之前AppContext.xml文件所展示的那样。程序清单3.4展现了provider.xml文件。关键的一点在于这个配置文件要部署在provider.jar模块中。不能试图在provider.jar模块外实例化Impl类。因为OSGi保证了封装,所以任何试图访问模块实现细节的行为将会导致运行时错误,如ClassNotFoundException异常。

程序清单3.4 provider.xml配置文件

OSGi如何阻止其他的类直接实例化Impl类呢?provider.jar模块中的清单文件(Manifest.mf)只提供了com.p2包中的类,并不含com.p2.impl包。所以,注册为OSGi μService的Inter接口能够被其他的模块访问,但是Impl类就不能被访问了。程序清单3.5展示了清单文件中导出包的那一部分。

程序清单3.5 provider.jar模块的Manifest.mf文件声明了所要导出的包

provider-osgi.xml文件使得事情变得很有意思,通过它将provider.jar模块的行为提供为OSGi μService,这个服务会作为Client和Impl之间的协议。为provider.jar模块提供配置的文件放在了provider.jar模块中,所以并没有违反封装的事情发生。

程序清单3.6展示了这个配置。使用OSGi框架注册的μService服务称为interService,它引用了程序清单3.4中定义的Impl bean,在这里以Inter类型提供行为。此时,provider.jar模块有一个名为interService的OSGi μService,它可以被其他的模块使用。在provider.jar模块在OSGi框架中安装并激活后,这个服务就可用了。

程序清单3.6 provider-osgi.xml配置文件

现在,看一下client.jar模块。client.xml文件会配置Client类。它实际上将程序清单3.3中Client类的main方法替换为run方法,OSGi框架会实例化Client类,使用Inter类型对其进行配置,并会调用run方法。程序清单3.7和程序清单3.8分别展现了client.xml文件和Client类。这种机制会进行初始化处理并取代之前样例中Client类的main方法。

程序清单3.7 client.xml配置文件

程序清单3.8 Client类

注入Client类中的Inter类型是通过client-osgi.xml配置文件完成的。这里声明需要使用一个Inter类型的μService,如程序清单3.9所示。

程序清单3.9 client-osgi.xml配置文件

client.jar模块的清单文件导入了com.p2包,这样就可以访问Inter μService了。程序清单3.10展示了client.jar模块导入和导出包这部分的清单文件配置。

程序清单3.10 client.jar模块的Manifest.mf文件

这个简单的例子有多个很有意思的设计。 [2]provider.jar模块是独立部署的(Independent Deployment pattern,独立部署模式,参见9.5节)。它不依赖其他模块,并且将自己的行为提供为μService。在这个系统中,没有其他模块需要知道这些细节。

将Impl类和Inter接口分别打包到不同模块中,会使设计更灵活。通过将接口从实现中分离,可以给系统带来更大的灵活性,尤其是在OSGi管理模块的情况下(Separate Abstraction pattern,分离抽象模式,参加11.3节)。

乍看起来,它似乎也违反了外部配置模式(External Configuration pattern,参见10.2节)。在为一个模块定义外部配置的时候,还希望能够保证将实现细节封装起来。外部配置更多的是允许客户端配置模块到上下文环境中,并不是关于提供模块实现细节的。

这个示例所能带给我们的最关键一点就是provider.jar模块中的类进行了严格的封装,因为OSGi框架强制实现了类型的可见性。我们只暴露了模块导出包中的公开类,μService机制使得模块间能够以一种很灵活的方式交流。μService跨越了系统的结合点(参见4.5节),因为OSGi是动态的,所以对μService的依赖也是如此。μService的实现可以在运行时添加或移除,系统可以在它们出现时绑定新的实例。

在后面的讨论中,会看到更多的例子。即使使用标准的Java无法强制封装模块的实现,但设计更加模块化的软件系统依然是很必要的。你将会看到,通过使用本书中讨论的一些技术,当使用运行时模块系统时,我们会占据一个很有利的位置。

[1] 在《构件化软件——超越面向对象编程》一书中,针对组件这个术语,Clemens Szyperski试图给出正式的定义,这是我看到的为数不多的尝试。他做得很棒。
[2] 尽管这个例子是基于OSGi Blueprint规范构建的,但你们可能并不喜欢XML。如果是这样,Peter Kriens有一个基于OSGi声明式服务的实现。示例代码可以在http://bit.ly/OSGiExamples的aQute.poma.basic目录下找到。