Zhigang Zhang's Blog

an idealist's cheap talk

基于Servlet3.0的JavaEE模块化设计

一、概述

技术成果的复用对项目的成本、质量、进度等多方面都有比较积极的作用。复用大体可分为四个级别,最高级别的复用是产品级复用,最低级别的复用是技术团队的复用,中间的两个级别是组件级复用(特指业务组件)和代码级复用。对于产品型项目,自然是产品级复用;对于工程型项目,则不同的技术团队能达到的复用程度各不相同。

本文主要介绍的是基于 Servlet 3.0 进行模块化设计的思路,基于此思路可以基本做到组件级复用。

通过组件级复用,实现由多个组件灵活组合,构建一个完整 JavaEE 应用,达到快速交付的目的。

本文不会提供相应的源码下载

二、Servlet 3.0

Servlet 3.0 于 2010 年随 Java EE 6 规范一起发布。其中有几项特性比较引人注目:异步处理支持/注解支持/插件式支持,具体的内容请参考 IBM developerWorks 的这篇文章:《Servlet 3.0 新特性详解》。文章中提到:

使用该特性(插件式支持),现在我们可以在不修改已有 Web 应用的前提下,只需将按照一定格式打成的 JAR 包放到 WEB-INF/lib 目录下,即可实现新功能的扩充,不需要额外的配置。

Servlet 3.0 引入了称之为“Web 模块部署描述符片段”的 web-fragment.xml 部署描述文件,该文件必须存放在在 jar 文件的 META-INF 目录下,该部署描述文件可以包含一切可以在 web.xml 中定义的内容。

也即是说在 Servlet3.0 里的 web-fragment.xml 与 web.xml 几乎具备了同等的能力。只不过,在 web.xml 中保留了是否启用这个特性的配置开关,各 web-fragment.xml 也可以指定各自加载的顺序。

另外, jar 文件的 META-INF/resources 目录相当于 JavaEE 工程中的 web 根目录,web 根目录里的文件会覆盖 jar 文件中的同名文件。

三、需要先解决的问题

JavaEE 工程的模块化设计与一般的 Java 模块相比,除了要做好模块的标识、依赖、接口等的设计及规范之外,还要处理好页面风格、静态资源、数据存储等方面的问题。因为 JavaEE 的业务模块应当包含一个完整的业务功能,界面、逻辑、数据存储都应当在模块内,同时模块还应提供必要的接口或服务以供其它模块使用。以此为目标进行模块化设计,需要先解决几个关键的问题:

界面问题

如果业务模块需要包含业务功能的界面,那么如何确保不同的模块有相同的界面风格?

方法一:先设计一套界面,以规范的方式要求每个模块都按此风格进行开发,然而这样以后就不便于进行风格的调整了。

方法二:先定义一套样式名规则,以规范的方式要求每个模块的页面都使用此样式名规则,但模块内部并不包含任何样式表文件、图片、页面模板等,具体的样式表文件、图片、页面模板等则在另一个模块中提供。

依赖与引用问题

模块之间必然会有依赖,如何管理好模块的依赖?如何方便各模块互相调用?

可使用 Maven 进行依赖管理。为方便模块间互相调用,可以参考 OSGI 的模型实现,在模块中添加配置文件,或要求每个模块实现一个特定的接口以发布服务,或以规范的形式暴露指定的类包。使用 Spring 管理类之间的引用。

配置问题

如何即方便模块管理自身的配置,又方便构建的 JavaEE 应用统一管理所有的配置?

可重载 Spring 的 PropertyPlaceholderConfigurer 类,以通配符的形式配置多个 properties 文件,每个模块都可以有自己的配置文件,应用也有一个配置文件,而且应用的配置文件里的配置可以覆盖模块内的配置信息。

调试问题

如何调试业务模块?

可将模块项目作为 Web 项目进行调试,利用 Maven 的 tomcat 插件很容易做到这一点。另外在 Maven 中配置生成 jar 文件的同时生成 source 文件,还可以比较方便的调试到引用的模块内部。

四、模块化设计

上述几个关键问题解决掉以后,则可以进行如下的设计:

模块标准

事实上,模块化设计的本质是模块标准,模块标准中定义了模块内部的结构、外部的接口、以及命名与编程的规范等,所有的符合标准的 jar 文件都可称为模块。而这个标准就是条目化的各种规则,完全可以用一个文档来描述它,然而更理想的描述方法应该是一套承载了这个标准的技术框架或研发平台。

因此在总体设计中,有一个名为“核心”的模块(Core)和一个模块的模板(Template),为了能够比较清晰的描述这个思路,先由模板说起:

1)模板

模板提供了构建一个模块的基本示例,是模块标准的具体体现,主要包含三个方面的内容:

  • 模块的内部基本结构(包含文件结构与包结构)
  • 配置文件模板
  • 示例代码

模板以 Maven 的 archetype 方式提供,其内部结构大致如下所示:

template.jar
    └─src/main/java
        ├─com.company.groupid.artifactid
        │  ├─controller
        │  ├─dao
        │  ├─facade
        │  ├─model
        │  ├─service
        │  └─ModuleListener.java
        └─META-INF
            │  artifactid.properties
            │  MANIFEST.MF
            │  spring-beans-artifactid.xml
            │  web-fragment.xml
            │
            └─resources
                ├─static
                │  └─artifactid
                └─views
                    └─artifactid

模板内部的 artifactid/groupid 可分别用于标识模块的名称和所在的分组。关于模块内部的这些文件稍后会详细介绍。

2)核心

模板以示例的方式描述了模块的标准,而核心包则是对其它的模块进行约束。这个特殊的模块设计为需要完成以下三个目标:

  • 统一各关键组件的选型,并确定版本
  • 定义各关键组件的配置文件,约束包/类/文件的命名规范
  • 定义一系列接口,约束各模块的类的设计

本文介绍的模块化设计是以 Spring MVC/Spring/Mybatis 为基础进行架构的,核心模块承担了这个架构的基础配置。

核心模块也是基于 Servlet 3.0 构建的 jar 文件,具体而言,核心包做了以下几件事:

第一,在这个模块的 pom.xml 里添加了 Spring MVC/Spring 的依赖;

第二,定义了 controller/service/dao/facade 各种类的接口(或抽象类),另外自定义了一个模块级的监听器抽象类 AbstractModuleListener,为各模块提供启动和停止的事件;

第三,添加 web-fragment.xml 文件,其中配置了 Spring 容器、Spring MVC 容器以及log4j,另外在 Spring MVC 的 Servlet 之后添加了一个自定义的自启动 Servlet,在这个 Servlet 里扫描其它模块里的监听器,并触发对应的启动事件和停止事件;

信息交互与接口

模块基于 Spring 进行构建,因此只要模块已经建立起了依赖关系,那么模块里的类都是能访问到的,因此在信息交互方面要考虑的应该是如何避免不合理的信息流通途径。通过规范进行控制或许是比较容易实施的办法。

第一,在模板里添加一个 facade 包,此包里的类用于向其它模块提供接口,原则上各模块之间的其它类不应该相互调用。

第二,facade 包应避免直接向外输出底层的 model 类,而应该返回带有业务功能特点的业务数据。

第三,facade 包里的类应该继承自核心包中的一个基类,以便于将 facade 里的类发布为服务。

关于控制类的使用范围,可以通过自定义 spring 容器的方法进行模块间 bean 的隔离,不过本设计方案中并未如此处理。

界面风格一致性

界面风格必须要有一套样式规则,和多套皮肤样式。

样式规则明确规定界面中的各类组件的表达方法。例如按钮必须写成 <button class="btn-save">按钮文本</button>;表格必须添加class="table";表格必须包含<tbody>等,对每一种组件都进行如此规定后,就可以使用 JavaEE 自定义标签的方式将这些页面组件包装为 Java tag。

有了各组件的统一表达,便可以制作多种不同的样式,每一套样式也可以以独立的模块提供。

在 Web 应用里选择一种样式风格,利用 sitemesh 为界面中的每一个页面添加相对应的样式表的引用。

多数据库支持

Mybatis 自 3.1.1 之后,已支持在每个 mapper 文件中定义第一个 sql 适用的数据库类型。利用这个特性,可将对不同数据库支持的 mapper.xml 放在不同的 jar 文件里,这样可方便进行系统升级和扩展。

对于数据库分页的问题,可采用 Mybatis 的拦截器机制,修改将要执行的 sql 并添加分页的语句,分页部分的 sql 语句也要针对不同的数据库做兼容性处理,不过好在此问题已有多个开源实现,在此不再多讲。

部署与管理

在每个模块的监听器类中可以进行数据库的检测,如果发现数据表不存在,可以直接创建依赖的表,并填充初始数据。这只是其中一种解决办法。还可以将部署过程交由另一个独立的模块进行处理,本文介绍的方案包含了一个名为“部署”的特殊的模块。

部署模块能够扫描当前应用里有多少个“模块”,并通过读取模块里的 MANIFEST.MF 文件分辨出是否符合模块标准,将符合标准的模块挑选出来,并通过反射调用模块中的一些特殊的类进行模块的部署。

要实现到这一点,可利用 Maven 生成 jar 文件时,往 MANIFEST.MF 文件中写入一些信息,主要包括以下内容:

module-name:名称
module-version:版本
module-description:描述
module-manufacturer:开发商
module-manager-class:定义的此模块的管理类
build-time:生成时间

此模块的管理类需要继承核心里的抽象类,或实现核心里的接口,反射此类后,即可调用抽象类或接口的方法进行模块的部署。

如果还需要对模块进行其它的管理操作(例如卸载、重新部署等),在核心里的抽象类或接口中添加对应的方法就可以了。

五、总结

基于 Servlet 3.0 进行模块化设计提高了 JavaEE 工程的研发、部署与组织的效率,也为后续的升级提供了一些便利。本文介绍的这种方式在信息交互与接口方面还有较大不足,类比 OSGi 也有较多不完整的地方,但可在企业内部作为开发平台或技术框架,随工程项目不断演进。

Posted: