第3章 提纲挈领—-Struts2概览
3.1 Struts2的来世今生
作为Apache旗下非常重要的开源项目,Struts这个项目的历史发展进程可以说比较特殊。Struts2的来世今生,从一个侧面反映出了整个Web开发的发展历程。在这里,我们简单对Struts项目的发展历程做一个简单的介绍,从而帮助大家对Web层框架的发展有更加深刻的了解。
查阅一下Struts的历史就可以发现,最早的Struts可以追溯到2000年3月31日,当时Apache有一则新闻,宣布开始编写一个新的Web开发框架—Struts。
Craig McClanahan has contributed a small framework for building web applications using the Model-View-Controller (MVC) design pattern commonly known as "Model 2" to the Jakarta project. The "Struts" project will serve as a platform for exploring the optimum approaches to implementing this design pattern, in a manner that can facilitate tool-based generation of application components.
项目取名为“Struts”,有“基础构建”的含义,在那个开发框架尚处于朦胧阶段的年代,“基础构建”无疑是每个程序员梦寐以求的东西。在新闻发布后不久,Apache便发布了Struts1.0.X的若干个版本和针对Struts1.0.X的升级版本Struts1.1.X。自此以后,Struts1.X系列开发框架就开始在世界范围内流行起来。在很长一段时间里,其流行程度几乎可以说垄断了整个Web开发领域,成为Web开发领域的实际开发标准。
Struts1.X系列开发框架能够如此流行的主要原因在于:首先,它是Apache出品,所谓树大好乘凉,Apache巨大的社区开发优势对它在全世界的流行起到了不可估量的作用;其次,Struts1.X也是较早实现MVC模式中“Model 2”概念的Web开发框架,在一定程度上它占有“天时”方面的优势;最后,各大中间件厂商尤其是IBM等公司对Struts1.X的巨大支持,为Struts1.X的推广起到了推波助澜的作用。
随着时代的进步,越来越多的程序员在使用Struts1.X进行开发的过程中发现Struts1.X在设计上存在着严重不足。与此同时,各种各样的Web层开发框架也如雨后春笋般涌现出来。在这些框架中,有一个来自于Opensymphony开源社区的优秀框架Webwork2逐渐被大家熟识和理解,并不断发展壮大。2004年12月14日,Webwork2.1.7版本发布,成为Webwork2的一个重要里程碑,它以优秀的设计思想和灵活的实现,吸引了大批Web层开发人员投入它的怀抱。在这之后,Webwork2.2.X的若干个版本依次发布,并开始支持JDK1.5的相关特性,极大地解放了Web层开发的生产力。
或许是看到了Struts1.X发展上的局限性(主要是Struts1.X在设计思想和技术实现上的种种局限性),Apache社区与Opensymphony开源组织在2005年12月14日宣布未来的Struts项目将与Webwork2项目合并,并联合推出Struts2,通过Apache社区的人气优势与OpenSymphony的技术优势,共同打造下一代的Web层开发框架。当时的新闻写到:
Apache Struts, the leading web application framework for Java, and Open Symphony WebWork, a leader in technical innovation, are working to merge their communities and codebases.
从这次合并中,我们可以非常明显地看到Apache社区在技术层面上的一个重大妥协。虽然Struts1.X借助Apache的社区力量占领了市场和人气,然而在技术上,Webwork2却完胜Struts1.X,合并之后的Struts2将完全采用Webwork2作为其代码的基础,摒弃Struts1.X的所有设计和代码。
所以,研究Struts2时必须明确一个非常重要的结论:
结论 Struts2来自于Webwork2,并且与Struts1.X完全不兼容。
认识这一点对所有Struts开发人员来说非常重要。而本书所讲解的内容,也全部都是针对Struts2而不是Struts1.X,这将成为我们之后所有讨论的基础。
经过几年的发展,Struts2又陆续发布了Struts2.0.X、Struts2.1.X和Struts2.2.X的若干个版本,伴随着版本的不断升级,我们不仅从中获得了越来越便利的开发模式,也可以体会到整个Web开发的发展历程,从一个侧面印证了技术的发展与设计思想的不断进步。
3.2 Struts2面面观
在深入研究Struts2内部的实现机理之前,我们将从Struts2的运行环境、应用场景以及核心技术这三个方面对Struts2进行简单的介绍。这三个不同的方面是描述Struts2外部环境的三个不同角度,能够帮助我们站在一个更高的高度对Struts2作为表示层框架在Web开发中所起的作用和Struts2与其他核心技术之间的关系有一个更深刻的理解。
3.2.1 Struts2的运行环境
在上一章中,我们就谈到本书的核心内容是通过对Struts2的分析,帮助读者领略Web开发的方方面面。从这一点上讲,我们的话题也就始终无法脱离Web开发以及Web应用程序的运行环境。Struts2在典型的三层结构的开发模式中被视作“表示层”的解决方案。因而,其最为核心的内容就是和Web容器打交道,帮助我们处理Http请求。
结论 Struts2是一个运行于Web容器的表示层框架,其核心作用是帮助我们处理Http请求。
请读者牢记这一条结论,因为这条结论不仅是整个Struts2框架的指导思想,也规定了Struts2作为一个框架所涉及的问题作用域。从这个结论中,我们很容易得到与Struts2的运行环境相关的两条推论:
推论 Struts2的运行环境是Web容器。
这条推论显而易见,其中却也隐含着许多重要内容。而最为重要的就是Struts2对其运行环境的Web容器有版本要求。我们知道,运行于Web容器中的程序,首先必须遵循基本的开发标准和规范:Servlet标准和JSP标准等等。而Java中的Web开发标准有着不同的版本,不同的Web服务器对于Servlet标准和JSP标准的支持程度也是不同的。对于Struts2而言,它所支持的Servlet标准的最低版本要求是2.4,相应的JSP标准的最低版本要求是2.0。这就对使用Struts2作为开发框架的应用程序的运行环境提出了要求,这一要求就像Struts2要求必须运行在JDK1.5版本之上一样。
结论 Struts2通过扩展实现Servlet标准来处理Http请求。
如果把整个Web容器看作是一个黑盒,Struts2无非也只是黑盒中所运行的一段程序代码段,如图3-1所示。
图3-1 Web容器的黑盒模型
从图中我们可以看到,Struts2这个框架在其中的作用实际上只是处理Http请求(Request),并进行内部处理(处理的过程被黑盒屏蔽了,我们暂时也无须关心),再进行Http返回(Response)。而整个这个过程的代码级别的实现,无论如何进行封装,都离不开对Servlet标准或者JSP标准所指定的底层API的调用。从这个角度来说,Struts2只是实现了一个具备通用性的Http请求处理机制,并且能够被我们的应用程序使用和扩展而已。
我们在这里花费一些笔墨向读者强调Struts2运行环境的目的在于提醒读者不应该忘记程序运行的基础条件。不要把任何一个框架看得很神秘,它们的本质也只不过是实现了基本的开发协议或者开发规范的程序集合而已。
3.2.2 Struts2的应用场景
在了解了Struts2的运行环境之后,Struts2的应用场景的问题也就能够迎刃而解了。当我们使用Java语言编写一个Web应用程序时,Struts2或许就能够帮得上忙。这就是Struts2的应用场景:帮助我们编写Web应用程序。
细心的读者会发现我们在这里使用了“或许”两个字。为什么要使用“或许”呢?Struts2是否能够成为所有Web应用的最佳选择呢?这个问题,我们还得回到上一章中曾经提到过的一个Struts2的官方表述,如图3-2所示。
图3-2 Struts2官方对Struts2应用场景的解释
非常明显,Struts2官方为我们描述的Struts2的应用场景是:
结论 Struts2并不适合所有的Web应用,但它却是复杂的、可扩展的Web应用的一剂良药。
因为当需要建立一个由复杂的业务逻辑和众多页面构成的Web应用时,我们不得不采用分层开发模式。例如,我们可以采用典型的三层开发模式,将表示层、业务逻辑层和持久层的逻辑完全分开,从而获得更好的程序可扩展性和可维护性。此时,Struts2就可以作为一个表示层的解决方案,帮助我们进行与表示层相关的开发工作,并提供在表示层范围之内高度的可扩展性和可维护性。
由此可见,在讨论Struts2的应用场景时,必须首先明确我们要开发的Web应用的规模和实际情况,并且理解Struts2自身的作用范围,从而能够合理地选择最适合的开发框架。
3.2.3 Struts2的核心技术
作为一个Web开发的解决方案,Struts2并不是一个可以独立运行的开源框架。Struts2的实现,首先将基于最为基本的Web开发标准。除此之外,Struts2自身还依赖于一些其他的开源框架和解决方案。我们在这里简单归纳一下这些Struts2的实现所涉及的核心技术,在本书的核心技术篇中,我们将对其中的核心技术进行详细分析。
3.2.3.1 Struts2与表示层技术
Struts2首先运行于Web容器之中。因而,它的核心依赖就是Web容器对于Servlet标准和JSP标准的实现。我们在之后的章节中对Struts2的主要分析,也将围绕着Struts2如何扩展实现这些基本的标准实现来展开。
作为一个服务于表示层的解决方案,Struts2有时候不得不与许多其他的表示层技术进行整合。例如,以Freemarker或者Velocity为核心的模板技术、构建Flash应用的Flex技术、Ajax技术等等。这些技术往往本身自成体系,而Struts2需要做的只是通过扩展实现一些Servlet标准与这些技术进行底层沟通从而完成与这些技术的整合。
Struts2与这些表示层技术的整合,往往通过“插件”的方式进行。有关插件(Plugin)的相关知识,我们将在本书的第12章中重点展开。在这里,我们所需要了解的是:开发人员可以根据项目实际情况,选择合适的插件并将其引入到Struts2的运行环境中。Struts2在运行时能够自动识别这些插件,从而完成与相关技术的自动整合。
3.2.3.2 Struts2与设计模式
设计模式(Design Pattern)是我们在日常编程中经常听到的一个名词。实际上所有的设计模式只不过是代码级别最佳实践的具体表现。
因此,任何设计模式都不是什么核心技术,也不是Struts2的实现严格依赖的内容主体。然而,Struts2的内部实现却离不开这些设计模式。有一些核心的设计模式,甚至贯穿了整个Struts2的逻辑主线,成为Struts2内部实现中不可或缺的重要组成部分。
在Struts2中,我们将先后接触到命令(Command)模式、ThreadLocal模式、装饰(Decorator)模式、策略(Strategy)模式、构造(Builder)模式、责任链(Chain Of Responsibility)模式、代理(Proxy)模式等等。这些设计模式的反复使用,使得Struts2的实现本身就充满了最佳实践。我们将在本书的第4章中重点就Struts2所用到的一些核心设计模式进行深入剖析,帮助读者更加深刻地理解这些设计模式在框架中的运用场景。
3.2.3.3 Struts2与OGNL
有过Web程序开发经验的读者会发现,表达式引擎是Web编程必不可少的重要元素。所谓表达式引擎,指的是通过程序建立起某个实体对象与某种公式表达之间的联系。在Java世界,这种联系具体表现为:使用某些符合特定规则的字符串表达式来对Java中的对象进行读和写的操作。
表达式引擎对Web开发的重要之处在于它在一定程度上解决了Web应用与Java世界之间的沟通问题。既然要使用Java来开发Web应用,就必须使Java的编程要素能够与Web浏览器之间在数据层面保持良好的沟通,而这种沟通就是通过表达式引擎来完成的。
Struts2作为Web层的开发框架,也势必要借助一个强大的表达式引擎来实现上述功能。因而,Struts2选择了在表达式引擎这一领域表现极为非常出色的OGNL作为其所依赖的表达式引擎。换言之,OGNL是Struts2运行所依赖的基本核心技术之一。OGNL的意义不仅在于完成不同形式数据之间的转化和通信,它也是Struts2实现视图层的基本依据。在之后的章节中,我们会发现这将成为Struts2区别于其他Web层框架的实现视图层与Web服务器之间数据交互的最为显著的特点。
OGNL表达式引擎也有较长的历史,它的早期版本的性能曾经遭到质疑。不过,它的性能问题已经在最新的3.0版本中得到了极大的改善。Struts2的最新版本Struts2.2.1所依赖的OGNL为3.0。有关OGNL的相关知识,我们将在第5章详细介绍。
3.2.3.4 Struts2与XWork
了解了Struts2的历史之后,我们知道Struts2来源于Webwork2。Webwork2之所以能够在技术上击败Struts1.X而成为新的框架Struts2的构建基础,主要是源于其优秀的设计思想。或许我们很难用一句话来准确描述这一设计思想的优越性体现在何处,但是当我们试图总结这种优秀思想中所蕴含的核心基石时,XWork将当仁不让地成为其中关键字的第一位。我们可以在XWork的官方网站上找到对XWork的一个大概描述:
XWork is a command-pattern framework that is used to power Struts 2 as well as other applications. XWork provides an Inversion of Control container, a powerful expression language, data type conversion, validation, and pluggable configuration.
XWork是Opensymphony开源组织贡献的另外一个开源项目,从其官方网站的介绍来看,XWork不仅提供了一系列基础构件,其中包括:一个IoC的容器、强大的表达式语言(OGNL)支持、数据类型转化、数据校验框架、可插拔的功能模块(插件模式)及其配置,并且在这一系列的基础构件之上,实现了一套基于Command设计模式的“事件请求执行框架”。
什么是“事件请求执行框架”呢?首先,这是一个基于“请求-响应”的处理器,能够对某一类“请求”做出相应的逻辑处理,并返回“响应”结果。其次,它定义了一套完整的事件逻辑处理的步骤,并且为每个步骤都提供了足够的扩展接口,使得整个事件的执行体系更为丰富、更加具备层次感和可扩展性。因而,“事件请求执行框架”就如同一条定义好的生产流水线,能够为我们提供完整的事件处理模型。回顾一下我们在上一章中提到的一个事件处理流水线的问题,XWork正是这样一个解决方案。
我们知道,所有B/S程序都是典型的基于“请求-响应”模式的Web应用。因而XWork天然地成了处理这种应用最合适的方案。有了XWork作为Struts2所依赖的底层核心,使得Struts2只需要关注与Web容器打交道的部分,而把其余的工作交给XWork即可。当Struts2收到一个Http请求时,Struts2只需要接收请求参数,交给XWork完成执行序列,当XWork执行完毕后,将结果交还Struts2返回相应的视图。可以看到,在整个过程中,XWork是这个“请求-响应”模式的执行核心。
XWork对于Struts2的地位如此重要,以至于Struts2的重大版本总是跟随着XWork的升级而变更。需要注意的是,Struts2的不同版本对XWork的版本依赖也是不同的,它们甚至是不兼容的。下面列出了Struts2发布以来它与XWork版本的兼容性情况:
struts2-core-2.0.x—xwork2.0.x
struts2-core-2.1.x—xwork2.1.x
struts2-core-2.2.x—xwork2.2.x
XWork不仅是Struts2的核心实现,也可以用于一切基于Command模式的Java程序。在实现Command模式时,XWork在其周围定义了丰富的执行层次,在每个执行层次中,都有足够的扩展点,使得我们可以将XWork视作一个工具包,简化我们的开发。
3.3 多视角透析Struts2
Struts2的外部环境并不复杂,因为其核心内容非常明确:探究Struts2运行时所必需的基本要素。我们对Struts2的运行环境和Struts2所依赖的核心技术的讲解,主要是为了让读者了解Struts2能够顺利运行的条件。
在明确Struts2的外部环境之后,我们讨论的话题就将转向Struts2本身。在本节中,我们将从宏观和微观这两个不同的视角,阐述Struts2的总体架构和内部元素构成,以此揭开Struts2的神秘面纱。
3.3.1 透视镜—-Struts2的宏观视图
Struts2的宏观视图是指站在整个框架的角度,了解程序的运行可以划分为哪些逻辑运行主线。对于一个框架的逻辑运行主线的研究,也是我们分析一个框架最为重要的切入点。而这一切入点,就位于Struts2的核心入口程序之中。
3.3.1.1 Struts2的核心入口程序
Struts2的核心入口程序,从功能上讲必须能够处理Http请求,这是表示层框架的基本要求。为了达到这一要求,Struts2毫无例外地遵循了Servlet标准,通过实现标准的Filter接口来进行Http请求的处理。我们通过在web.xml中指定这个实现类,就可以将Struts2框架引入到应用中来,如代码清单3-1所示。
代码清单3-1 web.xml
<filter> <filter-name>struts</filter-name> <filter-class>org.apache.struts2.dispatcher.ng.filter. StrutsPrepareAndExecuteFilter</filter-class> </filter> <filter-mapping> <filter-name>struts</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
打开StrutsPrepareAndExecuteFilter的源码,可以发现它只是一个实现了Filter接口的实现类。其方法列表如图3-3所示。
图3-3 StrutsPrepareAndExecuteFilter方法列表
根据Servlet标准中Filter的生命周期的相关知识,我们知道Filter中所定义的方法具有完全不同的执行时间段和生命周期,它们的执行互不影响,没有交叉。而Filter的生命周期也成为我们对整个Struts2进行运行逻辑主线划分的主要依据。
第一条主线 — Struts2的初始化: init方法驱动执行
第二条主线 — Struts2处理Http请求:doFilter方法驱动执行
在这里,我们首先不对StrutsPrepareAndExecuteFilter的源码做深入的探讨,不过可以先把这个入口程序的基本结构和功能结合我们对Struts2划分的两条逻辑主线,用示意图的形式表达出来,从而帮助读者对入口程序的运行逻辑主线有个初步的认识,如图3-4所示。
图3-4 Struts2入口程序执行示意图
从图中,我们可以清晰地看到Struts2的两条逻辑主线之间由一条分隔符分开。不同的逻辑主线之间完全没有交叉,驱动它们执行的时间节点和触发条件都不同。因而,我们日后对Struts2运行逻辑的分析,也将围绕着这两条主线分别展开。
Struts2入口程序的示意图,可以看作是对Web容器黑盒模型的第一层细化,读者在这里应该更多关心程序运行的基本方向,其内部细节有待我们在之后的章节为大家一一解开。
3.3.1.2 Struts2的初始化主线
Struts2的初始化主线发生在Web应用程序启动之初,由入口程序的init方法驱动执行完成。这条运行主线的主要特点有:
仅在Web应用启动时执行一次
由于这条主线由Filter中的init方法驱动执行,执行完毕后,该主线结束。也就是说,这条主线本身不参与后面任何Http请求的处理过程,无论Struts2之后面再收到多少Http请求,这条主线都不会重复执行。
init方法的执行失败将导致整个Web应用启动失败
如果在init方法执行的过程中发生异常,整个Web应用将无法启动。这个特点从框架规范的角度规定了我们必须确保初始化过程的顺利执行。因为在这个过程中,所有框架内部定义的元素将被初始化,并支撑起整个Struts2进行Http处理的过程。
这两大特点本身其实来源于Filter这个Servlet规范的基本运行特性。然而,这两大特点却也为应用程序在框架的基础之上进行逻辑扩展提供了理论上的指导。在之后有关如何扩展框架的话题讨论中,我们可以看到所有的扩展方案都将基于这两大特点进行设计。
那么,Struts2的初始化主线到底做了什么呢?对应于Struts2初始化的运行特点,Struts2的初始化主线也有两大主要内容:
框架元素的初始化工作
这一初始化工作包含了对框架内部的许多内置对象的创建和缓存。我们发现,对于框架初始化工作的基本要求,就是在整个框架的运行过程中仅执行一次,这正好符合这条主线的基本特点。
控制框架运行的必要条件
框架的可扩展特性保证了我们可以在应用层面对框架的运行参数和执行模式进行定制化,而框架则有必要对这种定制化进行正确性校验。当这种校验失败时,Web应用的启动会失败。这也就是Struts2在框架级别所能够提供的运行期的检查。
初始化主线贯穿了Struts2对其内置对象的创建和缓存的过程,这一过程相当于把整个Struts2作为一个框架的运行环境完整地创建出来。这条主线的顺利运行,为之后的Http请求处理主线提供了必要的框架运行环境。
我们在这里所说的运行环境和Struts2自身的运行环境不同,它是指建立在Web服务器之上,框架自身运行所必需的内置对象的集合。为了更好地对这些内置对象进行管理,Struts2引入了框架级别“容器”的概念。因而Struts2的初始化主线,实际上最终转化为对这个“容器”的初始化过程。有关这个“容器”的定义和初始化过程的细节,我们将在第5章为读者解开谜团。
3.3.1.3 Struts2的Http请求处理主线
Struts2的Http请求处理主线是Struts2的核心主线,包含了Struts2处理Http请求、进行必要的数据处理和处理数据返回的全部过程。这条主线将在任何满足web.xml中所指定的URL Pattern的Http请求发生时进行响应,由doFilter方法负责驱动执行。
回顾一下Struts2核心入口程序的流程图(图3-4),我们可以看到Struts2的Http请求处理主线又被一条分割线划分成了两个不同的执行阶段:
第一阶段—Http请求预处理
在这个阶段中,程序执行的控制权在Struts2手上。这个阶段的主要工作是针对每个Http请求进行预处理,为真正的业务逻辑执行做必要的数据环境和运行环境的准备。
程序代码在这个阶段有一个非常显著的特点:依赖于Web容器,并时时刻刻将与Web容器打交道作为主要工作。
第二阶段—XWork执行业务逻辑
在这个阶段,程序执行的控制权被移交给了XWork。Struts2在完成Http请求的预处理之后,将Http请求中的数据封装成普通的Java对象,并由XWork负责执行具体的业务逻辑。
程序代码在这个阶段的特点和第一阶段完全相反:不依赖于Web容器,完全由XWork框架驱动整个执行的过程。
从Struts2对于Http请求的处理过程中,我们可以看出Struts2的核心设计理念在于解耦。所谓解耦,实际上是尽可能地消除核心程序对外部运行环境的依赖,从而保证核心程序能够更加专注于应用程序的逻辑本身。在Struts2中,我们所说的外部运行环境就是Web容器。我们在这里可以看到,Struts2的核心设计理念与Struts2的运行环境居然是一个矛盾体!
结论 Struts2的核心设计理念在于消除核心程序对运行环境(Web容器)的依赖,而这一过程也是Struts2的解耦过程。
这种设计实现与目的之间的矛盾,却是Struts2的设计始终围绕着Web开发中的最佳实践的最有力证明,也是Struts2从设计理念开始,就优于其他表示层框架的精要所在。在解耦方面,Struts2也确实做到了2个不同的层面,从而使整个设计更加突显出其优秀之处:
从代码上进行物理解耦
Struts2将第一阶段中的代码整合到struts2-core-2.2.1.jar,而将第二阶段中的代码整合到xwork-core-2.2.1.jar。
将逻辑分配到不同的执行阶段
Struts2将处理数据的逻辑和处理业务的逻辑分配到2个不同的执行阶段,使得我们对于代码逻辑的关注点更为清晰。
因此,正如我们在之前的章节所谈到的,严格意义上的Struts2,实际上由2个不同的框架所组成。一个是真正意义上的Struts2,另外一个是XWork。从职责上来说,XWork才是真正实现MVC的框架,Struts2的工作是在对Http请求进行一定处理后,委托XWork完成真正的逻辑处理。将Web容器与MVC实现分离,是Struts2区别于其他Web框架的最重要的特性,也是最值得我们品味的一个宏观设计思路。当读者真正理解了其中的奥秘,相信也就真正掌握Web开发之道了。
3.3.2 显微镜 — Struts2的微观元素
在了解了Struts2的宏观面之后,我们再来一起探究一下构成这些宏观面的微观元素。同样,不同的主线和不同的阶段所承担的职责不同,构成它们的微观元素也不尽相同。在本节中,我们将列出每条主线和每个执行阶段的主要组成元素。或许在这其中大家会接触到许多新名词,读者不妨首先感性地认识一下这些概念性的名词,可以不求甚解,因为我们会在后续的章节中对Struts2的这些元素一一展开分析和讲解。
3.3.2.1 第一条主线 — Struts2的初始化
在对Struts2初始化主线的宏观分析中,我们曾经谈到为了帮助更好地管理Struts2中的内置对象,Struts2引入了一个“容器”的概念,将所有需要被管理的对象全部置于容器之中。因而,整个Struts2初始化过程也始终围绕着这个“容器”展开。除了“容器”,Struts2中的另一类配置元素PackageConfig,也是Struts2初始化的主要内容之一。如果我们从“数据 + 行为”的角度来分析,那么构成Struts2整个初始化过程的主要元素,就可以分为数据结构的定义和初始化行为的操作接口两个部分。
从数据结构定义的角度,“容器”顺理成章地成为Struts2初始化主线中的核心构成元素,而PackageConfig作为事件请求映射的配置元素也成为我们所需要重点关注的构成元素。它们的接口定义和实现类如表3-1所示。
表3-1 Struts2中的容器及其实现类
表3-1的定义从数据结构的角度指出了Struts2初始化流程的主要对象是哪些。而整个初始化的操作过程,则由另外两个相辅相成的元素配合共同完成,它们分别是加载接口(Provider)和构造器(Builder),其相关元素如表3-2所示。
表3-2 Struts2中容器的加载接口(Provider)和容器的构造器(Builder)
Struts2初始化主线中还有一些辅助元素,它们主要用于承载这些配置加载接口并在初始化时驱动整个初始化流程的顺利执行。相关元素如表3-3所示。
表3-3 Struts2初始化主线中的辅助元素
Struts2的初始化是一个非常复杂的流程。在这里我们仅仅给出了部分主要的数据结构和基础操作接口。读者可以使用IDE的源码查看功能找到这些接口的众多实现类。从这些实现类中,大家会发现这些实现类实际上是对整个Struts2配置元素的一个总串联,之后的章节我们会通过源码分析它们的联系和运行机理。
3.3.2.2 第二条主线—第一阶段:Http请求预处理
在这个阶段,我们知道程序的执行控制权还在Struts2手中。所以在这个阶段中所涉及的主要微观元素,都是Struts2的类。这些类的主要职责是与Web容器打交道。因而,从设计原则上,为了保持解耦,这个阶段做了大量的对象创建和对象转化的工作。当这些工作完成之后,就能交付第二个阶段继续执行。这个阶段的主要微观元素如表3-4所示。
表3-4 Struts2进行Http请求预处理阶段的主要微观元素
虽然这个阶段所涉及的元素很少,但是其中的Dispatcher却是整个Struts2框架的核心。Dispatcher被称为核心分发器,是Struts2进行Http请求处理的实际场所。在整个处理流程中,Dispatcher不仅是Http请求预处理的实际执行者,更是将Http请求与Web容器进行解耦并进行逻辑处理转发的执行驱动核心。事实上,我们对Struts2框架内部机理的研究,都将以Dispatcher作为重要的切入点而展开。
3.3.2.3 第二条主线 — 第二阶段:XWork执行业务逻辑
在这个阶段,程序的执行控制权被移交到了XWork框架中。我们可以想象位于这个执行阶段的微观元素区别于第一个阶段的显著特点:它们都是XWork框架所定义的类。其相关微观元素的定义如表3-5所示。
表3-5 XWork执行业务逻辑阶段的主要微观元素
在这里,我们形象地把XWork比喻成一条生产流水线。在这其中的每个元素,就像是生产线中的重要组成部分。XWork框架就像一个完整的事件执行器,进入框架中的事件就如同进入生产线中的原材料,会按照生产线中的定义依次执行并产生结果。
表3-5中的七个元素,被称为XWork的七大元素,贯穿了XWork事件执行器的整个生命周期。它们各司其职、精心配合,提供了事件执行框架足够的扩展接口。不仅如此,它们之间的调用关系也成为XWork框架设计中的经典。这些元素的调用关系,如图3-5所示。
图3-5 XWork元素的调用关系图
这幅图不仅包含了XWork框架中各个元素的调用关系,还把整个XWork框架置于Web容器这样一个大的环境之中,同时给出了XWork框架的调用核心Dispatcher。我们在这里不再对这幅图做深入的剖析,读者可以大致了解XWork中这些元素的结构和层次关系。在第8章中,我们将从数据流和控制流这两个不同的角度,对这张图的程序流转方向进行详细的分析。
3.4 Struts2的配置元素
上一节中,我们分别从宏观层面和微观层面对Struts2进行了初步的透视分析。从框架中提炼出了Struts2运行的2条主线和2个执行阶段,并以此为基础列出了每条主线和每个执行阶段的核心元素。2条主线和2个执行阶段的概念之所以被反复提及,是因为Struts2的所有内容全都无法脱离这些概念而独立存在,这些概念不仅支撑起了Struts2的核心构架,同时也是我们研究Struts2中构成元素和运行机理的主要依据。
我们在这里所说的“构成元素”和“运行机理”,实际上从另外一个角度表述了程序的构成方式。任何程序,如果我们从组织结构上进行分析,总是由两大类元素组成:一类用于描述问题,这类元素我们通常称之为数据结构(构成元素);另一类元素则是在数据结构的基础之上执行的逻辑代码,这类元素我们通常称之为算法(运行机理)。数据结构和算法的有机结合,构成了可运行的程序主体。这其实也是我们经常听到的一条结论:
结论 程序 = 数据结构 + 算法 (构成元素 + 运行机理)
对“构成元素”的研究,有助于我们站在全局的观点来审视支撑整个Struts2运行的底层核心。而这些构成元素,需要一个贯穿始终的粘合剂,不仅能够以一定的形式表现出这些构成元素互相之间的逻辑关系,同时能够将它们的执行逻辑串联起来。这种粘合剂,实际上就是框架的核心配置。在本节中,我们就来重点讨论一下Struts2的核心配置的表现形式、元素构成和运行机理。
3.4.1 Struts2配置详解
配置就像是程序的影子,与程序总是如影随形。无论是什么框架,配置总是作为一个重要的组成部分,在框架的运行过程中发挥作用。同时,配置所起的不同作用在一定程度上也决定了配置的存在形式。
3.4.1.1 配置概览
Struts2提供了多种可选的配置文件形式。根据这些配置文件的名称、所在位置、作用范围和用途我们制作了一张图表,如表3-6所示。
表3-6 Struts2配置文件的表现形式
表3-6就是Struts2中配置文件的一个概览。可以看到在表头部分,我们分别使用了配置文件、所在位置、作用范围、用途来对配置文件进行大致的描述。接下来,我们分别从不同的角度来深入挖掘一下隐藏在配置文件背后的秘密。
3.4.1.2 配置的表现形式
从表3-6的第一列中,我们可以看到Struts2所支持的所有配置形式:其中既有XML文件形式,也有Properties文件形式。这些不同形式的配置文件,它们格式不同、所在位置不同、作用范围也不同。那么这些配置文件有没有一个主心骨呢?答案是肯定的。
结论 从形式上讲,Struts2的配置元素的表现形式以XML为核心,而Properties文件则作为另外一种配置形式起到辅助作用。
事实上,除了XML和Properties文件的形式,Struts2对于配置的构成形式并没有一个明确而死板的规定,因而我们可以在很多Struts2的扩展中见到类似Annotation或者“约定大于配置”的配置形式。有关这一点,我们在后续的章节会详细展开。
3.4.1.3 配置的作用范围
在表3-6中,我们可以看到不同的配置文件有着不同的作用范围。
在这其中,struts-default.xml和default.properties是框架级别的配置文件。这两个文件蕴含在Struts2的核心JAR包之中,它们将在应用程序启动时被Struts2的初始化程序读取并加载。
在应用级别,Struts2提供了2个与框架级别的配置文件相对应的配置文件:struts. xml和struts.properties。它们的结构与框架级别的配置文件完全相同,但是其中定义的所有内容将覆盖框架级别的配置定义,从而为程序员提供进行应用级别配置扩展的基本方法。
除此之外,我们还可以通过Struts2的插件来进行应用级别的配置定义,这一配置定义在插件所在JAR包的根目录,并以struts-plugin.xml的文件名形式出现。这种插件形式的配置定义则从另外一个角度为程序员提供了足够多的配置扩展层次。
配置的作用范围从一定程度上应该与配置文件的加载顺序结合起来看。有兴趣的读者可以在本书的第12章找到相关内容。
3.4.1.4 配置文件的必要性
在配置文件的这些特性中,有一点值得我们注意:所有我们提到的应用级别的配置文件,都不是必须存在的。
因为在默认情况下,Struts2框架级别的配置文件已经足以支撑起一个Struts2的应用了。因而,在表3-6所列出的应用级别的配置文件中,只有web.xml中的配置是必需的。正如前面介绍的,因为我们需要在web.xml中定义Struts2的入口程序。这也是驱动整个Struts2的两条主线和两个阶段运行的核心所在。缺了web.xml的配置,Struts2本身也就无从谈起了。
3.4.1.5 配置元素的逻辑意义
虽然XML配置文件和Properties配置文件都是Struts2所支持的配置文件形式,但是从内容上说,XML包含了所有Struts2内置对象的定义、运行参数的定义、结构化配置定义、事件响应映射关系定义等所有Struts2运行必不可少的运行元素;而Properties文件呢,主要用于指定Struts2的运行参数。因此,我们可以得出一个结论:
结论 Struts2框架中的XML文件的配置元素定义是Properties文件的配置元素定义的超集。
也就是说,凡是能够在Properties文件中定义的配置元素,我们都可以在XML中找到相应的配置方式代替,反之则不成立。因而,要研究Struts2的微观构成元素,我们可以从分析Struts2的XML配置文件的元素入手。
3.4.2 Struts2配置元素定义
根据3.4.1节中我们对Struts2配置元素的分析所得到的结论,对Struts2所有配置元素的研究,可以从位于Struts2核心JAR包中的struts-default.xml文件入手,struts-default. xml部分源码如代码清单3-2所示。
代码清单3-2 struts-default.xml
<!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.1.7//EN" "http://struts.apache.org/dtds/struts-2.1.7.dtd"> <struts> <bean class="com.opensymphony.xwork2.ObjectFactory" name="xwork" /> <bean type="com.opensymphony.xwork2.ObjectFactory" name="struts" class="org.apache.struts2.impl.StrutsObjectFactory" /> // 这里省略了许多bean定义 <constant name="struts.multipart.handler" value="jakarta" /> <package name="struts-default" abstract="true"> <result-types> <result-type name="chain" class="com.opensymphony.xwork2.ActionChainResult"/> <result-type name="dispatcher" class="org.apache.struts2.dispatcher.ServletDispatcherResult" default="true"/> // 这里省略了许多result-type定义 </result-types> <interceptors> <interceptor name="alias" class="com.opensymphony.xwork2.interceptor.AliasInterceptor"/> <interceptor name="autowiring" class="com.opensymphony.xwork2.spring.interceptor. ActionAutowiringInterceptor"/> <interceptor name="chain" class="com.opensymphony.xwork2.interceptor.ChainingInterceptor"/> // 这里省略了许多interceptor定义 <!-- Basic stack --> <interceptor-stack name="basicStack"> <interceptor-ref name="exception"/> <interceptor-ref name="servletConfig"/> <interceptor-ref name="prepare"/> <interceptor-ref name="checkbox"/> <interceptor-ref name="multiselect"/> <interceptor-ref name="actionMappingParams"/> <interceptor-ref name="params"> <param name="excludeParams">dojo\..*,^struts\..*</param> </interceptor-ref> <interceptor-ref name="conversionError"/> </interceptor-stack> // 这里省略了许多interceptor-stack定义 </interceptors> <default-interceptor-ref name="defaultStack"/> <default-class-ref class="com.opensymphony.xwork2.ActionSupport" /> </package> </struts>
我们在这里为了显示XML配置文件的结构,省略了其中某些具体节点的定义。不过这丝毫不影响我们对XML配置中的一些主要节点进行研究。
3.4.2.1 include节点
include节点本身并没有在struts-default.xml中出现,但它却是Struts2配置文件所支持的第一层根节点之一。include节点的主要作用是帮助我们管理Struts2的配置文件,实现配置文件的模块化。
include节点最为常见的使用方法,是我们可以在应用级别的配置文件(struts.xml)里,将整个应用的配置根据一定的逻辑划分成若干个独立的配置文件,以方便进行模块化管理和团队开发。如代码清单3-3所示是一个典型的例子。
代码清单3-3 struts.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN" "http://struts.apache.org/dtds/struts-2.0.dtd"> <struts> <include file="web/struts-config.xml" /> <include file="web/struts-system.xml" /> <include file="web/struts-user.xml" /> </struts>
从上面的例子中,我们可以看到Struts2进行配置模块化的管理方式:利用结构完全相同的文件,通过include节点把这些文件串联起来。这是一个非常直观有效的管理方式,这种模式有点类似于Java语法中的“对象引用嵌套”。其最大的好处就在于方便理解,并且能够支持团队化开发,团队成员只需要关心他自身所工作的模块,从而避免了配置层面的冲突发生。
另外一种进行配置模块化管理的方式是使用“继承”机制。这种机制我们会在之后对package节点的分析中有所涉及,读者可以细细体会它们两者的不同之处。
3.4.2.2 bean节点
bean节点在struts-default.xml中广泛用于定义Struts2框架级别的内置对象。我们可以从bean节点的属性定义中发现这个节点实际上是一个用于描述接口及其实现类映射关系的节点。
在逻辑关系上,bean节点的寻址是通过name属性和type属性共同构成一个逻辑主键来共同决定一个class属性。也就是说,我们可以通过name属性和type属性的值来控制一个接口的不同实现方式,这也是Struts2用来切换程序运行机制的最基本方式。
这些bean节点在Struts2内部是以何种形式存在的呢?Struts2在框架级别实现了一个对象容器,并将配置文件中所有bean节点所定义的对象纳入容器之中进行管理。Struts2通过这个容器在框架级别负责这些对象的创建、销毁以及依赖关系的处理。熟悉Spring框架的读者会发现,在这里,这个容器的概念与Spring中容器的概念非常类似,都是用来管理程序运行过程中对象的生命周期的。Struts2不仅实现了一个容器,并且还在容器的基础之上实现了依赖注入(IoC),从而使得所有Struts2中所定义的对象都可以被有条不紊地创建、执行和销毁。
在应用级别的配置文件中,通过增加bean节点,就可以把一个对象纳入Struts2的容器中进行管理。此时,我们可以通过Struts2提供的容器访问接口(原生API方式)或者依赖注入的方式,获取我们自定义的这个bean对象并使用。
3.4.2.3 constant节点
constant节点从结构上看是一个非常典型的键值对类型的配置,主要用于定义Struts2运行时的参数。在struts-default.xml中,我们很少看到constant节点的定义,因为Struts2主要使用了Properties文件来定义运行时的参数而并非将它们放在XML中。这实际上符合了“让最合适的表现形式来表达最合适的配置语义”的设计原则。
constant节点的作用与Struts2的Properties文件的配置形式是完全重合的。这一点有助于我们理解Struts2所支持的XML配置形式实际上是Properties文件配置形式的超集这一结论。因而,constant节点也成为两种配置形式在逻辑上的交集。
constant节点中所有运行时的参数定义与bean节点一样,也会在系统初始化时被加载到Struts2的容器中进行统一管理。也就是说,Struts2的容器不仅仅负责对Struts2所有内置对象的管理,还要负责对系统的运行参数进行管理。Struts2将两者进行统一的主要好处在于Struts2在内部对与这些框架相关的运行对象或者运行参数可以一视同仁地进行处理,通过统一的容器访问接口(或者依赖注入的方式),Struts2可以从容器中方便地组织起整个程序的架构。这种统一性可大大降低编程的复杂度,使Struts2的运行机制更为顺畅。
3.4.2.4 package节点
package节点是一个复杂的复合节点,在其中包含了众多子节点:ResultType、Interceptor、InterceptorStack、Action,等等。实际上,这些子节点全部继承自XWork框架。还记得第2章中我们对XWork框架的比喻吗?XWork就像一条生产流水线,定义了一系列事件执行的步骤和次序。一个package节点实际上可以被看作是一条简单的XWork生产流水线,其中包含XWork框架如何应对某些请求并选择相应的执行序列进行处理的具体方式。其中的ResultType、Interceptor子节点和Action子节点也正是我们之前提到过的XWork中最为重要的微观元素,也是构成事件执行序列的主要元素。
从功能上分析,package节点与之前的bean节点和constant节点有着本质的区别。无论是bean节点还是constant节点,它们都与框架自身的运行状态有关,并且被Struts2内部的容器所管理。而package节点的作用却是定义一种映射关系,更多反映了框架如何与外部程序进行交互的过程。我们将在后续章节展开分析这些节点的实际作用,读者在这里只需要大致了解package节点的构成要素即可。
在package节点的属性之中,name是一个唯一的标识符,namespace则从命名空间的角度为整个事件请求机制划分不同的种类。在运行期,我们可以认为name属性和namespace属性都用于对请求进行逻辑划分。
package节点中的extends属性允许package和package之间形成相应的继承关系。通过继承,子package自动获得父package的所有配置定义。extends属性则从另外一个角度,允许开发人员进行配置的模块化管理,抽取公共的配置定义成为一个公共的父package,子package可以任意引用父package中的任何配置定义,这极大地简化了配置的工作量。读者在这里可以体会“继承”和“引用”这两种不同的扩展模式在Struts2配置文件中的应用。
3.4.3 Struts2配置元素的分类
在对struts-default.xml的分析中,我们实际上已经提供了在逻辑上对Struts2中的配置元素进行分类的思路:从节点所表达的逻辑含义和节点在程序中所起的作用对配置元素进行分类。
XML配置文件中的bean节点和constant节点,它们其中一个是构成程序运行的对象,而另一个用于指定程序运行的执行参数。它们都与Struts2自身的运行机制有关,并且有一个统一的管理机制,将它们归为同一类的配置元素应该毫无异议。习惯上,我们把这类配置元素称之为“容器配置元素”。有关这个命名的具体由来,我们将在之后的章节详细解释。
XML配置文件中的package节点定义了一种事件请求响应的映射关系,反映的是Struts2对于外部事件请求时如何进行响应的处理序列,它与bean节点和constant节点在整个框架中起的作用完全不同,是自成体系的另一类配置元素。对于这一类配置元素,我们通常称之为“事件映射关系”。
在这里我们需要强调的是,Struts2的配置元素的定义体系与XWork框架是一脉相承的。无论是节点定义还是节点的逻辑关系,Struts2都直接从XWork框架继承而来。
明确Struts2对于配置元素的分类,可为整个框架进行配置元素的对象化打下坚实的基础。在之后的章节中,我们可以看到Struts2针对不同的配置元素,定义了与之对应的数据结构和处理方法。
3.5 小结
在本章中,我们从Struts2的历史谈起,对Struts2的外部环境和内部构成机理都做了简要的分析,目的是使读者对Struts2框架有个初步的认识。其中,对Struts2的外部环境的介绍有助于使读者从技术的角度了解Struts2的适用范围及与其依赖的项目之间的联系。而对Struts2内部运行主线和构成元素的剖析则可帮助读者对Struts2自身的架构有所掌握。
本章的内容对于全书有着提纲挈领的作用,尤其是对Struts2运行的逻辑主线的分析,将成为我们研究Struts2内在机理的切入点。因而,本章中的许多概念和结论会在之后的分析讲解中被反复提及。读者应谨记这些结论,并与之后的章节对应起来,体会Struts2的总体架构。
回顾本章,大家可以重新思考下面的这些问题,是否在本章中得到了答案呢?
Struts2和Struts1.X有什么区别?它们各自的发展轨迹又如何?
Struts2依赖于哪些核心技术?
Struts2可以应用在什么样的项目中?
Struts2可以分成哪两条逻辑运行主线?
Struts2在处理Http请求时,可以分成哪两个主要阶段?
Struts2通过哪些元素的相互配合来完成初始化运行主线?
XWork框架主要由哪些元素构成?它们之间有什么关系?
Struts2有哪些配置表现形式?
Struts2中的配置元素可以分为哪两个大类?
什么是配置元素的对象化过程?
Struts2的配置元素的对象化过程由哪两大元素配合完成?