UML软件工程组织

软件体系架构模式在J2EE中的应用
刘兵 (cnj2ee@tom.com) 软件公司技术顾问
2003 年 10 月  来源:IBM 

本文介绍了软件体系架构产生的背景和架构模式的基本理论.重点介绍管道与过滤器体系架构模式的结构,实现,优缺点等,然后以J2EE的Servlet Filter为例进行剖析它是怎样应用该架构模式的,最后简单阐述了在其它J2ee应用中(Jboss和Axis)的实践.

1 软件体系架构

1.1 软件体系架构产生背景

在经历60年代的软件危机之后,使人们开始重视软件工程的研究。来自不同应用领域的软件专家总结了大量的有价值的知识. 当初,人们把软件设计的重点放在数据结构和算法的选择上,如Knuth提出了数据结构+算法=程序. 但是随着软件系统规模越来越大、越来越复杂,使软件系统的架构越来越重要。软件危机的程度日益加剧,现有的软件工程方法对此显得力不从心。对于大规模的复杂软件系统来说,软件体系架构比起对程序的算法和数据结构的选择已经变得明显重要得多。在此种背景下,人们认识到软件体系架构的重要性,并认为对软件体系架构系统、深入的研究将会成为提高软件生产效率和解决软件危机的最有希望的途径. 这时对软件体系架构的研究如雨后春笋般,出现了百家争鸣的现象,如Rational公司提出了"以架构为中心"的统一软件开发过程(RUP).

1.2 软件体系架构模式

软件设计的一个核心问题是能否使用重复的体系架构,即能否达到体系架构级的软件重用。也就是说,能否在不同的软件系统中,使用同一体系架构。基于这个目的,许多学者们开始研究和实践软件体系架构的模式问题。在<Pattern-Oriented Software Architecture (面向模式的软件体系架构) >中首次提出了8种体系结构模式: 层(L a y e r s)、管道和过滤器(Pipes and Filters) 、黑板( B l a c k b o a r d )、代理者( B r o k e r)、模型-视图-控制器( M o d e l - Vi e w - C o n t r o l l e r)、表示-抽象-控制(P r e s e n t a t i o n - A b s t r a c t i o n - C o n t r o l)、微核(M i c r o k e r n e l)、映像(R e f l e c t i o n)。

2 J2EE体系架构

目前,J2EE技术已经成为企业级应用的首选平台,基于J2EE技术构建的软件系统越来越多.J2EE代表着先进的软件体系架构思想,许多软件体系架构模式在J2ee中均被广泛应用,从本文起陆续介绍各架构模式在J2EE中的应用.

3 管道与过滤器

3.1 概述

管道和过滤器(Pipes and Filters)体系架构模式是为处理数据流的系统提供的一种模式。它是由过滤器和管道组成的.每个处理步骤都被封装在一个过滤器组件中,数据通过相邻过滤器之间的管道进行传输。每个过滤器可以单独修改,功能单一,并且它们之间的顺序可以进行配置。下图是管道/过滤器模式的示意图。一个典型的管道/过滤器体系结构的例子是以Unix shell编写的程序。Unix既提供一种符号,以连接各组成部分(Unix的进程),又提供某种进程运行时机制以实现管道。另一个著名的例子是传统的编译器。传统的编译器一直被认为是一种管道系统,在该系统中,一个阶段(包括词法分析、语法分析、语义分析和代码生成)的输出是另一个阶段的输入。


 

3.2 问题

假如你正在开发一个必须处理或转换输入数据流的系统。把这样的系统作为单个组件实现是不容易的,这有几个原因:系统必须由几个开发人员同时进行协作开发,整个系统任务自然就被分解为几个处理阶段,而且需求很容易变动。因此你就要通过替换或重新排序处理步骤来为将来的灵活性作规划。通过加入这样的灵活性,采用现有处理组件构建是可以办到的。系统的设计尤其是处理步骤的内部连接,必须考虑以下因素:

  • 未来系统的升级通过替换某些处理步骤,或重组步骤.
  • 不同的语境中小的处理步骤要比大的组件更易于重用。
  • 不相连的处理步骤不可共享信息。
  • 存在不同的输入数据源,
  • 可以用多种方式输出或存放最终结果。

3.3 解决方案与结构

管道和过滤器体系架构模式把系统任务分成为几个独立的处理步骤。这些步骤采用通过系统的数据流连接.一个步骤的输出是下一个步骤的输入。每个处理步骤由一个过滤器组件实现,它处理或者转化数据,并且系统的输入可以是多种数据源.

这种体系架构模式具有许多特性,如下:

  • 过滤器是独立运行的部件.也就是除了输入和输出外,每个过滤器不受任何其他过滤器运行的影响.在设计上,过滤器之间不共享任何状态信息.
  • 独立性还表现在它对其处理的上游和下游连接的过滤器是"无知"的.它的设计和使用不对与其连接的任何过滤器施加限制,唯一关心的是其输入数据的,然后进行加工处理,最后产生数据输出.

3.4 非软件描述

基于各种流体工作的系统,普遍都采用由输送管道连接起来的处理结构.例如我们冬天见到的供暖系统中,处理器包括加热器,过滤器,调节阀,流量表等,每个处理器都有流体的入口和出口,它们通过各种管道连接在一起形成了整个系统.这样的结构在城市的自来水系统也可以看到.见下图:


 

3.5 优点与缺点

3.5.1 优点

  • 通过使用过滤器交换增加了灵活性
  • 通过重组增加了灵活性
  • 过滤器组件的重用
  • 流水线的快速原型
  • 并行处理提高效率

3.5.2 缺点

  • 共享状态信息或者昂贵或者不灵活
  • 数据转换额外开销。
  • 错误处理

4 Servlet2.3 Filter

4.1 Servlet Filter概述

凡是开发过J2EE的web application的人员都知道,经常需要处理以下几种情况:

  • 访问特定资源(Web 页、JSP 页、servlet)时的身份认证
  • 应用程序级的访问资源的审核和记录
  • 应用程序范围内对资源的加密访问,它建立在定制的加密方案基础上
  • 对被访问资源的及时转换, 包括从 servlet 和 JSP 的动态输出

在servlet2.3之前这些功能处理是很难实现的,但是Java Servlet 2.3 规范新增了不少激动人心的功能,其中之一便是过滤器(Filter),其实这就是我们所说的管道和过滤器体系架构在J2EE中的应用实践. 通过使用该模式使得Web Application开发者能够在请求到达Web资源之前截取请求,在处理请求之后修改应答。其结构图如下:


 

一个执行过滤器的Java 类必须实现javax.servlet.Filter 接口。这一接口含有三个方法:

  • init(FilterConfig):这是容器所调用的初始化方法。它保证了在第一次 doFilter() 调用前由容器调用。它能获取在 web.xml 文件中指定的filter初始化参数。
  • doFilter(ServletRequest, ServletResponse, FilterChain):这是一个完成过滤行为的方法。它同样是上一个过滤器调用的方法。引入的 FilterChain 对象提供了后续过滤器所要调用的信息。
  • destroy():容器在销毁过滤器实例前,doFilter()中的所有活动都被该实例终止后,调用该方法。


 

4.2 Filter链介绍

所有过滤器都服从调用的过滤器链,并通过定义明确的接口得到执行。WebApplication可以指定许多过滤器来完成相关的工作.那么它们就组成一个过滤器链来完成相应的工作.其结构如下图:


 

4.3 例子

4.3.1 简单filter

在PetStore1.3.1中的就存在两个Filter过滤器.其中一个过滤器,完成字符集的编码的转化,如大家经常遇到的汉字编码问题,你只需配置为GBK即可.它从Web.xml之中读取这些参数的配置信息,然后进行编码的转化.另一个是安全校验Fliter,它负责进行安全检查哪些页面可以进行,哪些不可.它们组成一个Filter链,结构图和实现代码如下(完整代码参见附件):


 


public class EncodingFilter implements Filter {
    private FilterConfig config = null;
    // default to ASCII
    private String targetEncoding = "ASCII";

    public void init(FilterConfig config) throws ServletException {
        this.targetEncoding = config.getInitParameter("encoding");
    }
     //在过滤器中实现字符集编码转化
     public  void doFilter(ServletRequest srequest, ServletResponse  sresponse, FilterChain chain)
        throws IOException, ServletException {

        HttpServletRequest request = (HttpServletRequest)srequest;
        request.setCharacterEncoding(targetEncoding);
        // move on to the next
       chain.doFilter(srequest,sresponse);
    }
    public void destroy() {
	    ……………..
    }	
}

public class SignOnFilter implements Filter {
      public void init(FilterConfig config) throws ServletException {
        this.config = config;
        URL protectedResourcesURL = null;
        try {
            protectedResourcesURL = config.getServletContext().getResource("/WEB-INF/signon-config.xml");
            ...............
        } catch (java.net.MalformedURLException ex) {
            System.out.println("SignonFilter: malformed URL exception: " + ex);
        }
    }

    public void destroy() {
        config = null;
    }
    public  void doFilter(ServletRequest request, ServletResponse  response, FilterChain chain)
        throws IOException, ServletException {
        
        ........
    }
}

容器通过 Web 应用程序中的配置描述符 web.xml 文件解析过滤器配置信息。有两个新的标记与过滤器相关:<filter> 和 <filter-mapping>。<filter> 标记是一个过滤器定义,它必定有一个 <filter- name> 和 <filter-class> 子元素。<filter-name> 子元素给出了一个与过滤器实例相关的名字。<filter-class> 指定了由容器载入的实现类。您能随意地包含一个 <init-param> 子元素为过滤器实例提供初始化参数。<filter-mapping> 标记代表了一个过滤器的映射,指定了过滤器会对其产生作用的 URL 的子集。


    <!-- Encoding Filter Declaration Start -->
  <filter>
    <filter-name>EncodingFilter</filter-name>
    <display-name>Encoding Filter</display-name>
    <description>no description</description>
    <filter-class>com.sun.j2ee.blueprints.encodingfilter.web.EncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
  </filter>
  <!-- Encoding Filter Declaration End -->
  <!-- Signon Filter Declaration Start -->
  <filter>
    <filter-name>SignOnFilter</filter-name>
    <display-name>SignOn Filter</display-name>
    <description>no description</description>
    <filter-class>com.sun.j2ee.blueprints.signon.web.SignOnFilter</filter-class>
  </filter>
  <!-- Signon Filter Declaration End -->

  <!-- Encoding Filter Mapping Start-->
  <filter-mapping>
    <filter-name>EncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
  <!-- Encoding Filter Mapping End -->
  <!-- Signon Filter Mapping Start-->
  <filter-mapping>
    <filter-name>SignOnFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
  <!-- Signon Filter Mapping End -->
  

4.3.2 复杂的filter

上面是petstore的例子,演示了通过Fliter修改字符编码和安全认证的功能.下面提供一个示例演示通过修改返回数据(通过过滤器把response的字符串变成大写).


 public class UCaseResponse extends HttpServletResponseWrapper {
    public UCaseResponse(HttpServletResponse response) {
        super(response);
    }

    public PrintWriter getWriter() throws IOException {
        return new UCaseWriter(super.getWriter());
    }
}

public class UCaseWriter extends PrintWriter {
    public UCaseWriter(Writer out) {
        super(out);
    }
    public void write(int c) {
        super.write(Character.toUpperCase( (char) c));
    }
    public void write(char buf[], int off, int len) {
        for (int i = 0;i < len;i++) {
            write(buf[off + i]);
        }
    }
    public void write(String s, int off, int len) {
        for (int i = 0;i < len;i++) {
            write(s.charAt(off + i));
        }
    }
} 

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) {
        try {
            filterChain.doFilter(request, new UCaseResponse((HttpServletResponse)(response)));
        }catch(Exception sx) {
            filterConfig.getServletContext().log(sx.getMessage());
 }
 

该示例使用HttpServletResponseWrapper技术,它是对HttpServletResponse的包装,其实就是装饰(decorate)设计模式的应用.这个例子能够工作的关键是UCaseResponse和UCaseWriter类,它实现了对每个要输出的字符都转成了大写后再写入实际的输出流的功能。

4.4 体系架构的实现

实现一个管道和过滤器一般要注意以下几个方面:

  • 把系统任务分成一系列处理阶段。
    根据管道和过滤器的设计方案,必须把系统处理的任务分割成相应独立的任务,如日志,数据转化,安全认证等.这样每个阶段仅依赖其前一阶段的输出。通过数据流将所有阶段相连起来。并且你可以进行替换每个步骤,或者可以调整它们之间的顺序,以产生新的结果.
    如petstore中的编码转化Filter和安全Filter,分成两个独立的处理阶段.
  • 定义沿每个管道传输的数据格式。
    我们知道每个过滤器,定义一个统一格式以获得最大的灵活性,因为它使过滤器的重组变得容易。如:每个过滤器的方法是doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)它们的参数是必须相同的.
  • 决定如何实现每个管道连接
    Filter过滤器的连接是推得方式来实现的.前一个过滤器主动的调用filterChain.doFilter(request, response);来实现转向下一个过滤器.
  • 设计和实现过滤器
    设计每个Filter具有独立的功能,如编码转化,安全校验,等功能.并且每个Fliter都应该在实现javax.servlet.Filter接口.
  • 建立处理流水线
    过滤器的部署是在Web.xml中进行配置,描述过滤器的实现类以及它们的map关系,来确定它们的顺序.

5 其他应用实例

5.1 JBOSS

如果大家对EJB了解,应该知道客户调用EJB实际上并不是直接引用EJB实例(ejb instance).它通过容器来截获客户端的请求,然后按照EJB描述符做些许多相应的工作如,安全校验,事务的处理,线程并发处理等.这样就可以使开发人员仅关心自己的业务逻辑,而不需对复杂的基础服务进行实现.使开发人员从繁琐的工作中解脱出来.集中精力处理自己的业务逻辑,它的结构图如下:


 

笔者有幸阅读了JBOSS的源码,分析了Jboss的EJB容器的实现. EJB的容器通过许多拦截器(Inteceptor)来实现,每个拦截器处理一定的功能,一个处理结束后转发给下一个拦截器,最后一个拦截器才把真正调用EJB的实例.其中它的EntityBean 容器的拦截器的结构如下:


 

我们看其中的log拦截器


   public class LogInterceptor extends AbstractInterceptor{

public Object invoke(Invocation invocation)
      throws Exception
   {
      boolean trace = log.isTraceEnabled();
      
      // Log call details
      if (callLogging)
      {
	......进行log的处理
      }

      //处理结束,把请求转发给下一个拦截器
      return getNext().invoke(invocation);
      
   }	   
   

这些拦截器配置在standardjboss.xml文件中,如下:


	   <container-interceptors>
	    <interceptor>org.jboss.ejb.plugins.LogInterceptor</interceptor>
	    <interceptor>org.jboss.ejb.plugins.SecurityInterceptor</interceptor>
	    <interceptor>org.jboss.ejb.plugins.TxInterceptorCMT</interceptor>
	    <interceptor metricsEnabled="true">org.jboss.ejb.plugins.MetricsInterceptor</interceptor>
	    <interceptor>org.jboss.ejb.plugins.EntityLockInterceptor</interceptor>
	    <interceptor>org.jboss.ejb.plugins.EntityInstanceInterceptor</interceptor>
	    <interceptor>org.jboss.resource.connectionmanager.CachedConnectionInterceptor</interceptor>
	    <interceptor>org.jboss.ejb.plugins.EntitySynchronizationInterceptor</interceptor>
	    <interceptor>org.jboss.ejb.plugins.cmp.jdbc.JDBCRelationInterceptor</interceptor>
	 </container-interceptors>
	 

这就是Jboss容器架构最巧妙的地方,最初这个架构就是由天才少年Rickard Oberg提出的.其实这个架构就应用我们讨论的管道和过滤器模式,其实每个拦截器就是一个过滤器,使用该模式给Jboss带来了如下的好处:

  • 使系统的架构更容易理解,因为每个过滤器完成单一的功能
  • 使系统更加模块化,有利于系统的模块重用和扩展,如果系统想增加某种功能只需增加一个实现Interceptor接口的拦截器,然后配置在standardjboss.xml文件中即可
  • 使系统的容易进行错误处理,如果在一个拦截器中发现错误(error)或者异常(exception),只需返回即可.

5.2 AXIS

无独有偶,同样在Axis上也应用了管道和过滤器模式.Aixs是apache开源的webservice实现服务器. 简单的说,axis就是处理Message,它首先截获客户端的请求,然后转发到真正的实现业务逻辑上处理客户端的请求,在这之前经过一系列的handler处理.它的结构很像EJB容器.其实就是管道和过滤器模式的应用,Handler就是过滤器.它的处理顺序主要考虑两个方面一个是部署描述符(deployment configuration )另一个就是是客户端还是服务器端.Handler处理的对象是MessageContext它的由3个重要的部分组成,一是一个request Message,一个是response message,还有许多属性.

我们经研究源码分析,在服务器端,有一个Transport Listener 它监听客户端的请求, 可以通过多种协议,一旦有客户请求,它将按照协议的规范把数据解析生成生成一个Message对象,然后把它设置到MessageContext,然后调用一系列的Handler进行处理.

其结构图如下:


 

6 相关设计模式

在使用管道和过滤器模式时,一般会使用以下的GOF设计模式.

6.1 职责链模式(Chain Of Responsibility)

职责链设计模式的意图就是使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。其实管道和过滤器模式就是职责链模式的抽象,把它应用到软件体系架构中.

6.2 命令模式(Command)

命令模式的意图将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。在管道和过滤器模式中每个过滤器一般使用命令模式,把请求封装成一个命令进行处理.

6.3 装饰模式(Decorator)

装饰模式的意图就是动态地给一个对象添加一些额外的职责。就增加功能来说,Decorator模式相比生成子类更为灵活。

在管道和过滤器模式中,在每个过滤器中经常需要对请求进行动态的增加功能,或者修改请求的内容,这时一般会使用装饰模式.如Servlet filter的javax.servlet.http.HttpServletRequestWrapper, javax.servlet.http.HttpServletResponseWrapper就是装饰模式的应用.

7 总结

本文讨论了管道和过滤器模式的解决方案,以及它的优缺点.然后以J2EE规范的Servlet Filter为例详细介绍了怎样应用该模式,同时简单的介绍了在Jboss,Axis中的应用.

8 参考资料

  • 面向模式的软件体系结构 卷1:模式系统
    Pattern-Oriented Software Architecture, Volume 1: A System of Patterns 作者: Frank Buschmann,Regine meunier,Hans Rohnert,Peter Sommerlad,Michael Stal
  • 软件体系结构的原理,组成与应用 万建成 卢雷
  • <程序员> 6.8.9期 软件体系架构 张友生
  • 软件体系结构(Software Architecture)-门初露端倪学科的展望(英文影印版)
  • 设计模式:可复用面向对象软件的基础 Erich Gamma 等

9 关于作者

刘兵,某软件公司技术顾问,有5年多的J2EE软件开发经验和多年的项目管理实践经验.目前研究兴趣在J2EE,OOA/D,设计模式,软件体系架构等.可以通过cnj2ee@tom.com与他联系.

 

版权所有:UML软件工程组织