本章要点

— Web应用的发展

— Model 1和Model 2

— MVC思想

— MVC模式的优势

— 常用MVC框架及其特征

— Struts 1的基本结构及其存在的问题

— WebWork的基本结构

— Struts 2的起源

— Struts 2的框架架构

— Struts 2的标签库

— Struts 2的控制器组件

— Struts 1和Struts 2的对比

Struts 1是全世界第一个发布的MVC框架，它由Craig McClanahan在2001年发布，该框架一经推出，就得到了世界上Java Web开发者的拥护，经过长达6年时间的锤炼，Struts 1框架更加成熟、稳定，性能也有了很好的保证。因此，到目前为止，Struts 1依然是世界上使用最广泛的MVC框架。

目前，基于Web的MVC框架非常多，发展也很快，每隔一段时间就有一个新的MVC框架发布，例如像JSF、Tapestry和Spring MVC等。除了这些有名的MVC框架外，还有一些边缘团队的MVC框架也很有借鉴意义。

对于企业实际使用MVC框架而言，框架的稳定性则应该是最值得考虑的问题。一个刚刚起步的框架，可能本身就存在一些隐藏的问题，会将自身的BUG引入自己的应用。这也是笔者不推荐开发者自己实现框架的原因。

虽然Struts 2号称是一个全新的框架，但这仅仅是相对Struts 1而言。Struts 2与Struts 1相比，确实有很多革命性的改进，但它并不是新发布的新框架，而是在另一个赫赫有名的框架：WebWork基础上发展起来的。从某种程度上来讲，Strut2没有继承Struts 1的血统，而是继承了WebWork的血统。或者说，WebWork衍生出了Struts 2，而不是Struts 1衍生了Struts 2。因为Struts 2是WebWork的升级，而不是一个全新的框架，因此稳定性、性能等各方面都有很好的保证；而且吸收了Struts 1和WebWork两者的优势，因此，是一个非常值得期待的框架。

1.1 MVC思想概述

今天，我们见到的绝大部分应用，都是基于B/S（浏览器/服务器）架构的，其中的服务器就是Web服务器。可见，Web应用是目前广泛使用的应用模式，而Struts 2是一个具有很好的实用价值的Web MVC框架。介绍Struts MVC框架之前，我们首先介绍Web应用的发展历史和MVC思想。

1.1.1 Web技术的发展

随着Internet技术的广泛使用，Web技术已经广泛应用于Internet上，但早期的Web应用全部是静态的HTML页面，用于将一些文本信息呈现给浏览者，但这些信息是固定写在HTML页面里的，该页面不具备与用户交互的能力，没有动态显示的功能。

很自然地，人们希望Web应用里应该包含一些能动态执行的页面，最早的CGI（通用网关接口）技术满足了该要求，CGI技术使得Web应用可以与客户端浏览器交互，不再需要使用静态的HTML页面。CGI技术可以从数据库读取信息，将这些信息呈现给用户；还可以获取用户的请求参数，并将这些参数保存到数据库里。

CGI技术开启了动态Web应用的时代，给了这种技术无限的可能性。但CGI技术存在很多缺点，其中最大的缺点就是开发动态Web应用难度非常大，而且在性能等各方面也存在限制。到1997年时，随着Java语言的广泛使用，Servlet技术迅速成为动态Web应用的主要开发技术。相比传统的CGI应用而言，Servlet具有大量的优势：

— Servlet是基于Java语言创建的，而Java语言则内建了多线程支持，这一点大大提高了动态Web应用的性能。

— Servlet应用可以充分利用Java语言的优势，例如JDBC（Java DataBase Connection）等。同时，Java语言提供了丰富的类库，这些都简化了Servlet的开发。

— 除此之外，Servlet运行在Web服务器中，由Web服务器去负责管理Servlet的实例化，并对客户端提供多线程、网络通信等功能，这都保证Servlet有更好的稳定性和性能。

Servlet在Web应用中被映射成一个URL（统一资源定位），该URL可以被客户端浏览器请求，当用户向指定URL对应的Servlet发送请求时，该请求被Web服务器接收到，该Web服务器负责处理多线程、网络通信等功能，而Servlet的内容则决定了服务器对客户端的响应内容。

图1.1显示了Servlet的响应流程。

正如图1.1所显示的，浏览器向Web服务器内指定的Servlet发送请求，Web服务器根据Servlet生成对客户端的响应。

实际上，这是后来所有的动态Web编程技术所使用的模型，这种模型都需要一个动态的程序，或者一个动态页面，当客户端向该动态程序或动态页面发送请求时，Web服务器根据该动态程序来生成对客户端的响应。

图1.1 Servlet的响应流程

到了1998年，微软发布了ASP 2.0。它是Windows NT 4 Option Pack的一部分，作为IIS 4.0的外接式附件。它与ASP 1.0的主要区别在于它的外部组件是可以初始化的，这样，在ASP程序内部的所有组件都有了独立的内存空间，并可以进行事务处理。标志着ASP技术开始真正作为动态Web编程技术。

当ASP技术在世界上广泛流行时，人们很快感受到这种简单的技术的魅力：ASP使用VBScript作为脚本语言，它的语法简单、开发效率非常高。而且，世界上已经有了非常多的VB程序员，这些VB程序员可以很轻易地过渡成ASP程序员——因此，ASP技术马上成为应用最广泛的动态Web开发技术。

随后，由Sun带领的Java阵营，立即发布了JSP标准，从某种程度上来看，JSP是Java阵营为了对抗ASP推出的一种动态Web编程技术。

ASP和JSP从名称上如此相似，但它们的运行机制存在一些差别，这主要是因为VBScript是一种脚本语言，无需编译，而JSP使用Java作为脚本语句——但Java从来就不是解释型的脚本语言，因此JSP页面并不能立即执行。因此，JSP必须编译成Servlet，这就是说：JSP的实质还是Servlet。不过，书写JSP比书写Servlet简单得多。

JSP的运行机理如图1.2所示。

图1.2 JSP的运行机理

对比图1.1和图1.2，发现不论是Servlet动态Web技术，还是JSP动态Web技术，它们的实质完全一样。可以这样理解：JSP是一种更简单的Servlet技术，这也是JSP技术出现的意义——作为一个和ASP对抗的技术，简单就是JSP的最大优势。

随着实际Web应用的使用越来越广泛，Web应用的规模也越来越大，开发人员发现动态Web应用的维护成本越来越大，即使只需要修改该页面的一个简单按钮文本，或者一段静态的文本内容，也不得不打开混杂的动态脚本的页面源文件进行修改——这是一种很大的风险，完全有可能引入新的错误。

这个时候，人们意识到：使用单纯的ASP，或者JSP页面充当过多角色是相当失败的选择，这对于后期的维护相当不利。慢慢地开发人员开始在Web开发中使用MVC模式。

随后就是Java阵营发布了一套完整的企业开发规范：J2EE（现在已经更名为Java EE），紧跟着微软也发布了ASP.NET技术，它们都采用一种优秀的分层思想，力图解决Web应用维护困难的问题。

动态Web编程技术大致有如图1.3所示的路线。

1.1.2 Model 1和Model 2

对于Java阵营的动态Web编程技术而言，则经历了所谓的Model 1和Model 2时代。

图1.3 动态Web编程技术的发展历史

所谓Model 1就是JSP大行其道的时代，在Model 1模式下，整个Web应用几乎全部由JSP页面组成，JSP页面接收处理客户端请求，对请求处理后直接做出响应。用少量的JavaBean来处理数据库连接、数据库访问等操作。

图1.4显示了Model 1的程序流程。

Model 1模式的实现比较简单，适用于快速开发小规模项目。但从工程化的角度看，它的局限性非常明显：JSP页面身兼View和Controller两种角色，将控制逻辑和表现逻辑混杂在一起，从而导致代码的重用性非常低，增加了应用的扩展性和维护的难度。

早期有大量ASP和JSP技术开发出来的Web应用，这些Web应用都采用了Model 1架构。

Model 2已经是基于MVC架构的设计模式。在Model 2架构中，Servlet作为前端控制器，负责接收客户端发送的请求，在Servlet中只包含控制逻辑和简单的前端处理；然后，调用后端JavaBean来完成实际的逻辑处理；最后，转发到相应的JSP页面处理显示逻辑。其具体的实现方式如图1.5所示。

图1.4 Model 1的程序流程

图1.5显示了Model 2的程序流程。

图1.5 Model 2的程序流程

正如图1.5中看到的，Model 2下JSP不再承担控制器的责任，它仅仅是表现层角色，仅仅用于将结果呈现给用户，JSP页面的请求与Servlet（控制器）交互，而Servlet负责与后台的JavaBean通信。在Model 2模式下，模型（Model）由JavaBean 充当，视图（View）由JSP页面充当，而控制器（Controller）则由Servlet充当。

由于引入了MVC模式，使Model 2具有组件化的特点，更适用于大规模应用的开发，但也增加了应用开发的复杂程度。原本需要一个简单的JSP页面就能实现的应用，在Model 2中被分解成多个协同工作的部分，需花更多时间才能真正掌握其设计和实现过程。

Model 2已经是MVC设计思想下的架构，下面简要介绍MVC设计思想的优势。

注意 对于非常小型的Web站点，如果后期的更新、维护工作不是特别大，可以使用Model 1的模式来开发应用，而不是使用Model 2的模式。虽然Model 2提供了更好的可扩展性及可维护性，但增加了前期开发成本。从某种程度上讲，Model 2为了降低系统后期维护的复杂度，却导致前期开发的更高复杂度。

1.1.3 MVC思想及其优势

MVC并不是Java语言所特有的设计思想，也并不是Web应用所特有的思想，它是所有面向对象程序设计语言都应该遵守的规范。

MVC思想将一个应用分成三个基本部分：Model（模型）、View（视图）和Controller（控制器），这三个部分以最少的耦合协同工作，从而提高应用的可扩展性及可维护性。

起初，MVC模式是针对相同的数据需要不同显示的应用而设计的，其整体的效果如图1.6所示。

图1.6 MVC结构

在经典的MVC模式中，事件由控制器处理，控制器根据事件的类型改变模型或视图，反之亦然。具体地说，每个模型对应一系列的视图列表，这种对应关系通常采用注册来完成，即：把多个视图注册到同一个模型，当模型发生改变时，模型向所有注册过的视图发送通知，接下来，视图从对应的模型中获得信息，然后完成视图显示的更新。

从设计模式的角度来看，MVC思想非常类似于一个观察者模式，但与观察者模式存在少许差别：观察者模式下观察者和被观察者可以是两个互相对等的对象，但对于MVC思想而言，被观察者往往只是单纯的数据体，而观察者则是单纯的视图页面。

概括起来，MVC有如下特点。

— 多个视图可以对应一个模型。按MVC设计模式，一个模型对应多个视图，可以减少代码的复制及代码的维护量，一旦模型发生改变，也易于维护。

— 模型返回的数据与显示逻辑分离。模型数据可以应用任何的显示技术，例如，使用JSP页面、Velocity模板或者直接产生Excel文档等。

— 应用被分隔为三层，降低了各层之间的耦合，提供了应用的可扩展性。

— 控制层的概念也很有效，由于它把不同的模型和不同的视图组合在一起，完成不同的请求。因此，控制层可以说是包含了用户请求权限的概念。

— MVC更符合软件工程化管理的精神。不同的层各司其职，每一层的组件具有相同的特征，有利于通过工程化和工具化产生管理程序代码。

相对于早期的MVC思想，Web模式下的MVC思想则又存在一些变化，因为对于一个应用程序而言，我们可以将视图注册给模型，当模型数据发生改变时，即时通知视图页面发生改变；而对于Web应用而言，即使将多个JSP页面注册给一个模型，当模型发生变化时，模型无法主动发送消息给JSP页面（因为Web应用都是基于请求/响应模式的），只有当用户请求浏览该页面时，控制器才负责调用模型数据来更新JSP页面。

注意 MVC思想与观察者模式有一定的相似之处，但并不完全相同。经典的MVC思想与Web应用的MVC思想也存在一定的差别，引起差别的主要原因是因为Web应用是一种请求/响应模式下应用，对于请求/响应应用，如果用户不对应用发出请求，视图无法主动更新自己。

1.1.4 常用的MVC框架

目前常用的MVC框架，除了Struts 2的两个前身外，还有一些非常流行的MVC框架，这些框架都提供了较好的层次分隔能力。在实现良好的MVC 分隔的基础上，还提供一些辅助类库，帮助应用的开发。

目前常用的MVC框架还有如下一些。

1．JSF

准确地说，JSF是一个标准，而不是一个产品。目前，JSF已经有两个实现产品可供选择，包含Sun的参考实现和Apache的MyFaces。大部分的时候，我们所说的JSF都是指Sun的参考实现。目前，JSF是作为JEE 5.0的一个组成部分，与JEE 5.0一起发布。

JSF的行为方法在POJO中实现，JSF的Managed Bean无需继承任何特别的类。因此，无需在表单和模型对象之间实现多余的控制器层。JSF中没有控制器对象，控制器行为通过模型对象实现。

当然，JSF也允许生成独立的控制器对象。在Struts 1中，Form Bean包含数据，Action Bean包含业务逻辑，二者无法融合在一起。在JSF中，既可以将二者分开，也可以合并在一个对象中，提供更多灵活的选择。

JSF的事件框架可以细化到表单中每个字段。JSF依然是基于JSP/Servlet的，仍然是JSP/Servlet架构，因而学习曲线相对简单。在实际使用过程中，JSF也会存在一些不足：

— 作为新兴的MVC框架，用户相对较少，相关资源也不是非常丰富。

— JSF并不是一个完全组件化的框架，它依然是基于JSP/Servlet架构的。

— JSF的成熟度还有待进一步提高。

2．Tapestry

Tapestry并不是一种单纯的MVC框架，它更像MVC框架和模板技术的结合，它不仅包含了前端的MVC框架，还包含了一种视图层的模板技术，使用Tapestry完全可以与Servlet/JSP API分离，是一种非常优秀的设计。

通过使用Tapestry，开发者完全不需要使用JSP技术，用户只需要使用Tapestry提供的模板技术即可，Tapestry实现了视图逻辑和业务逻辑的彻底分离。

Tapestry使用组件库替代了标签库，没有标签库概念，从而避免了标签库和组件结合的问题。Tapsetry是完全组件化的框架。Tapestr只有组件或页面两个概念，因此，链接跳转目标要么是组件，要么是页面，没有多余的path概念。组件名，也就是对象名称，组件名称和path名称合二为一。

Tapestry具有很高的代码复用性，在Tapestry中，任何对象都可看作可复用的组件。JSP开发者是真正面向对象，而不是URL解析。对于对页面要求灵活度相当高的系统，Tapestry是第一选择。精确地错误报告，可以将错误定位到源程序中的行，取代了JSP中那种编译后的提示。

因此，笔者一直对Tapestry情有独钟：如果技术允许，使用Tapestry会带给整个应用更加优雅的架构，更好的开发效率。

但是，在实际开发过程中，采用Tapestry也面临着一些问题必须考虑：

— Tapestry的学习曲线相对陡峭，国内开发群体不是非常活跃，文档不是十分丰富。官方的文档太过学院派，缺乏实际的示例程序。

— Tapestry的组件逻辑比较复杂，再加上OGNL表达式和属性指定机制，因而难以添加注释。

3．Spring MVC

Spring提供了一个细致完整的MVC框架。该框架为模型、视图、控制器之间提供了一个非常清晰的划分，各部分耦合极低。Spring的MVC是非常灵活的，它完全基于接口编程，真正实现了视图无关。视图不再强制要求使用JSP，可以使用Velocity、XSLT或其他视图技术。甚至可以使用自定义的视图机制——只需要简单地实现View接口，并且把对应视图技术集成进来。Spring的Controllers由IoC容器管理。因此，单元测试更加方便。

Spring MVC框架以DispatcherServlet为核心控制器，该控制器负责拦截用户的所有请求，将请求分发到对应的业务控制器。

Spring MVC还包括处理器映射、视图解析、信息国际化、主题解析、文件上传等。所有控制器都必须实现Controller接口，该接口仅定义ModelAndView handleRequest（request，response）方法。通过实现该接口来实现用户的业务逻辑控制器。

Spring MVC框架有一个极好的优势，就是它的视图解析策略：它的控制器返回一个ModelAndView对象，该对象包含视图名字和Model，Model提供了Bean的名字及其对象的对应关系。视图名解析的配置非常灵活，抽象的Model完全独立于表现层技术，不会与任何表现层耦合：JSP、Velocity或者其他的技术——都可以和Spring整合。

但相对于Tapestry框架而言，Spring MVC依然是基于JSP/Servlet API的。

总体上来看，Spring MVC框架致力于一种完美的解决方案，并与Web应用紧紧耦合在一起。这都导致了Spring MVC框架的一些缺点：

— Spring的MVC与Servlet API耦合，难以脱离Servlet容器独立运行，降低了Spring MVC框架的可扩展性。

— 太过细化的角色划分，太过烦琐，降低了应用的开发效率。

— 过分追求架构的完美，有过度设计的危险。

1.2 Struts 2的起源和背景

Struts 2以WebWork优秀的设计思想为核心，吸收了Struts 1的部分优点，建立了一个兼容WebWork和Struts 1的MVC框架，Struts 2的目标是希望可以让原来使用Struts 1、WebWork的开发人员，都可以平稳过渡到使用Struts 2框架。

1.2.1 Struts 1简介及存在的问题

从过去的岁月来看，Struts 1是所有MVC框架中不容辩驳的胜利者，不管是市场占有率，还是所拥有的开发人群，Struts 1都拥有其他MVC框架不可比拟的优势。Struts 1的成功得益于它丰富的文档、活跃的开发群体。当然，Struts 1是世界上第一个发布的MVC框架：Struts 1.0在2001年6月发布，这一点可能是使它得到如此广泛拥戴的主要原因。

为了使读者可以明白Struts 1的运行机制，下面将简要介绍Struts 1的基本框架。

Struts 1框架以ActionServlet作为核心控制器，整个应用由客户端请求驱动。当客户端向Web应用发送请求时，请求将被Struts 1的核心控制器ActionServlet拦截，ActionServlet根据请求决定是否需要调用业务逻辑控制器处理用户请求（实际上，业务逻辑控制器还是控制器，它只是负责调用模型来处理用户请求），当用户请求处理完成后，其处理结果通过JSP呈现给用户。

对于整个Struts 1框架而言，控制器就是它的核心，Struts 1的控制器由两个部分组成：核心控制器和业务逻辑控制器。其中核心控制器就是ActionServlet，由Struts 1框架提供；业务逻辑控制就是用户自定义的Action，由应用开发者提供。

对于大部分用户请求而言，都需要得到服务器的处理。当用户发送一个需要得到服务器处理的请求时，该请求被ActionServlet拦截到，ActionServlet将该请求转发给对应的业务逻辑控制器，业务逻辑控制器调用模型来处理用户请求；如果用户请求只是希望得到某个URL资源，则由ActionServlet将被请求的资源转发给用户。

Struts 1的程序运行流程如图1.7所示。

图1.7 Struts 1的程序运行流程

下面就Struts 1程序流程具体分析MVC中的三个角色。

（1）Model部分

Struts 1的Model部分主要由底层的业务逻辑组件充当，这些业务逻辑组件封装了底层数据库访问、业务逻辑方法实现。实际上，对于一个成熟的企业应用而言，Model部分也不是一个简单的JavaBean所能完成的，它可能是一个或多个EJB组件，可能是一个WebService服务。总之，Model部分封装了整个应用的所有业务逻辑，但整个部分并不是由Struts 1提供的，Struts 1也没有为实现Model组件提供任何支持。

（2）View部分

Struts 1的View部分采用JSP实现。Struts 1提供了丰富的标签库，通过这些标签库可以最大限度地减少脚本的使用。这些自定义的标签库可以输出控制器的处理结果。

虽然Struts 1提供了与Ties框架的整合，但Struts 1所支持的表现层技术非常单一：既不支持FreeMarker、Velocity等模板技术，也不支持JasperReports等报表技术。

（3）Controller部分

Struts 1的Controller由两个部分组成。

— 系统核心控制器：由Struts 1框架提供，就是系统中的ActionServlet。

— 业务逻辑控制器：由Struts 1框架提供，就是用户自己实现的Action实例。

Struts 1的核心控制器对应图1.7中的核心控制器（ActionServlet）。该控制器由Struts 1框架提供，继承HttpServlet类，因此可以配置成一个标准的Servlet，该控制器负责拦截所有HTTP请求，然后根据用户请求决定是否需要调用业务逻辑控制器，如果需要调用业务逻辑控制器，则将请求转发给Action处理，否则直接转向请求的JSP页面。

业务逻辑控制器负责处理用户请求，但业务逻辑控制器本身并不具有处理能力，而是调用Model来完成处理。

Struts 1提供了系统所需要的核心控制器，也为实现业务逻辑控制器提供了许多支持。因此，控制器部分就是Struts 1框架的核心。有时候，我们直接将MVC层称为控制器层。

提示 对于任何的MVC框架而言，其实只实现了C（控制器）部分，但它负责用控制器调用业务逻辑组件，并负责控制器与视图技术（JSP、FreeMarker和Velocity等）的整合。

对于Struts 1框架而言，因为它与JSP/Servlet耦合非常紧密，因而导致了许多不可避免的缺陷，随着Web应用的逐渐扩大，这些缺陷逐渐变成制约Struts 1发展的重要因素——这也是Struts 2出现的原因。下面具体分析Struts 1中存在的种种缺陷。

（1）支持的表现层技术单一

Struts 1只支持JSP作为表现层技术，不提供与其他表现层技术，例如Velocity、FreeMarker等技术的整合。这一点严重制约了Struts 1框架的使用，对于目前的很多Java EE应用而言，并不一定使用JSP作为表现层技术。

虽然Struts 1处理完用户请求后，并没有直接转到特定的视图资源，而是返回一个ActionForward对象（可以理解ActionForward是一个逻辑视图名），在struts-config.xml文件中定义了逻辑视图名和视图资源之间的对应关系，当ActionServlet得到处理器返回的ActionForword对象后，可以根据逻辑视图名和视图资源之间的对应关系，将视图资源呈现给用户。

从上面的设计来看，不得不佩服Struts 1的设计者高度解耦的设计：控制器并没有直接执行转发请求，而仅仅返回一个逻辑视图名——实际的转发放在配置文件中进行管理。但因为Struts 1框架出现的年代太早了，那时候还没有FreeMarker、Velocity等技术，因而没有考虑与这些FreeMarker、Velocity等视图技术的整合。

提示 Struts 1已经通过配置文件管理逻辑视图名和实际视图之间的对应关系，只是没有做到让逻辑视图名可以支持更多的视图技术。

虽然Struts 1有非常优秀的设计，但由于历史原因，它没有提供与更多视图技术的整合，这严重限制了Struts 1的使用。

（2）与Servlet API严重耦合，难于测试

因为Struts 1框架是在Model 2的基础上发展起来的，因此它完全是基于Servlet API的，所以在Struts 1的业务逻辑控制器内，充满了大量的Servlet API。

看下面的Action代码片段：

//业务逻辑控制器必须继承Struts 1提供的Action类

public class LoginAction extends Action

{

//处理用户请求的execute方法

public ActionForward execute(ActionMapping mapping, ActionForm form,

HttpServletRequest request, HttpServletResponse response)throws

AuctionException

{

//获取封装用户请求参数的ActionForm对象

//将其强制类型转换为登录用的ActionForm

LoginForm loginForm = (LoginForm)form;

//当用户名为scott,密码为tiger时返回成功

if ("scott".equals(loginForm.getUsername()

&& "tiger".equals(loginForm.getPassword())

{

//处理成功，返回一个ActionForward对象

return mapping.findForward("success");

}

else

{

//处理失败，返回一个ActionForward对象

return mapping.findForward("success");

}

}

}

当我们需要测试上面Action类的execute方法时，该方法有4个参数：ActionMapping、ActionForm、HttpServletRequest和HttpServletResponse，初始化这4个参数比较困难，尤其是HttpServletRequest和HttpServletResponse两个参数，通常由Web容器负责实例化。

因为HttpServletRequest和HttpServletResponse两个参数是Servlet API，严重依赖于Web服务器。因此，一旦脱离了Web服务器，Action的测试非常困难。

（3）代码严重依赖于Struts 1 API，属于侵入式设计

正如从上面代码片段中所看到的，Struts 1的Action类必须继承Struts 1的Action基类，实现处理方法时，又包含了大量Struts 1 API：如ActionMapping、ActionForm和ActionForward类。这种侵入式设计的最大弱点在于，一旦系统需要重构时，这些Action类将完全没有利用价值，成为一堆废品。

可见，Struts 1的Action类这种侵入式设计导致了较低的代码复用。

1.2.2 WebWork简介

WebWork虽然没有Struts 1那样赫赫有名，但也是出身名门，WebWork来自另外一个优秀的开源组织：opensymphony，这个优秀的开源组织同样开发了大量优秀的开源项目，如Qutarz、OSWorkFlow等。实际上，WebWork的创始人则是另一个Java领域的名人：Rickard Oberg（他就是JBoss和XDoclet的作者）。

相对于Struts 1存在的那些先天性不足而言，WebWork则更加优秀，它采用了一种更加松耦合的设计，让系统的Action不再与Servlet API耦合。使单元测试更加方便，允许系统从B/S结构向C/S结构转换。

相对于Struts 1仅支持JSP表现层技术的缺陷而言，WebWork支持更多的表现层技术，如Velocity、FreeMarker和XSLT等。

WebWork可以脱离Web应用使用，这一点似乎并没有太多优势，因为，一个应用通常开始已经确定在怎样的环境下使用。WebWork有自己的控制反转（Inversion of Control）容器，通过控制反转，可以让测试变得更简单，测试中设置实现服务接口的Mock对象完成测试，而不需要设置服务注册。

WebWork 2使用OGNL这个强大的表达式语言，可以访问值栈。OGNL对集合和索引属性的支持非常强大。

WebWork建立在XWork之上，使用ServletDispatcher作为该框架的核心控制器，处理HTTP的响应和请求。

从处理流程上来看，WebWork与Struts 1非常类似，它们的核心都由控制器组成，其中控制器都由两个部分组成：

— 核心控制器ServletDispatcher，该控制器框架提供。

— 业务逻辑控制器Action，该控制器由程序员提供。

相对Struts 1的Action与Servlet API紧紧耦合的弱点来说，WebWork的Action则完全与Servlet API分离，因而该Action更容易测试。

WebWork的Action可以与Servlet API分离，得益于它灵巧的设计，它使用一个拦截器链，负责将用户请求数据转发到Action，并负责将Action的处理结果转换成对用户的响应。

当用户向Web应用发送请求时，该请求经过ActionContextCleanUp、SiteMesh等过滤器过滤，由WebWork的核心控制器拦截，如果用户请求需要WebWork的业务逻辑控制器处理，该控制器则调用Action映射器，该映射器将用户请求转发到对应的业务逻辑控制器。值得注意的是，此时的业务逻辑控制器并不是开发者实现的控制器，而是WebWork创建的控制器代理。

创建控制器代理时，WebWork需要得到开发者定义的xwork.xml配置文件，控制器代理以用户实现的控制器作为目标，以拦截器链中的拦截器作为处理（Advice）。

提示 WebWork中创建控制器代理的方式，就是一种AOP（面向切面编程）编程方式，只是这种AOP中的拦截器由系统提供，因此无需用户参与。如果读者需要获取更多关于AOP编程的知识，请参阅AOP相关资料，或笔者所著的《Spring 2.0宝典》一书的第6章。

开发者自己实现的业务逻辑控制器只是WebWork业务控制器的目标——这就是为什么开发者自己实现的Action可以与Servlet API分离的原因。当开发者自己的Action处理完HTTP请求后，该结果只是一个普通字符串，该字符串将对应到指定的视图资源。

指定的试图资源经过拦截器链的处理后，生成对客户端的响应输出。

上面整个过程的数据流图如图1.8所示。

与前面的Struts 1框架对比，不难发现WebWork在很多地方确实更优秀。相对Struts 1的种种缺点而言，WebWork存在如下优点：

图1.8 WebWork的数据流图

（1）Action无需与Servlet API耦合，更容易测试

相对于Struts 1框架中的Action出现了大量Servlet API而言，WebWork的Action更像一个普通Java对象，该控制器代码中没有耦合任何Servlet API。看下面的WebWork的Action示例：

public class LoginAction implements Action

{

//该字符串常量将作为Action的返回值

private final static String LOGINFAIL="loginfail";

//该Action封装的两个请求参数

private String password;

private String username;

//password请求参数对应的getter方法

public String getPassword()

{

return password;

}

//password请求参数对应的setter方法

public void setPassword(String password)

{

this.password = password;

}

//username请求参数对应的getter方法

public String getUsername()

{

return username;

}

//username请求参数对应的setter方法

public void setUsername(String username)

{

this.username = username;

}

//处理用户请求的execute方法

public String execute() throws Exception

{

if ("yeeku".equalsIgnoreCase(getUsername())

&& "password".equals(getPassword()))

{

ActionContext ctx = ActionContext.getContext();

//将当前登录的用户名保存到Session

Map session = ctx.getSession();

session.put("username",getUsername());

return SUCCESS;

}

else

{

return LOGINFAIL;

}

}

}

在上面的Action代码中，我们看不到任何的Servlet API，当系统需要处理两个请求参数：username和password时，Action并未通过HttpServletRequest对象来获得请求参数，而是直接调用访问该Action的username和password成员属性——这两个属性由Action拦截器负责初始化，以用户请求参数为其赋值。

即使Action中需要访问HTTP Session对象，依然没有在代码中直接出现HttpSession API，而是以一个Map对象代表了HTTP Session对象。

当我们将WebWork的Action和Struts 1的Action进行对比时，不难发现Struts 1的Action确实太臃肿了，确实不如WebWork的Action那么优雅。

如果需要测试上面的Action代码，测试用例的书写将非常容易，因为execute方法中没有包含任何Servlet API，甚至没有WebWork的API。

（2）Action无需与WebWork耦合，代码重用率高

在上面的Action代码中，不难发现WebWork中的Action其实就是一个POJO，该Action仅仅实现了WebWork的Action接口，包含了一个execute方法。

Struts 1中的Action类需要继承Struts 1的Action类。我们知道，实现一个接口和继承一个类是完全不同的概念：实现一个接口对类的污染要小得多，该类也可以实现其他任意接口，还可以继承一个父类；但一旦已经继承一个父类，则意味着该类不能再继承其他父类。

除此之外，Struts 1中Action也包含了一个execute方法，但该方法需要4个参数，类型分别是ActionMapping、ActionForm、HttpServletRequest和HttpServletResponse，一个包含了这4个参数的方法，除了在Struts 1框架下有用外，笔者难以想象出该代码还有任何复用价值。但WebWork的execute方法则完全不同，该方法中没有出现任何Servlet API，也没有出现任何WebWork API，这个方法在任何环境下都有重用的价值。

得益于WebWork灵巧的设计，WebWork中的Action无需与任何Servlet API、WebWork API耦合，从而具有更好的代码重用率。

（3）支持更多的表现层技术，有更好的适应性

正如从图1.8所见到的，WebWork对多种表现层技术：JSP、Velocity和FreeMarker等都有很好的支持，从而给开发更多的选择，提供了更好的适应性。

1.2.3 Struts 2起源

经过五年多的发展，Struts 1已经成为一个高度成熟的框架，不管是稳定性还是可靠性，都得到了广泛的证明。但由于它太“老”了，一些设计上的缺陷成为它的硬伤。面对大量新的MVC框架蓬勃兴起，Struts 1也开始了血液的更新。

目前，Struts已经分化成两个框架：第一个框架就是传统Struts 1和WebWork结合后的Struts 2框架。Struts 2虽然是在Struts 1的基础上发展起来的，但实质上是以WebWork为核心，Struts 2为传统Struts 1注入了WebWork的设计理念，统一了Struts 1和WebWork两个框架，允许Struts 1和WebWork开发者同时使用Struts 2框架。

Struts分化出来的另外一个框架是Shale，这个框架远远超出了Struts 1原有的设计思想，它与原有的Struts 1的关联很少，它使用全新的设计思想。Shale更像一个新的框架，而不是Struts的升级。Shale 在很多方面与Struts存在不同之处，其中有两点最为突出：

— Struts与JSF集成，而Shale则是建立在JSF之上。

— Struts实质上是一个巨大的、复杂的请求处理器；而Shale则是一组能以任何方式进行组合的服务，简单地说，Shale是一种SOA（面向服务架构）架构。

在后面的介绍中，我们会发现，Struts 2非常类似于WebWork框架，而不像Struts 1框架，因为Struts 2是以WebWork为核心，而不是以Struts 1为核心的。正因为此，许多WebWork开发者会发现，从WebWork过渡到Struts 2是一件非常简单的事情。

当然，对于传统的Struts 1开发者，Struts 2也提供了很好的向后兼容性，Struts 2可与Struts 1有机整合，从而保证Struts 1开发者能平稳过渡到Struts 2。

1.3 Struts 2体系介绍

Struts 2的体系与Struts 1体系的差别非常大，因为Struts 2使用了WebWork的设计核心，而不是使用Struts 1的设计核心。Struts 2大量使用拦截器来处理用户请求，从而允许用户的业务逻辑控制器与Servlet API分离。

1.3.1 Struts 2框架架构

从数据流图上来看，Struts 2与WebWork相差不大，Struts 2同样使用拦截器作为处理（Advice），以用户的业务逻辑控制器为目标，创建一个控制器代理。

控制器代理负责处理用户请求，处理用户请求时回调业务控制器的execute方法，该方法的返回值将决定了Struts 2将怎样的视图资源呈现给用户。

图1.9显示了Struts 2的体系概图。

图1.9 Struts 2的体系概图

Struts 2框架的大致处理流程如下：

* 浏览器发送请求，例如请求/mypage.action、/reports/myreport.pdf等。

核心控制器FilterDispatcher根据请求决定调用合适的Action。

WebWork的拦截器链自动对请求应用通用功能，例如workflow、validation或文件上传等功能。

回调Action的execute方法，该execute方法先获取用户请求参数，然后执行某种数据库操作，既可以是将数据保存到数据库，也可以从数据库中检索信息。实际上，因为Action只是一个控制器，它会调用业务逻辑组件来处理用户的请求。

Action的execute方法处理结果信息将被输出到浏览器中，可以是HTML页面、图像，也可以是PDF文档或者其他文档。此时支持的视图技术非常多，既支持JSP，也支持Velocity、FreeMarker等模板技术。

1.3.2 Struts 2的配置文件

当Struts 2创建系统的Action代理时，需要使用Struts 2的配置文件。

Struts 2的配置文件有两份：

— 配置Action的struts.xml文件。

— 配置Struts 2全局属性的struts.properties文件。

struts.xml文件内定义了Struts 2的系列Action，定义Action时，指定该Action的实现类，并定义该Action处理结果与视图资源之间的映射关系。

下面是struts.xml配置文件的示例：

<struts>

<!-- Struts 2的Action都必须配置在package里 -->

<package name="default" extends="struts-default">

<!-- 定义一个Logon的Action，实现类为lee.Logon -->

<action name="Logon" class="lee.Logon">

<!-- 配置Action返回input时转入/pages/Logon.jsp页面 -->

<result name="input">/pages/Logon.jsp</result>

<!-- 配置Action返回cancel时重定向到Welcome的Action-->

<result name="cancel" type="redirect-action">Welcome</result>

<!-- 配置Action返回success时重定向到MainMenu的Action -->

<result type="redirect-action">MainMenu</result>

<!-- 配置Action返回expired时进入ChangePassword的Action链 -->

<result name="expired" type="chain">ChangePassword</result>

</action>

<!-- 定义Logoff的Action，实现类为lee.Logoff -->

<action name="Logoff" class=" lee.Logoff">

<!-- 配置Action返回success时重定向到MainMenu的Action -->

<result type="redirect-action">Welcome</result>

</action>

</package>

</struts>

在上面的struts.xml文件中，定义了两个Action。定义Action时，不仅定义了Action的实现类，而且的定义Action的处理结果时，指定了多个result，result元素指定execute方法返回值和视图资源之间的映射关系。对于如下配置片段：

<result name="cancel" type="redirect-action">Welcome</result>

表示当execute方法返回cancel的字符串时，跳转到Welcome的Action。定义result元素时，可以指定两个属性：type和name。其中name指定了execute方法返回的字符串，而type指定转向的资源类型，此处转向的资源可以是JSP，也可以是FreeMarker等，甚至是另一个Action——这也是Struts 2可以支持多种视图技术的原因。

除此之外，Struts 2还有一个配置Struts 2全局属性的Properties文件：struts.properties。该文件的示例如下：

#指定Struts 2处于开发状态

struts.devMode = false

//指定当Struts 2配置文件改变后，Web框架是否重新加载Struts 2配置文件

struts.configuration.xml.reload=true

正如上面见到的，struts.properties文件的形式是系列的key、value对，它指定了Struts 2应用的全局属性。

1.3.3 Strut 2的标签库

Struts 2的标签库也是Struts 2的重要组成部分，Struts 2的标签库提供了非常丰富的功能，这些标签库不仅提供了表现层数据处理，而且提供了基本的流程控制功能，还提供了国际化、Ajax支持等功能。

通过使用Struts 2的标签，开发者可以最大限度地减少页面代码的书写。

看下面的JSP页面的表单定义片段：

<!-- 定义一个Action -->

<form method="post" action="basicvalid.action">

<!-- 下面定义三个表单域 -->

名字：<input type="text" name="name"/><br>

年纪：<input type="text" name="age"/><br>

喜欢的颜色：<input type="text" name="favorite"/><br>

<!-- 定义一个输出按钮 -->

<input type="submit" value="提交"/>

</form>

上面页面使用了传统的HTML标签定义表单元素，还不具备输出校验信息的功能，但如果换成如下使用Struts 2标签的定义方式：

<!-- 使用Struts 2标签定义一个表单 -->

<s:form method="post" action="basicvalid.action">

<!-- 下面使用Struts 2标签定义三个表单域 -->

<s:textfield label="名字" name="name"/>

<s:textfield label="年纪" name="age"/>

<s:textfield label="喜欢的颜色" name="answer"/>

<!-- 定义一个提交按钮 -->

<s:submit/>

</s:form>

则页面代码更加简洁，而且有更简单的错误输出。图1.10是上面使用Struts 2标签执行数据校验后的输出。

图1.10 使用Struts 2标签的效果

提示 Struts 2的标签库的功能非常复杂，该标签库几乎可以完全替代JSTL的标签库。而且Struts 2的标签支持表达式语言，这种表达式语言支持一个强大和灵活的表达式语言：OGNL（Object Graph Notation Language），因此功能非常强大。

1.3.4 Struts 2的控制器组件

Struts 2的控制器组件是Struts 2框架的核心，事实上，所有MVC框架都是以控制器组件为核心的。正如前面提到的，Struts 2的控制器由两个部分组成：FilterDispatcher和业务控制器Action。

实际上，Struts 2应用中起作用的业务控制器不是用户定义的Action，而是系统生成的Action代理，但该Action代理以用户定义的Action为目标。

下面是Struts 2的Action代码示例：

public class LoginAction

{

//封装用户请求参数的username属性

private String username;

//封装用户请求参数的password属性

private String password;

//username属性的getter方法

public String getUsername()

{

return username;

}

//username属性的setter方法

public void setUsername(String username)

{

this.username = username;

}

//password属性的getter方法

public String getPassword()

{

return password;

}

//password属性的setter方法

public void setPassword(String password)

{

this.password = password;

}

//处理用户请求的execute方法

public String execute() throws Exception

{

//如果用户名为scott，密码为tiger，则登录成功

if (getUsername().equals("scott")

&& getPassword().equals("tiger") )

{

return "success";

}

else

{

return "error";

}

}

}

通过查看上面的Action代码，发现该Action比WebWork中的Action更彻底，该Action无需实现任何父接口，无需继承任何Struts 2基类，该Action类完全是一个POJO（普通、传统的Java对象），因此具有很好的复用性。

归纳起来，该Action类有如下优势：

— Action类完全是一个POJO，因此具有很好的代码复用性。

— Action类无需与Servlet API耦合，因此进行单元测试非常简单。

— Action类的execute方法仅返回一个字符串作为处理结果，该处理结果可映射到任何的视图，甚至是另一个Action。

1.4 Struts 2与Struts 1的对比

经过上面简要介绍，不难发现，Struts 2确实在Struts 1上做出了巨大的改进，的确是一个非常具有实用价值的MVC框架。下面是Struts 1和Struts 2在各方面的简要对比。

— 在Action实现类方面的对比：Struts 1要求Action类继承一个抽象基类；Struts 1的一个具体问题是使用抽象类编程而不是接口。Struts 2 Action类可以实现一个Action接口，也可以实现其他接口，使可选和定制的服务成为可能。Struts 2提供一个ActionSupport基类去实现常用的接口。即使Action接口不是必须实现的，只有一个包含execute方法的POJO类都可以用作Struts 2的Action。

— 线程模式方面的对比：Struts 1 Action是单例模式并且必须是线程安全的，因为仅有Action的一个实例来处理所有的请求。单例策略限制了Struts 1 Action能做的事，并且要在开发时特别小心。Action资源必须是线程安全的或同步的；Struts 2 Action对象为每一个请求产生一个实例，因此没有线程安全问题。

— Servlet依赖方面的对比：Struts 1 Action依赖于Servlet API，因为Struts 1 Action的execute方法中有HttpServletRequest和HttpServletResponse方法。Struts 2 Action不再依赖于Servlet API，从而允许Action脱离Web容器运行，从而降低了测试Action的难度。 当然，如果Action需要直接访问HttpServletRequest和HttpServletResponse参数，Struts 2 Action仍然可以访问它们。但是，大部分时候，Action都无需直接访问HttpServetRequest和HttpServletResponse，从而给开发者更多灵活的选择。

— 可测性方面的对比：测试Struts 1 Action的一个主要问题是execute方法依赖于Servlet API，这使得Action的测试要依赖于Web容器。为了脱离Web容器测试Struts 1的Action，必须借助于第三方扩展：Struts TestCase，该扩展下包含了系列的Mock对象（模拟了HttpServetRequest和HttpServletResponse对象），从而可以脱离Web容器测试Struts 1的Action类。Struts 2 Action可以通过初始化、设置属性、调用方法来测试。

— 封装请求参数的对比：Struts 1使用ActionForm对象封装用户的请求参数，所有的ActionForm必须继承一个基类：ActionForm。普通的JavaBean不能用作ActionForm，因此，开发者必须创建大量的ActionForm类封装用户请求参数。虽然Struts 1提供了动态ActionForm来简化ActionForm的开发，但依然需要在配置文件中定义ActionForm；Struts 2直接使用Action属性来封装用户请求属性，避免了开发者需要大量开发ActionForm类的烦琐，实际上，这些属性还可以是包含子属性的Rich对象类型。如果开发者依然怀念Struts 1 ActionForm的模式，Struts 2提供了ModelDriven模式，可以让开发者使用单独的Model对象来封装用户请求参数，但该Model对象无需继承任何Struts 2基类，是一个POJO，从而降低了代码污染。

— 表达式语言方面的对比：Struts 1整合了JSTL，因此可以使用JSTL表达式语言。这种表达式语言有基本对象图遍历，但在对集合和索引属性的支持上则功能不强；Struts 2可以使用JSTL，但它整合了一种更强大和灵活的表达式语言：OGNL（Object Graph Notation Language），因此，Struts 2下的表达式语言功能更加强大。

— 绑定值到视图的对比：Struts 1使用标准JSP机制把对象绑定到视图页面；Struts 2使用“ValueStack”技术，使标签库能够访问值，而不需要把对象和视图页面绑定在一起。

— 类型转换的对比：Struts 1 ActionForm 属性通常都是String类型。Struts 1使用Commons-Beanutils进行类型转换，每个类一个转换器，转换器是不可配置的；Struts 2使用OGNL进行类型转换，支持基本数据类型和常用对象之间的转换。

— 数据校验的对比：Struts 1支持在ActionForm重写validate方法中手动校验，或者通过整合Commons alidator框架来完成数据校验。Struts 2支持通过重写validate方法进行校验，也支持整合XWork校验框架进行校验。

— Action执行控制的对比：Struts 1支持每一个模块对应一个请求处理（即生命周期的概念），但是模块中的所有Action必须共享相同的生命周期。Struts 2支持通过拦截器堆栈（Interceptor Stacks）为每一个Action创建不同的生命周期。开发者可以根据需要创建相应堆栈，从而和不同的Action一起使用。

1.5 WebWork和Struts 2对比

从某种程度上来看，Struts 2是WebWork的升级，而不是Struts 1的升级，甚至在Apache的Struts 2的官方文档都提到：WebWork到Struts 2是一次平滑的过渡。实际上，Struts 2.0其实是WebWork 2.3而已，从WebWork 2.2迁移到Struts 2.0不会比从WebWork 2.1到2.2更麻烦。

在很多方面，Struts 2仅仅是改变了WebWork下的名称，因此，如果开发者具有WebWork的开发经验，将可以更加迅速地进入Struts 2的开发领域。

下面是Struts 2与WebWork命名上存在改变（见表1.1）：

表1.1 Struts 2和WebWork成员名称的对应

Struts 2成员 | WebWork成员
com.opensymphony.xwork2.* | com.opensymphony.xwork.*
org.apache.Struts2.* | com.opensymphony.webwork.*
struts.xml | xwork.xml
struts.properties | webwork.properties
Dispatcher | DispatcherUtil
org.apache.Struts2.config.Settings | com.opensymphony.webwork.config.Configuration

除此之外，Struts 2也删除了WebWork中少量特性：

— AroundInterceptor：Struts 2不再支持WebWork中的AroundInterceptor。如果应用程序中需要使用AroundInterceptor，则应该自己手动导入WebWork中的AroundInterceptor类。

— 富文本编辑器标签：Struts 2不再支持WebWork的富文本编辑器，如果应用中需要使用富文本编辑器，则应该使用Dojo的富文本编辑器。

— IoC容器支持：Struts 2不再支持内建的IoC容器，而改为全面支持Spring的IoC容器，以Spring的IoC容器作为默认的Object工厂。

1.6 本章小结

本章大致介绍了Web应用的开发历史，从历史的角度介绍了Model 1和Model 2的简要模型和特征，进而介绍了MVC模式的主要策略和主要优势。接着介绍了常用的MVC框架，包括JSF、Tapestry和Spring MVC，以及这些框架的基本知识和相关特征。本章重点介绍了Struts 2的两个前身：Struts 1和WebWork，以及这两个框架的架构和主要特征，从而引申出对Struts 2起源的介绍。最后大致介绍了Struts 2框架的体系，包括Struts 2框架的架构、标签库、控制器组件等，并就Struts 1和Struts 2的相关方面进行了比较。

下一章将以一个简单的HelloWorld应用为示例，来介绍基于Struts 2的应用，让读者感受基于Struts 2的MVC应用。


为Struts 2

为Struts 2.0做好准备

Struts作为MVC 2的Web框架，自推出以来不断受到开发者的追捧，得到用广泛的应用。作为最成功的Web框架，Struts自然拥有众多的优点：

· MVC 2模型的使用

· 功能齐全的标志库（Tag Library）

· 开放源代码

但是，所谓“金无赤金，人无完人”，Struts自身也有不少的缺点：

· 需要编写的代码过多，容易引起“类爆炸”

· 单元测试困难

这些缺点随着Web的发展越来越明显。这就促生了Struts 2.0，它的诞生能很好的解决上述问题。 好啦，废话就不多说了，现在就让我们感受一下的Struts 2.0的魅力吧。

1. 搭建开发和运行环境

1. 到Apache下载Struts 2.0包

2. 打开Eclipse 3.2新建Web工程

点击菜单File\New\Project，出现如图1所示对话框
图1 新建工程对话框
图1 新建工程对话框
选择Web\Dynamic Web Project，点击“Next”，出现图2对话框

图2 新建动态Web工程对话框
图2 新建动态Web工程对话框
在“Project Name”中键入Struts2_HelloWorld，点击“New”，出现以下对话框

图3 新建服务器运行时对话框
图3 新建服务器运行时对话框
选择“Apache\Apache Tomat v5.5”，点击“Next”，出现以下对话框

图4新建服务器运行时对话框
图4新建服务器运行时对话框
点击“Finish”，关闭对话框。

3. 将Struts 2.0 lib下的jar文件加到工程的构建路径（build path）

图5 Struts 2.0的lib目录
图5 Struts 2.0的lib目录
按ctr+a全选，复制，再转到Eclipse窗口，在“Project Explorer”子窗口中选中Struts2_HelloWorld\WebContent\WEB-INF\lib，然后粘贴。经过Eclipse自动刷新“Project Explorer”子窗口，刚才所粘贴的jar文件应该会出现在Struts2_HelloWorld\Java Resources: src\Libraries\Web App Libraries下，如图6所示：

图6 Project Explorer子窗口
图6 Project Explorer子窗口

4. 打开web.xml文件，将其修改为以下代码：

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<display-name>Struts 2.0 Hello World</display-name>
<filter>
<filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>

5. 新建struts.xml文件

右键点击，Struts2_HelloWorld\Java Resources: src，出现如图7所示菜单
图7 新建Other菜单
图7 新建Other菜单
点击“Other”，出现新建对话框，如图8所示

图8 新建对话框
图8 新建对话框
点击“Next”，出现新建文件对话框，如图9所示

图9 新建文件对话框
图9 新建文件对话框
在“File name”中键入sturts.xml，点击“Finish”，然后将struts.xml的内容修改为：

<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<include file="struts-default.xml"/>
</struts>

6. 新建index.html文件

右键点击Struts2_HelloWorld\WebContent，出现如图10所示的菜单
图10 新建Other菜单
图10 新建Other菜单
点击“Other”，出现新建对话框，如图11所示

图11 新建对话框
图11 新建对话框
选择Web\HTML，点击“Next”出现如图12所示的对话框

图12 新建HTML页面对话框
图12 新建HTML页面对话框
在“File Name”中键入index.html，点击“Next”，出现如图13所示的对话框

图13 模板选择对话框
图13 模板选择对话框
点击“Finish”，将index.html的内容修改为以下内容：

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Hello World</title>
</head>
<body>
<h3>Hello World!</h3>
</body>
</html>

7. 将应用程序打包到tomcat上

右键点击Struts_HelloWorld，出现如图14所示的菜单
图14 工程菜单
图14 工程菜单
点击“Export\WAR file”，出现如图15所示的对话框

图15 输出对话框
图15 输出对话框
选择“Web\WAR file”，点击“Next”，出现如图16所示的对话框

图16 输出路径对话框
图16 输出路径对话框
输入war文件的路径（如%tomcat%\webapps\Struts2_HelloWorld.war），点击“Finish”关闭对话框。

8. 启动tomcat，运行应用程序

打开你的Internet Explorer，键入http://localhost:8080/Struts2_HelloWorld/，窗口输出如图17所示
图17 Hello World窗口
图17 Hello World窗口

2. 第一个Struts 2.0应用程序——Hello World

1. 新建类包（package）

右键点击Struts2_HelloWorld\Java Resources: src，出现如图18所示菜单
图18 新建菜单
图18 新建菜单"
点击“New\Package”，出现如图19所示对话框

图19新建Java类包对话框
图19新建Java类包对话框
在“Name”键入tutorial，点击“Finish”关闭对话框。

2. 新建HelloWorld.java文件

右键点击Struts2_HelloWorld\Java Resources: src\tutorial，出现如图20所示菜单
图20 新建菜单
图20 新建菜单
点击“New\Class”，出现如图21所示对话框

图21 新建Java类对话框
图21 新建Java类对话框
在“Name”中键入HelloWorld，在“Superclass”中键入com.opensymphony.xwork2.ActionSupport，点击“Finish”关闭对话框。将HelloWorld.java的内容修改为：

package tutorial;

import com.opensymphony.xwork2.ActionSupport;

public class HelloWorld extends ActionSupport [image] {
private String name;

public String getName() [image] {
return name;
}

public void setName(String name) [image] {
this.name = name;
}

public String execute() [image] {
name = "Hello, " + name + "!";
return SUCCESS;
}
}

3. 在struts.xml中添加action映射(mapping)

<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<include file="struts-default.xml"/>
<package name="tutorial" extends="struts-default">
<action name="HelloWorld" class="tutorial.HelloWorld">
<result>HelloWorld.jsp</result>
</action>
</package>
</struts>

4. 新建SayHello.jsp

参考“新建index.html文件”步骤，弹出如图22所示对话框
图22 新建对话框
图22 新建对话框
点击“Next”, 进入下一步，如图23所示

图23 新建JSP对话框
图23 新建JSP对话框
在“File name”键入SayHello.jsp，点击“Next”进入下一步，如图24所示

图24 模板选择对话框
图24 模板选择对话框
点击“Finish”关闭对话框，并将SayHello.jsp的内容修改为：

<% [image] @ page contentType="text/html; charset=UTF-8" %>
<% [image] @ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Say Hello</title>
</head>
<body>
<h3>Say "Hello" to: </h3>
<s:form action="HelloWorld">
Name: <s:textfield name="name" />
<s:submit />
</s:form>
</body>
</html>

5. 新建HelloWorld.jsp(请参考上一步)，HelloWorld.jsp的内容为：

<% [image] @ page contentType="text/html; charset=UTF-8" %>
<% [image] @ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Hello</title>
</head>
<body>
<h3><s:property value="name" /></h3>
</body>
</html>

6. 重新打包发布应用程序

先停止tomcat, 再将tomcat里webapps下的Struts2_HelloWorld.war和Struts2_HelloWorld文件夹删除，参照“将应用程序打包到tomcat上”重新发布应用程序。

7. 启动tomcat，运行测试

打开Internet Explorer，键入http://localhost:8080/Struts2_HelloWorld/SayHello.jsp，窗口输出如图25所示

图25 SayHello.jsp
在“Name”键入字符串（如World），点击Submit，转到HelloWorld.jsp页面，如图26所示

图26 HelloWorld.jsp

3. 单元测试Hello World

在文章开始的时候提及，单元测试困难是Struts一大缺点。现在让我们在体验一下，在Struts 2.0中是如何进行测试的。

1. 新建JUnit单元测试

右键点击Struts2_HelloWorld\Java Resources: src\tutorial，弹出如图27所示对话框
图27 新建菜单
图27 新建菜单
点击“Next\Other”

图28 新建对话框
图28 新建对话框
选择“Java\JUnit\JUnit Test Case”，点击“Next”

图29 新建JUnit 测试用例对话框
图29 新建JUnit 测试用例对话框
选择“New JUnit 4 test”，在“Name”中键入HelloWorldTest，在“Class under test”键入tutorial.HelloWorld，点击“Next”

图30 选择方法对话框
图30 选择方法对话框
选中HelloWorld\execute方法，点击Finish。如果生成的HelloWorldTest.java文件的图标（Icon）出现红色交叉标志，请进行以下步骤添加JUnit 4的jar包。

右键点击Struts2_HelloWorld，出现如图所示菜单。 图31 新建菜单
图31 新建菜单
点击“Build Path\Add Libararis”，弹出图32对话框

图32 添加库对话框
图32 添加库对话框
选中“JUnit”，点击“Next”

图33 选择版本对话框
图33 选择版本对话框
选择“JUnit 4”，点击“Finish”关闭对话框，并将HelloWorldTest.java的内容修改为：

package tutorial;

import static org.junit.Assert.assertTrue;

import org.junit.Test;

import com.opensymphony.xwork2.ActionSupport;

public class HelloWorldTest [image] {

@Test
public void testExecute() [image] {
HelloWorld hello = new HelloWorld();
hello.setName("World");
String result = hello.execute();

assertTrue("Expected a success result!", ActionSupport.SUCCESS.equals(result));

final String msg = "Hello, World!";
assertTrue("Expected the default message!", msg.equals(hello.getName()));
}

}

2. 运行单元测试

右键点击Struts2_HelloWorld\Java Resources: src\tutorial\HelloWorldTest.java，弹出如图34所示菜单
图34 运行为菜单
图34 运行为菜单
点击“Run As\JUnit Test”，出现JUnit子窗口如图35所示

图35 JUnit子窗口
图35 JUnit子窗口
图35的绿色矩形表示，所有单元测试通过。

4. 总结

上面的例子简单地演示了，Web 应用程序的基本操作，也即是，页面输入->Action处理->再输出到另外页面。Struts 2.0的简单易用、方便测试相信也会给大家留下不错的印象吧。我相信，Struts 2.0作为一个全新的Web架构，将会再次掀起Web开发的热潮。 不过，Struts 2.0还在测试中，正式版的发布还需些时日，所以文档方面可能有所欠缺。请大家继续留意我的博客，我会尽我所能为大家写更多关于Struts 2.0的文章。


常用的Struts 2

常用的Struts 2.0的标志（Tag）介绍

在上一篇文章《为Struts 2.0做好准备》中，我过于详细地介绍了Struts 2.0开发环境和运行环境的配置，所以，本文很少涉及的以上两方面的细节。如果，您看完《为Struts 2.0做好准备》后，还有什么不明白，或者没法运行文中例子，请联系我。我的E-MAIL：Max.M.Yuan@gmail.com。

在介绍常用标志前，我想先从总体上，对Struts 1.x与Struts 2.0的标志库（Tag Library）作比较。

 | Struts 1.x | Struts 2.0
分类 | 将标志库按功能分成HTML、Tiles、Logic和Bean等几部分 | 严格上来说，没有分类，所有标志都在URI为“/struts-tags”命名空间下，不过，我们可以从功能上将其分为两大类：非UI标志和UI标志
表达式语言（expression languages） | 不支持嵌入语言（EL） | OGNL、JSTL、Groovy和Velcity

以上表格，纯属个人总结，如有所不足或错误，请不吝指正

好了，我要开始介绍“常用”（这里所谓的“常用”，是指在已往工作中使用Struts里经常用到的）的标志了。

要在JSP中使用Struts 2.0标志，先要指明标志的引入。通过在JSP的代码的顶部加入以下代码可以做到这点。
<%@taglib prefix="s" uri="/struts-tags" %>

1. 非UI标志

o                              if、elseif和else

描述：
执行基本的条件流转。

参数：

名称 | 必需 | 默认 | 类型 | 描述 | 备注
test | 是 |  | Boolean | 决定标志里内容是否显示的表达式 | else标志没有这个参数
id | 否 |  | Object/String | 用来标识元素的id。在UI和表单中为HTML的id属性 |

例子：

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Condition Flow</title>
</head>
<body>
<h3>Condition Flow</h3>
<!--
这里有点小技巧：
本来可以用#parameters.name[0]来获得，请求中name的值。但是，在我实现include例子时，
无论我用param标志给name赋任何值，#parameters里面不会含有任何值，所以#parameters.name也为空值。

其原因为：
当使用include标志时，被包含的页面（included）里#parameters拿到的是包含页面里的请求参数。

因此，这里必须手工调用request.getParameter("name")。
-->
<s:set name="name" value="<%= "'" + request.getParameter("name") + "'" %>" />
<s:if test="#name == 'Max'">
Max's file here
</s:if>
<s:elseif test="#name == 'Scott'">
Scott's file here
</s:elseif>
<s:else>
Other's file here
</s:else>
</body>
</html>

例1 condition.jsp

o                              iterator

描述：
用于遍历集合（java.util.Collection）或枚举值（java.util.Iterator）。

参数：

名称 | 必需 | 默认 | 类型 | 描述
status | 否 |  | String | 如果设置此参数，一个IteratorStatus的实例将会压入每个遍历的堆栈
value | 否 |  | Object/String | 要遍历的可枚举的（iteratable）数据源，或者将放入新列表（List）的对象
id | 否 |  | Object/String | 用来标识元素的id。在UI和表单中为HTML的id属性

例子：

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%@ taglib prefix="s" uri="/struts-tags" %>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%
List list = new ArrayList();
list.add("Max");
list.add("Scott");
list.add("Jeffry");
list.add("Joe");
list.add("Kelvin");
request.setAttribute("names", list);
%>
<html>
<head>
<title>Iterator</title>
</head>
<body>
<h3>Names: </h3>
<!--
1、此处的空property元素用于获得当前iterator的值
2、status被设成stuts，在iterator的里面就可以通过#stuts取得IteratorStatus的对象。IteratorStatus类包含当前序号信息，如是否第一个或最后一个，是否为奇数序号。这些信息在我们做格式化的时候，显得非常有用。
-->
<ol>
<s:iterator value="#request.names" status="stuts">
<s:if test="#stuts.odd == true">
<li>White <s:property /></li>
</s:if>
<s:else>
<li style="background-color:gray"><s:property /></li>
</s:else>
</s:iterator>
</ol>
</body>
</html>

例2 iterator.jsp

o                              i18n

描述：
加载资源包到值堆栈。它可以允许text标志访问任何资源包的信息，而不只当前action相关联的资源包。

参数：

名称 | 必需 | 默认 | 类型 | 描述
value | 是 |  | Object/String | 资源包的类路径（如com.xxxx.resources.AppMsg）
id | 否 |  | Object/String | 用来标识元素的id。在UI和表单中为HTML的id属性

例子：

HelloWorld=Hello Wrold!

例3 classes\ ApplicationMessages.properties

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Internationization</title>
</head>
<body>
<h3>
<s:i18n name="ApplicationMessages">
<s:text name="HelloWorld" />
</s:i18n>
</h3>
</body>
</html>

例3 i18n.jsp

o                              include

描述：
包含一个servlet的输出（servlet或jsp的页面）。

参数：

名称 | 必需 | 默认 | 类型 | 描述
value | 是 |  | String | 要包含的jsp或servlet
id | 否 |  | Object/String | 用来标识元素的id。在UI和表单中为HTML的id属性

例子：

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Iterator</title>
</head>
<body>
<h3>Interator Page</h3>
<s:include value="/condition.jsp">
<s:param name="name">Max</s:param>
</s:include>
<h3>i18n</h3>
<s:include value="/i18n.jsp" />
</body>
</html>

例4 include.jsp

o                              param

描述：
为其他标签提供参数,比如include标签和bean标签. 参数的name属性是可选的,如果提供，会调用Component的方法addParameter(String, Object), 如果不提供，则外层嵌套标签必须实现UnnamedParametric接口(如TextTag)。

value的提供有两种方式,通过value属性或者标签中间的text,不同之处我们看一下例子:

<param name="color">blue</param><!-- (A) -->

<param name="color" value="blue"/><!-- (B) -->
(A)参数值会以String的格式放入statck.
(B)该值会以java.lang.Object的格式放入statck.

参数：

名称 | 必需 | 默认 | 类型 | 描述
name | 否 |  | String | 参数名
value | 否 |  | String | value表达式
id | 否 |  | Object/String | 用来标识元素的id。在UI和表单中为HTML的id属性

例子：
请参考例4。

o                              set

描述：
set标签赋予变量一个特定范围内的值。当希望给一个变量赋一个复杂的表达式,每次访问该变量而不是复杂的表达式时用到。其在两种情况下非常有用: 复杂的表达式很耗时 (性能提升) 或者很难理解 (代码可读性提高)。

参数：

名称 | 必需 | 默认 | 类型 | 描述
name | 是 |  | String | 变量名字
scope | 否 |  | String | 变量作用域,可以为application, session, request, page, 或action.
value | 否 |  | Object/String | 将会赋给变量的值
id | 否 |  | Object/String | 用来标识元素的id。在UI和表单中为HTML的id属性

例子：
请参考例1。

o                              text

描述：
支持国际化信息的标签。国际化信息必须放在一个和当前action同名的resource bundle中,如果没有找到相应message,tag body将被当作默认message,如果没有tag body,message的name会被作为默认message。

参数：

名称 | 必需 | 默认 | 类型 | 描述
name | 是 |  | String | 资源属性的名字
id | 否 |  | Object/String | 用来标识元素的id。在UI和表单中为HTML的id属性

例子：
请参考例3。

o                              url

描述：
该标签用于创建url,可以通过"param"标签提供request参数。

当includeParams的值时'all'或者'get', param标签中定义的参数将有优先权,也就是说其会覆盖其他同名参数的值。

参数： 略

例子：

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>URL</title>
</head>
<body>
<h3>URL</h3>
<a href='<s:url value="/i18n.jsp" />'>i18n</a><br />
<s:url id="url" value="/condition.jsp">
<s:param name="name">Max</s:param>
</s:url>
<s:a href="%{url}">if\elseif\else</s:a>
</body>
</html>

例5 url.jsp

o                              property

描述：
得到'value'的属性,如果value没提供,默认为堆栈顶端的元素。

参数：

名称 | 必需 | 默认 | 类型 | 描述
default | 否 |  | String | 如果属性是null则显示的default值
escape | 否 | true | Booelean | 是否escape HTML
value | 否 | 栈顶 | Object | 要显示的值
id | 否 |  | Object/String | 用来标识元素的id。在UI和表单中为HTML的id属性

例子：
请参考例2。

2. UI标志

UI标志又可以分为表单UI和非表单UI两部分。表单UI部分基本与Struts 1.x相同，都是对HTML表单元素的包装。不过，Struts 2.0加了几个我们经常在项目中用到的控件如：datepicker、doubleselect、timepicker、optiontransferselect等。因为这些标志很多都经常用到，而且参数也很多，要在一篇文章详细说明并非易事。

需要深入了解这些标志的朋友，可以到以下查看以下网址：
http://wiki.javascud.org/display/ww2cndoc/Tags WebWork2文档中文化计划（中文）
http://cwiki.apache.org/WW/tag-reference.html Tag Developers Guide（英文）
本文有相当的内容也来自这两处。

在此，我虽然不能够详细讲述这些标志，但是可以与大家分享一个来Struts 2.0 Show Case一个例子。

/**//*
* $Id: UITagExample.java 420385 2006-07-10 00:57:05Z mrdon $
*
* Copyright 2006 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.struts2.showcase;

import org.apache.struts2.ServletActionContext;
import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.Validateable;
import com.opensymphony.xwork2.util.OgnlValueStack;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.io.File;

/** *//**
*/
public class UITagExample extends ActionSupport implements Validateable [image] {

private static final long serialVersionUID = -94044809860988047L;
String name;
Date birthday;
String bio;
String favoriteColor;
List friends;
boolean legalAge;
String state;
String region;
File picture;
String pictureContentType;
String pictureFileName;
String favouriteLanguage;
String favouriteVehicalType = "MotorcycleKey";
String favouriteVehicalSpecific = "YamahaKey";

List leftSideCartoonCharacters;
List rightSideCartoonCharacters;

List favouriteLanguages = new ArrayList();
List vehicalTypeList = new ArrayList();
Map vehicalSpecificMap = new HashMap();

String thoughts;

public UITagExample() [image] {
favouriteLanguages.add(new Language("EnglishKey", "English Language"));
favouriteLanguages.add(new Language("FrenchKey", "French Language"));
favouriteLanguages.add(new Language("SpanishKey", "Spanish Language"));

VehicalType car = new VehicalType("CarKey", "Car");
VehicalType motorcycle = new VehicalType("MotorcycleKey", "Motorcycle");
vehicalTypeList.add(car);
vehicalTypeList.add(motorcycle);

List cars = new ArrayList();
cars.add(new VehicalSpecific("MercedesKey", "Mercedes"));
cars.add(new VehicalSpecific("HondaKey", "Honda"));
cars.add(new VehicalSpecific("FordKey", "Ford"));

List motorcycles = new ArrayList();
motorcycles.add(new VehicalSpecific("SuzukiKey", "Suzuki"));
motorcycles.add(new VehicalSpecific("YamahaKey", "Yamaha"));

vehicalSpecificMap.put(car, cars);
vehicalSpecificMap.put(motorcycle, motorcycles);
}

public List getLeftSideCartoonCharacters() [image] {
return leftSideCartoonCharacters;
}
public void setLeftSideCartoonCharacters(List leftSideCartoonCharacters) [image] {
this.leftSideCartoonCharacters = leftSideCartoonCharacters;
}

public List getRightSideCartoonCharacters() [image] {
return rightSideCartoonCharacters;
}
public void setRightSideCartoonCharacters(List rightSideCartoonCharacters) [image] {
this.rightSideCartoonCharacters = rightSideCartoonCharacters;
}

public String getFavouriteVehicalType() [image] {
return favouriteVehicalType;
}

public void setFavouriteVehicalType(String favouriteVehicalType) [image] {
this.favouriteVehicalType = favouriteVehicalType;
}

public String getFavouriteVehicalSpecific() [image] {
return favouriteVehicalSpecific;
}

public void setFavouriteVehicalSpecific(String favouriteVehicalSpecific) [image] {
this.favouriteVehicalSpecific = favouriteVehicalSpecific;
}

public List getVehicalTypeList() [image] {
return vehicalTypeList;
}

public List getVehicalSpecificList() [image] {
OgnlValueStack stack = ServletActionContext.getValueStack(ServletActionContext.getRequest());
Object vehicalType = stack.findValue("top");
if (vehicalType != null && vehicalType instanceof VehicalType) [image] {
List l = (List) vehicalSpecificMap.get(vehicalType);
return l;
}
return Collections.EMPTY_LIST;
}

public List getFavouriteLanguages() [image] {
return favouriteLanguages;
}

public String execute() throws Exception [image] {
return SUCCESS;
}

/**//* [image] Getters and Setters [image] */

public String doSubmit() [image] {
return SUCCESS;
}

// === inner class
public static class Language [image] {
String description;
String key;

public Language(String key, String description) [image] {
this.key = key;
this.description = description;
}

public String getKey() [image] {
return key;
}
public String getDescription() [image] {
return description;
}

}

public static class VehicalType [image] {
String key;
String description;
public VehicalType(String key, String description) [image] {
this.key = key;
this.description = description;
}

public String getKey() [image] { return this.key; }
public String getDescription() [image] { return this.description; }

public boolean equals(Object obj) [image] {
if (! (obj instanceof VehicalType)) [image] {
return false;
}
else [image] {
return key.equals(((VehicalType)obj).getKey());
}
}

public int hashCode() [image] {
return key.hashCode();
}
}

public static class VehicalSpecific [image] {
String key;
String description;
public VehicalSpecific(String key, String description) [image] {
this.key = key;
this.description = description;
}

public String getKey() [image] { return this.key; }
public String getDescription() [image] { return this.description; }

public boolean equals(Object obj) [image] {
if (! (obj instanceof VehicalSpecific)) [image] {
return false;
}
else [image] {
return key.equals(((VehicalSpecific)obj).getKey());
}
}

public int hashCode() [image] {
return key.hashCode();
}
}
}

例6 org.apache.struts2.showcase.UITagExample.java

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<title>UI Tags Example</title>
<s:head/>
</head>
<body>

<s:actionerror/>
<s:actionmessage/>
<s:fielderror />

<s:form action="exampleSubmit" method="post" enctype="multipart/form-data" tooltipConfig="#{'jsTooltipEnabled':'true'}">
<s:textfield
label="Name"
name="name"
tooltip="Enter your Name here" />

<s:datepicker
tooltip="Select Your Birthday"
label="Birthday"
name="birthday" />

<s:textarea
tooltip="Enter your Biography"
label="Biograph"
name="bio"
cols="20"
rows="3"/>

<s:select
tooltip="Choose Your Favourite Color"
label="Favorite Color"
list="{'Red', 'Blue', 'Green'}"
name="favoriteColor"
emptyOption="true"
headerKey="None"
headerValue="None"/>

<s:select
tooltip="Choose Your Favourite Language"
label="Favourite Language"
list="favouriteLanguages"
name="favouriteLanguage"
listKey="key"
listValue="description"
emptyOption="true"
headerKey="None"
headerValue="None"/>

<s:checkboxlist
tooltip="Choose your Friends"
label="Friends"
list="{'Patrick', 'Jason', 'Jay', 'Toby', 'Rene'}"
name="friends"/>

<s:checkbox
tooltip="Confirmed that your are Over 18"
label="Age 18+"
name="legalAge"/>

<s:doubleselect
tooltip="Choose Your State"
label="State"
name="region" list="{'North', 'South'}"
value="'South'"
doubleValue="'Florida'"
doubleList="top == 'North' ? {'Oregon', 'Washington'} : {'Texas', 'Florida'}"
doubleName="state"
headerKey="-1"
headerValue="---------- Please Select ----------"
emptyOption="true" />

<s:doubleselect
tooltip="Choose your Vehical"
label="Favourite Vehical"
name="favouriteVehicalType"
list="vehicalTypeList"
listKey="key"
listValue="description"
value="'MotorcycleKey'"
doubleValue="'YamahaKey'"
doubleList="vehicalSpecificList"
doubleListKey="key"
doubleListValue="description"
doubleName="favouriteVehicalSpecific" headerKey="-1"
headerValue="---------- Please Select ----------"
emptyOption="true" />

<s:file
tooltip="Upload Your Picture"
label="Picture"
name="picture" />

<s:optiontransferselect
tooltip="Select Your Favourite Cartoon Characters"
label="Favourite Cartoons Characters"
name="leftSideCartoonCharacters"
leftTitle="Left Title"
rightTitle="Right Title"
list="{'Popeye', 'He-Man', 'Spiderman'}"
multiple="true"
headerKey="headerKey"
headerValue="--- Please Select ---"
emptyOption="true"
doubleList="{'Superman', 'Mickey Mouse', 'Donald Duck'}"
doubleName="rightSideCartoonCharacters"
doubleHeaderKey="doubleHeaderKey"
doubleHeaderValue="--- Please Select ---"
doubleEmptyOption="true"
doubleMultiple="true" />

<s:textarea
label="Your Thougths"
name="thoughts"
tooltip="Enter your thoughts here" />

<s:submit onclick="alert('aaaa');" />
<s:reset onclick="alert('bbbb');" />
</s:form>

</body>
</html>

例6 example.jsp

<action name="example" class="org.apache.struts2.showcase.UITagExample">
<result>example.jsp</result>
<result name="input">example.jsp</result>
</action>

例6 struts.xml代码片段


Struts 2

Struts 2.0的Action讲解

有Struts 1.x经验的朋友都知道Action是Struts的核心内容，当然Struts 2.0也不例外。不过，Struts 1.x与Struts 2.0的Action模型很大的区别。

 | Struts 1.x | Stuts 2.0
接口 | 必须继承org.apache.struts.action.Action或者其子类 | 无须继承任何类型或实现任何接口
表单数据 | 表单数据封装在FormBean中 | 表单数据包含在Action中，通过Getter和Setter获取

虽然，理论上Struts 2.0的Action无须实现任何接口或继承任何类型，但是，我们为了方便实现Action，大多数情况下都会继承com.opensymphony.xwork2.ActionSupport类，并重载（Override）此类里的String execute()方法。具体的实现，如例1所示：

<% @ page contentType = " text/html; charset=UTF-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>
< html >
< head >
< title > Hello World! </ title >
</ head >
< body >
< h2 >< s:property value ="message" /></ h2 >
</ body >
</ html >

例1 HelloWorld.jsp

package tutorial;

import java.text.DateFormat;
import java.util.Date;

import com.opensymphony.xwork2.ActionSupport;

public class HelloWorld extends ActionSupport [image] {
 private String message;

 public String getMessage() [image] {
 return message;
}

@Override
public String execute() [image] {
message = " Hello World, Now is " + DateFormat.getInstance().format( new Date());
 return SUCCESS;
}
}

例1 classes/tutorial/HelloWorld.java

< package name ="ActionDemo" extends ="struts-default" >
< action name ="HelloWorld" class ="tutorial.HelloWorld" >
< result > /HelloWorld.jsp </ result >
</ action >
</ package >

例1 classes/struts.xml中 HelloWorld Action的配置

在浏览器地址栏中键入http://localhost:8080/Struts2_Action/HelloWorld.action，可以看到如图1所示页面。

图1 HelloWorld输出页面
图1 HelloWorld输出页面

参考JavaDoc，可知ActionSupport类实现了接口：

· com.opensymphony.xwork2.Action

· com.opensymphony.xwork2.LoaleProvider

· com.opensymphony.xwork2.TextProvider

· com.opensymphony.xwork2.Validateable

· com.opensymphony.xwork2.ValidationAware

· com.uwyn.rife.continuations.ContinuableObject

· java.io.Searializable

· java.lang.Cloneable

默认情况下，当请求HelloWorld.action发生时，Struts运行时（Runtime）根据struts.xml里的Action映射集(Mapping)，实例化tutoiral.HelloWorld类，并调用其execute方法。当然，我们可以通过以下两种方法改变这种默认调用。这个功能（Feature）有点类似Struts 1.x中的LookupDispathAction。

1. 在classes/sturts.xml中新建Action，并指明其调用的方法；

2. 访问Action时，在Action名后加上“!xxx”（xxx为方法名）。

实现方法请参考例2：

在classes/tutorial/HelloWorld.java中加入以下方法：

public String aliasAction() {
message ="自定义Action调用方法";
return SUCCESS;
}

例2 classes/tutorial/HelloWorld.java代码片段

实现方法一，在classes/sturts.xml中加入下面代码：

<action name="AliasHelloWorld" class="tutorial.HelloWorld" method="aliasAction">
<result>/HelloWorld.jsp</result>
</action>

例2 classes/struts.xml中 AlaisHelloWorld Action的配置

实现方法二，使用http://localhost:8080/Struts2_Action/HelloWorld!aliasAction.action地址来访问HelloWorld Action。

在浏览器地址栏中键入http://localhost:8080/Struts2_Action/AliasHelloWorld.action或 http://localhost:8080/Struts2_Action/HelloWorld!aliasAction.action，可以看到如图2所示页面。

图2 自定义Action调用方法页面
图2 自定义Action调用方法页面

通过上面的两个例子，细心的朋友应该可能会发现classes/tutorial/HelloWorld.java中Action方法（execute和aliasAction）返回都是SUCCESS。这个属性变量我并没有定义，所以大家应该会猜到它在ActionSupport或其父类中定义。没错，SUCCESS在接口com.opensymphony.xwork2.Action中定义，另外同时定义的还有ERROR, INPUT, LOGIN, NONE。

此外，我在配置Action时都没有为result定义名字（name），所以它们默认都为success。值得一提的是Struts 2.0中的result不仅仅是Struts 1.x中forward的别名，它可以实现除forward外的很激动人心的功能，如将Action输出到FreeMaker模板、Velocity模板、JasperReports和使用XSL转换等。这些都过result里的type（类型）属性（Attribute）定义的。另外，您还可以自定义result类型。

下面让我们来做一个Velocity模板输出的例子，首先在classes/struts.xml中新建一个Action映射（Mapping），将其result类型设为velocity，如以下代码所示：

<action name="VMHelloWorld" class="tutorial.HelloWorld">
<result type="velocity">/HelloWorld.vm</result>
</action>

例3 classes/struts.xml中 VMHelloWorld Action的配置

新建HelloWorld.vm，内容如下所示：

<html>
<head>
<title>Velocity</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>
<body>
<h2>Message rendered in Velocity: $message</h2>
</body>
</html>

例3 HelloWorld.vm

在浏览器地址栏中键入http://localhost:8080/Struts2_Action/VMHelloWorld.action，页面输出如下图3所示。

图3 HelloWorld.vm的输出页面

要运行例3需要在WEB-INF/lib中添加以下几个包：

· commons-collections-3.2.jar

· velocity-1.4.jar

· velocity-tools-view-1.2.jar

· avalon-logkit-2.1.jar

前面，我花了不少的时间讨论Action的输出。我老板有句名言——程序无非就是输入、操作和输出。因此，现在我们要讨论一下输入——表单输入。

使用Struts 2.0，表单数据的输入将变得非常方便，和普通的POJO一样在Action编写Getter和Setter，然后在JSP的UI标志的name与其对应，在提交表单到Action时，我们就可以取得其值。

让我们看一个例子，新建Login Action，它通过Login.jsp的表单获得用户名和密码，验查用户名是否为“max”，密码是否则为“secret”。如果，两者都符合，就在HelloWorld中显示“Welcome, max”，否则显示“Invalid user or Password”。

package tutorial;

import com.opensymphony.xwork2.ActionSupport;

publicclass Login extends ActionSupport [image] {
private String name;
private String password;
private String message;

public String getName() [image] {
return name;
}

publicvoid setName(String name) [image] {
this.name = name;
}

public String getPassword() [image] {
return password;
}

publicvoid setPassword(String password) [image] {
this.password = password;
}

public String getMessage() [image] {
return message;
}

@Override
public String execute() [image] {
if("max".equals(name) &&"Secret".equals(password)) [image] {
message ="Welcome, "+ name;
}else [image] {
message ="Invalid user or password";
}
return SUCCESS;
}
}

例4 classes/tutorial/Login.java

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags"%>
<html>
<head>
<title>Login</title>
</head>
<body>
<s:form action="Login" method="POST">
<s:textfield name="name" label="User name"/>
<s:password name="password" label="Password"/>
<s:submit value="Submit"/>
</s:form>
</body>
</html>

例4 Login.jsp

<action name="Login" class="tutorial.Login">
<result>/HelloWorld.jsp</result>
</action>

例4 classes/struts.xml中Login Action的配置

运行Tomcat，在浏览器地址栏中键入http://localhost:8080/Struts2_Action/Login.jsp，出现如图4所示页面。

图4 Login.jsp输出页面

分别在User name中输入“max”和“secret”，点击“Submit”按钮，出现如图5所示页面。

图5 Login成功页面
图5 Login成功页面

在浏览器地址栏中键入http://localhost:8080/Struts2_Action/Login.jsp，分别在User name中输入“Scott”和“password”，点击“Submit”按钮，出现如图6所示页面。

图6 Login失败页面
图6 Login失败页面

Struts 2.0更厉害的是支持更高级的POJO访问，如user.getPassword()。我们可以用另一写法实现例4。首先，将name和password从Login类中分离出来，到新建类User中。这样对我们开发多层系统尤其有用。它可以使系统结构更清晰。

package tutorial;

import com.opensymphony.xwork2.ActionSupport;

publicclass LoginX extends ActionSupport [image] {
private User user;
private String message;

publicvoid setUser(User user) [image] {
this.user = user;
}

public User getUser() [image] {
return user;
}

public String getMessage() [image] {
return message;
}

@Override
public String execute() [image] {
if("max".equals(user.getName()) &&"secret".equals(user.getPassword())) [image] {
message ="Welcome, "+ user.getName();
}else [image] {
message ="Invalid user or password";
}
return SUCCESS;
}
}

例5 classes/tutorial/LoginX.java

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags"%>
<html>
<head>
<title>Login</title>
</head>
<body>
<s:form action="LoginX" method="POST">
<s:textfield name="user.name" label="User name"/>
<s:password name="user.password" label="Password"/>
<s:submit value="Submit"/>
</s:form>
</body>
</html>

例5 LoginX.jsp

<action name="LoginX" class="tutorial.LoginX">
<result>/HelloWorld.jsp</result>
</action>

例5 classes/struts.xml中的 LoginX Action配置

很多时候我的同事会问我：“如果我要取得Servlet API中的一些对象，如request、response或session等，应该怎么做？这里的execute不像Struts 1.x的那样在参数中引入。”开发Web应用程序当然免不了跟这些对象打交道。在Strutx 2.0你可以有两种方式获得这些对象：非IoC（控制反转Inversion of Control）方式和IoC方式。

1. 非IoC方式

要获得上述对象，关键Struts 2.0中com.opensymphony.xwork2.ActionContext类。我们可以通过它的静态方法getContext()获取当前Action的上下文对象。 另外，org.apache.struts2.ServletActionContext作为辅助类（Helper Class），可以帮助您快捷地获得这几个对象。

o                              HttpServletRequest request = ServletActionContext.getRequest();

o                              HttpServletResponse response = ServletActionContext.getResponse();

o                              HttpSession session = request.getSession();

如果你只是想访问session的属性（Attribute），你也可以通过ActionContext.getContext().getSession()获取或添加session范围（Scoped）的对象。

2. IoC方式

要使用IoC方式，我们首先要告诉IoC容器（Container）想取得某个对象的意愿，通过实现相应的接口做到这点。具体实现，请参考例6 IocServlet.java。

package tutorial;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.struts2.ServletActionContext;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionSupport;

publicclass NonIoCServlet extends ActionSupport [image] {
private String message;

public String getMessage() [image] {
return message;
}

@Override
public String execute() [image] {
ActionContext.getContext().getSession().put("msg", "Hello World from Session!");

HttpServletRequest request = ServletActionContext.getRequest();
HttpServletResponse response = ServletActionContext.getResponse();
HttpSession session = request.getSession();

StringBuffer sb =new StringBuffer("Message from request: ");
sb.append(request.getParameter("msg"));
sb.append("<br>Response Buffer Size: ");
sb.append(response.getBufferSize());
sb.append("<br>Session ID: ");
sb.append(session.getId());

message = sb.toString();
return SUCCESS;
}
}

例6 classes/tutorial/NonIoCServlet.java

package tutorial;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.struts2.interceptor.ServletRequestAware;
import org.apache.struts2.interceptor.ServletResponseAware;
import org.apache.struts2.interceptor.SessionAware;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionSupport;

publicclass IoCServlet extends ActionSupport implements SessionAware, ServletRequestAware, ServletResponseAware [image] {
private String message;
private Map att;
private HttpServletRequest request;
private HttpServletResponse response;

public String getMessage() [image] {
return message;
}

publicvoid setSession(Map att) [image] {
this.att = att;
}

publicvoid setServletRequest(HttpServletRequest request) [image] {
this.request = request;
}

publicvoid setServletResponse(HttpServletResponse response) [image] {
this.response = response;
}

@Override
public String execute() [image] {
att.put("msg", "Hello World from Session!");

HttpSession session = request.getSession();

StringBuffer sb =new StringBuffer("Message from request: ");
sb.append(request.getParameter("msg"));
sb.append("<br>Response Buffer Size: ");
sb.append(response.getBufferSize());
sb.append("<br>Session ID: ");
sb.append(session.getId());

message = sb.toString();
return SUCCESS;
}
}

例6 classes/tutorial/IoCServlet.java

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags"%>
<html>
<head>
<title>Hello World!</title>
</head>
<body>
<h2>
<s:property value="message" escape="false"/>
<br>Message from session: <s:property value="#session.msg"/>
</h2>
</body>
</html>

例6 Servlet.jsp

<action name="NonIoCServlet" class="tutorial.NonIoCServlet">
<result>/Servlet.jsp</result>
</action>
<action name="IoCServlet" class="tutorial.IoCServlet">
<result>/Servlet.jsp</result>
</action>

例6 classes/struts.xml中 NonIocServlet 和 IoCServlet Action的配置

运行Tomcat，在浏览器地址栏中键入http://localhost:8080/Struts2_Action/NonIoCServlet.action?msg=Hello%20World! 或http://localhost:8080/Struts2_Action/IoCServlet.action?msg=Hello%20World!，出现如图7所示页面。

图7 Servlet.jsp的输出页面

在Servlet.jsp中，我用了两次property标志，第一次将escape设为false为了在JSP中输出<br>转行，第二次的value中的OGNL为“#session.msg”，它的作用与session.getAttribute("msg")等同。
关于property或其它标志，可以参考我的上一篇文章《常用的Struts 2.0的标志（Tag）介绍 》。


在Struts 2

在Struts 2.0中国际化(i18n)您的应用程序

国际化是商业系统中不可或缺的一部分，所以无论您学习的是什么Web框架，它都是必须掌握的技能。

其实，Struts 1.x在此部分已经做得相当不错了。它极大地简化了我们程序员在做国际化时所需的工作，例如，如果您要输出一条国际化的信息，只需在代码包中加入FILE-NAME_xx_XX.properties（其中FILE-NAME为默认资源文件的文件名），然后在struts-config.xml中指明其路径，再在页面用<bean:message>标志输出即可。

不过，所谓“没有最好，只有更好”。Struts 2.0并没有在这部分止步，而是在原有的简单易用的基础上，将其做得更灵活、更强大。

国际化Hello World

下面让我们看一个例子——HelloWorld。这个例子演示如何根据用户浏览器的设置输出相应的HelloWorld。

1. 在Eclipse创建工程配置开发和运行环境（如果对这个步骤有问题，可以参考我早前的文章《为Struts 2.0做好准备》）。

2. 在src文件夹中加入struts.properties文件，内容如下：

struts.custom.i18n.resources=globalMessages

Struts 2.0有两个配置文件，struts.xml和struts.properties都是放在WEB-INF/classes/下。

o                                            struts.xml用于应用程序相关的配置

o                                            struts.properties用于Struts 2.0的运行时（Runtime）的配置

3. 在src文件夹中加入globalMessages_en_US.properties文件，内容如下：

HelloWorld=Hello World!

4. 在src文件夹中加入globalMessages_zh_CN.properties文件，内容如下：

HelloWorld=你好，世界！

在此想和大家分享一个不错的编写properties文件的Eclipse插件（plugin），有了它我们在编辑一些简体中文、繁体中文等Unicode文本时，就不必再使用native2ascii编码了。您可以通过Eclipse中的软件升级（Software Update）安装此插件，步骤如下：

1、展开Eclipse的Help菜单，将鼠标移到Software Update子项，在出现的子菜单中点击Find and Install；
2、在Install/Update对话框中选择Search for new features to install，点击Next；
3、在Install对话框中点击New Remote Site；
4、在New Update Site对话框的Name填入“PropEdit”或其它任意非空字符串，在URL中填入http://propedit.sourceforge.jp/eclipse/updates/；
5、在Site to include to search列表中，除上一步加入的site外的其它选项去掉，点击Finsih；
6、在弹出的Updates对话框中的Select the features to install列表中将所有结尾为“3.1.x”的选项去掉（适用于Eclipse 3.2版本的朋友）；
7、点击Finish关闭对话框；
8、在下载后，同意安装，再按提示重启Eclipse，在工具条看到形似vi的按钮表示安装成功，插件可用。此时，Eclpise中所有properties文件的文件名前有绿色的P的图标作为标识。

5. 在WebContent文件夹下加入HelloWorl.jsp文件，内容如下：

<%@ page contentType="text/html; charset=UTF-8"%>
<%@taglib prefix="s" uri="/struts-tags"%>
<html>
<head>
<title>Hello World</title>
</head>
<body>
<h2><s:text name="HelloWorld"/></h2>
<h2><s:property value="%{getText('HelloWorld')}"/></h2>
</body>
</html>

6. 发布运行应用程序，在浏览器地址栏中输入http://localhost:8080/Struts2_i18n/HelloWorld.jsp ，出现图1所示页面。
图1 中文输出
图1 中文输出

7. 将浏览器的默认语言改为“英语（美国）”，刷新页面，出现图2所示页面。
图2 英文输出
图2 英文输出

上面的例子的做法，与Struts 1.x的做法相似，似乎并不能体现Struts 2.0的优势。不过，我在上面的例子用了两种方法来显示国际化字符串，其输出是相同的。其实，这就是Struts 2.0的一个优势，因为它默认支持EL，所示我们可以用getText方法来简洁地取得国际化字符串。另外更普遍的情况——在使用UI表单标志时，getText可以用来设置label属性，例如：

<s:textfield name="name" label="%{getText('UserName')}"/>

资源文件查找顺序

之所以说Struts 2.0的国际化更灵活是因为它可以能根据不同需要配置和获取资源（properties）文件。在Struts 2.0中有下面几种方法：

1. 使用全局的资源文件，方法如上例所示。这适用于遍布于整个应用程序的国际化字符串，它们在不同的包（package）中被引用，如一些比较共用的出错提示；

2. 使用包范围内的资源文件。做法是在包的根目录下新建名的package.properties和package_xx_XX.properties文件。这就适用于在包中不同类访问的资源；

3. 使用Action范围的资源文件。做法为Action的包下新建文件名（除文件扩展名外）与Action类名同样的资源文件。它只能在该Action中访问。如此一来，我们就可以在不同的Action里使用相同的properties名表示不同的值。例如，在ActonOne中title为“动作一”，而同样用title在ActionTwo表示“动作二”，节省一些命名工夫；

4. 使用<s:i18n>标志访问特定路径的properties文件。使用方法请参考我早前的文章《常用的Struts 2.0的标志（Tag）介绍》。在您使用这一方法时，请注意<s:i18n>标志的范围。在<s:i18n name="xxxxx">到</s:i18n>之间，所有的国际化字符串都会在名为xxxxx资源文件查找，如果找不到，Struts 2.0就会输出默认值（国际化字符串的名字）。

上面我列举了四种配置和访问资源的方法，它们的范围分别是从大到小，而Struts 2.0在查找国际化字符串所遵循的是特定的顺序，如图3所示：

图3 资源文件查找顺序图
图3 资源文件查找顺序图

假设我们在某个ChildAction中调用了getText("user.title")，Struts 2.0的将会执行以下的操作：

1. 查找ChildAction_xx_XX.properties文件或ChildAction.properties；

2. 查找ChildAction实现的接口，查找与接口同名的资源文件MyInterface.properties；

3. 查找ChildAction的父类ParentAction的properties文件，文件名为ParentAction.properties；

4. 判断当前ChildAction是否实现接口ModelDriven。如果是，调用getModel()获得对象，查找与其同名的资源文件；

5. 查找当前包下的package.properties文件；

6. 查找当前包的父包，直到最顶层包；

7. 在值栈（Value Stack）中，查找名为user的属性，转到user类型同名的资源文件，查找键为title的资源;

8. 查找在struts.properties配置的默认的资源文件，参考例1;

9. 输出user.title。

参数化国际化字符串

许多情况下，我们都需要在动行时（runtime）为国际化字符插入一些参数，例如在输入验证提示信息的时候。在Struts 2.0中，我们通过以下两种方法做到这点：

1. 在资源文件的国际化字符串中使用OGNL，格式为${表达式}，例如：

validation.require=${getText(fileName)} is required

2. 使用java.text.MessageFormat中的字符串格式，格式为{ 参数序号（从0开始）, 格式类形（number | date | time | choice）, 格式样式}，例如：

validation.between=Date must between {0, date, short} and {1, date, short}

在显示这些国际化字符时，同样有两种方法设置参数的值：

1. 使用标志的value0、value1...valueN的属性，如：

<s:text name="validation.required" value0="User Name"/>

2. 使用param子元素，这些param将按先后顺序，代入到国际化字符串的参数中，例如：

<s:text name="validation.required">
<s:param value="User Name"/>
</s:text>

让用户方便地选择语言

开发国际化的应用程序时，有一个功能是必不可少的——让用户快捷地选择或切换语言。在Struts 2.0中，通过ActionContext.getContext().setLocale(Locale arg)可以设置用户的默认语言。不过，由于这是一个比较普遍的应用场景（Scenario），所以Struts 2.0为您提供了一个名i18n的拦截器（Interceptor），并在默认情况下将其注册到拦截器链（Interceptor chain）中。它的原理为在执行Action方法前，i18n拦截器查找请求中的一个名为"request_locale"的参数。如果其存在，拦截器就将其作为参数实例化Locale对象，并将其设为用户默认的区域（Locale），最后，将此Locale对象保存在session的名为“WW_TRANS_I18N_LOCALE”的属性中。

下面，我将提供一完整示例演示它的使用方法。

package tutorial;

import java.util.Hashtable;
import java.util.Locale;
import java.util.Map;

publicclass Locales [image] {
public Map<String, Locale> getLocales() [image] {
Map<String, Locale> locales =new Hashtable<String, Locale>(2);
locales.put("American English", Locale.US);
locales.put("Simplified Chinese", Locale.CHINA);
return locales;
}
}

tutorial/Locales.java

<%@taglib prefix="s" uri="/struts-tags"%>
<script type="text/javascript">
<!--
function langSelecter_onChanged()
{
document.langForm.submit();
}
//-->
</script>
<s:set name="SESSION_LOCALE" value="#session['WW_TRANS_I18N_LOCALE']"/>
<s:bean id="locales" name="tutorial.Locales"/>
<form action="<s:url includeParams="get" encode="true"/>" name="langForm"
style="background-color: powderblue;
padding-top: 4px; padding-bottom: 4px;">
Language: <s:select label="Language"
list="#locales.locales" listKey="value"    listValue="key"
value="#SESSION_LOCALE == null ? locale
: #SESSION_LOCALE"
name="request_locale" id="langSelecter"
onchange="langSelecter_onChanged()" theme="simple"/>
</form>

LangSelector.jsp

上述代码的原理为，LangSelector.jsp先实例化一个Locales对象，并把对象的Map类型的属性locales赋予下拉列表（select） 。如此一来，下拉列表就获得可用语言的列表。大家看到LangSelector有<s:form>标志和一段Javascript脚本，它们的作用就是在用户在下拉列表中选择了后，提交包含“reqeust_locale”变量的表单到Action。在打开页面时，为了下拉列表的选中的当前区域，我们需要到session取得当前区域（键为“WW_TRANS_I18N_LOCALE”的属性），而该属性在没有设置语言前是为空的，所以通过值栈中locale属性来取得当前区域（用户浏览器所设置的语言）。

你可以把LangSelector.jsp作为一个控件使用，方法是在JSP页面中把它包含进来，代码如下所示：

<s:include value="/LangSelector.jsp"/>

在例1中的HellloWorld.jsp中<body>后加入上述代码，并在struts.xml中新建Action，代码如下：

<action name="HelloWorld">
<result>/HelloWorld.jsp</result>
</action>

或者，如果你多个JSP需要实现上述功能，你可以使用下面的通用配置，而不是为每一个JSP页面都新建一个Action。

<action name="*">
<result>/{1}.jsp</result>
</action>

分布运行程序，在浏览器的地址栏中输入http://localhost:8080/Struts2_i18n/HelloWorld.action，出现图4所示页面：

图3 HelloWorld.action

在下拉列表中，选择“American English”，出现图5所示页面：

图4 HelloWorld.action

可能大家会问为什么一定要通过Action来访问页面呢？
你可以试一下不用Action而直接用JSP的地址来访问页面，结果会是无论你在下拉列表中选择什么，语言都不会改变。这表示不能正常运行的。其原因为如果直接使用JSP访问页面，Struts 2.0在web.xml的配置的过滤器（Filter）就不会工作，所以拦截器链也不会工作。


转换器（Converter）——Struts 2

转换器（Converter）——Struts 2.0中的魔术师

在我已往的Struts 1.x项目经验中，有个问题不时的出现——在创建FormBean时，对于某个属性到底应该用String还是其它类型？

开发Web应用程序与开发传统桌面应用程序不同，Web应用程序实际上是分布个不同的主机（当然也可以同一个主机，不过比较少见）上的两个进程之间互交。这种互交建立在HTTP之上，它们互相传递是都是字符串。换句话说， 服务器可以的接收到的来自用户的数据只能是字符串或字符数组，而在服务器上的对象中，这些数据往往有多种不同的类型，如日期（Date），整数（int），浮点数（float）或自定义类型（UDT）等，如图1所示。因此，我们需要服务器端将字符串转换为适合的类型。

图1　UI与服务器对象关系
图1　UI与服务器对象关系

同样的问题也发生在使用UI展示服务器数据的情况。HTML的Form控件不同于桌面应用程序可以表示对象，其值只能为字符串类型，所以我们需要通过某种方式将特定对象转换成字符串。

要实现上述转换，Struts 2.0中有位魔术师可以帮到你——Converter。有了它，你不用一遍又一遍的重复编写诸如此类代码：

Date
birthday = DateFormat.getInstance(DateFormat.SHORT).parse(strDate);
<input type="text" value="<%=
DateFormat.getInstance(DateFormat.SHORT).format(birthday) %>" />

好了，现在让我们来看一个例子。

转换器——Hello World

在我的上一篇文章《在Struts 2.0中国际化(i18n)您的应用程序》的最后我举了一个可以让用户方便地切换语言的例子，下面例子与其相似，但实现方法不同。

首先，如《在Struts 2.0中国际化(i18n)您的应用程序》的第一个例子一样，创建和配置默认的资源文件；

接着，新建源代码文件夹下的tutorial包创建HelloWorld.java文件，代码如下：

package tutorial;

import java.util.Locale;

import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.util.LocalizedTextUtil;

public class HelloWorld extends ActionSupport [image] {
 private String msg;
 private Locale loc = Locale.US;

 public String getMsg() [image] {
 return msg;
}

public Locale getLoc() [image] {
 return loc;
}

public void setLoc(Locale loc) [image] {
 this .loc = loc;
}

@Override
 public String execute() [image] {
 // LocalizedTextUtil是Struts 2.0中国际化的工具类，<s:text>标志就是通过调用它实现国际化的
msg = LocalizedTextUtil.findDefaultText( " HelloWorld " , loc);
 return SUCCESS;
}
}

然后，在源代码文件夹下的struts.xml加入如下代码新建Action：

< package name ="ConverterDemo" extends ="struts-default" >
< action name ="HelloWorld" class ="tutorial.HelloWorld" >
< result > /HelloWorld.jsp </ result >
</ action >
</ package >

再在Web文件夹下，新建 HelloWorld.jsp，代码如下：

< %@ page contentType ="text/html; charset=UTF-8" % >
< %@taglib prefix ="s" uri ="/struts-tags" % >
< html >
< head >
< title > Hello World </ title >
</ head >
< body >
< s:form action ="HelloWorld" theme ="simple" >
Locale: < s:textfield name ="loc" /> &nbsp; < s:submit />
</ s:form >
< h2 >< s:property value ="msg" /></ h2 >
</ body >
</ html >

接下来，在源代码文件夹的tutorial包中新建LocaleConverter.java文件，代码如下：

package tutorial;

import java.util.Locale;
import java.util.Map;

public class LocaleConverter extends ognl.DefaultTypeConverter [image] {
@Override
 public Object convertValue(Map context, Object value, Class toType) [image] {
 if (toType == Locale. class ) [image] {
String locale = ((String[]) value)[ 0 ];
 return new Locale(locale.substring( 0 , 2 ), locale.substring( 3 ));
} else if (toType == String. class ) [image] {
Locale locale = (Locale) value;
 return locale.toString();
}
return null ;
}
}

再接下来，在源代码文件夹下新建xwork-conversion.properties，并在其中添加如下代码：

java.util.Locale = tutorial.LocaleConverter

发布运行应用程序，在浏览器中键入http://localhost:8080/Struts2_Converter/HelloWorld.action，输出页面如图2所示：
图2 HelloWorld英文输出
图2 HelloWorld英文输出

在Locale输入框中输入“zh_CN”，按“Submit”提交，出现如图3所示页面：
图3 HelloWorld中文输出
图3 HelloWorld中文输出

上述例子中，Locale文本输入框对应是Action中的类型为java.util.Locale的属性loc，所以需要创建一个自定义转变器实现两者间的转换。所有的Struts 2.0中的转换器都必须实现ognl.TypeConverter接口。 为了简单起见，OGNL包也为你提供了ognl.DefaultTypeConverter类去帮助您实现转换器。在例子中，LocaleConverter继承了ognl.DefaultTypeConverter，重载了其方法原型为“public Object convertValue(Map context, Object value, Class toType)”的方法。下面简单地介绍一下函数的参数：

1. context——用于获取当前的ActionContext

2. value——需要转换的值

3. toType——需要转换成的目标类型

实现转换器，我们需要通过配置告诉Struts 2.0。我们可以通过以下两种方法做到这点：

1. 配置全局的类型转换器，也即是上例的做法——在源代码文件夹下，新建一个名为“xwork-conversion.properties”的配置文件，并在文件中加入“待转换的类型的全名（包括包路径和类名）=转换器类的全名”对；

2. 应用于某个特定类的类型转换器，做法为在该类的包中添加一个格式为“类名-conversion.properties”的配置文件，并在文件中加入“待转换的属性的名字=转换器类的全名”对。上面的例子也可以这样配置——在源代码文件夹的tutorial包下新建名为“HelloWorld-conversion.properties”文件，并在其中加入“loc=tutorial.LocaleConverter”。

在继承DefaultTypeConverter时，如果是要将value转换成其它非字符串类型时，要记住value是String[]类型，而不是String类型。它是通过request.getParameterValues(String arg)来获得的，所以不要试图将其强行转换为String类型。

已有的转换器

对于一此经常用到的转换器，如日期、整数或浮点数等类型，Struts 2.0已经为您实现了。下面列出已经实现的转换器。

1. 预定义类型，例如int、boolean、double等；

2. 日期类型， 使用当前区域（Locale）的短格式转换，即DateFormat.getInstance(DateFormat.SHORT)；

3. 集合（Collection）类型， 将request.getParameterValues(String arg)返回的字符串数据与java.util.Collection转换；

4. 集合（Set）类型， 与List的转换相似，去掉相同的值；

5. 数组（Array）类型， 将字符串数组的每一个元素转换成特定的类型，并组成一个数组。

对于已有的转换器，大家不必再去重新发明轮子。Struts在遇到这些类型时，会自动去调用相应的转换器。

批量封装对象（Bean）

不知道大家是否遇过这种情况，在一个页面里同时提交几个对象。例如，在发布产品的页面，同时发布几个产品。我在之前一个项目就遇到过这种需求，当时用的是Struts 1.x。那是一个痛苦的经历，我在Google搜了很久都没有理想的结果。幸运的是，在Struts 2.0中这种痛苦将一去不复返。下面我就演示一下如何实现这个需求。

首先，在源代码文件夹下的tutorial包中新建Product.java文件，内容如下：

package tutorial;

import java.util.Date;

publicclass Product [image] {
private String name;
privatedouble price;
private Date dateOfProduction;

public Date getDateOfProduction() [image] {
return dateOfProduction;
}

publicvoid setDateOfProduction(Date dateOfProduction) [image] {
this.dateOfProduction = dateOfProduction;
}

public String getName() [image] {
return name;
}

publicvoid setName(String name) [image] {
this.name = name;
}

publicdouble getPrice() [image] {
return price;
}

publicvoid setPrice(double price) [image] {
this.price = price;
}
}

然后，在同上的包下添加ProductConfirm.java类，代码如下：

package tutorial;

import java.util.List;

import com.opensymphony.xwork2.ActionSupport;

publicclass ProductConfirm extends ActionSupport [image] {
public List<Product> products;

public List<Product> getProducts() [image] {
return products;
}

publicvoid setProducts(List<Product> products) [image] {
this.products = products;
}

@Override
public String execute() [image] {
for(Product p : products) [image] {
System.out.println(p.getName() + " | "+ p.getPrice() +" | " + p.getDateOfProduction());
}
return SUCCESS;
}
}

接看，在同上的包中加入ProductConfirm-conversion.properties，代码如下：

Element_products=tutorial.Product

再在struts.xml文件中配置ProductConfirm Action，代码片段如下：

<action name="ProductConfirm" class="tutorial.ProductConfirm">
<result>/ShowProducts.jsp</result>
</action>

在WEB文件夹下新建AddProducts.jsp，内容如下：

<%@ page contentType="text/html; charset=UTF-8"%>
<%@taglib prefix="s" uri="/struts-tags"%>
<html>
<head>
<title>Hello World</title>
</head>
<body>
<s:form action="ProductConfirm" theme="simple">
<table>
<tr style="background-color:powderblue; font-weight:bold;">
<td>Product Name</td>
<td>Price</td>
<td>Date of production</td>
</tr>
<s:iterator value="new int[3]" status="stat">
<tr>
<td><s:textfield name="%{'products['+#stat.index+'].name'}"/></td>
<td><s:textfield name="%{'products['+#stat.index+'].price'}"/></td>
<td><s:textfield name="%{'products['+#stat.index+'].dateOfProduction'}"/></td>
</tr>
</s:iterator>
<tr>
<td colspan="3"><s:submit /></td>
</tr>
</table>
</s:form>
</body>
</html>

在同样的文件夹下创建ShowProducts.jsp，内容如下：

<%@ page contentType="text/html; charset=UTF-8"%>
<%@taglib prefix="s" uri="/struts-tags"%>
<html>
<head>
<title>Hello World</title>
</head>
<body>
<table>
<tr style="background-color:powderblue; font-weight:bold;">
<td>Product Name</td>
<td>Price</td>
<td>Date of production</td>
</tr>
<s:iterator value="products" status="stat">
<tr>
<td><s:property value="name"/></td>
<td>$<s:property value="price"/></td>
<td><s:property value="dateOfProduction"/></td>
</tr>
</s:iterator>
</table>
</body>
</html>

发布运行应用程序，在浏览器中键入http://localhost:8080/Struts2_Converter/AddProducts.jsp，出现如图4所示页面：
图4 添加产品页面
图4 添加产品页面

按图4所示，填写表单，按“Submit”提交，出现图5所示页面：
图5 查看产品页面
图5 查看产品页面

查看服务器的控制台，有如下输出：

Expert One-on-One J2EE Development without EJB | 39.99 | Mon Jun 2100:00:00 CST 2004
Pro Spring | 32.99 | Mon Jan 3100:00:00 CST 2005
Core J2EE Patterns: Best Practices and Design Strategies, Second Edition | 34.64 | Sat May 1000:00:00 CST 2003

上面的代码并不复杂，但有几点需要说明：

1. ProductConfirm文件中的for(Product p : productes)的写法是J2SE 5.0中的新特性，作用遍历products列表；

2. List<Product>也是J2SE 5.0的才有的泛型（Generic）；

3. ProductConfirm-conversion.properties中“Element_products=tutorial.Product”是告诉Struts 2.0列表products的元素的类型为Product，而不是定义转换器；

4. 在AddProducts.jsp的<s:textfield>的name为“%{'products['+#stat.index+'].name'}”，%{exp}格式表示使用OGNL表达式，上述表达式的相当于<%= "products[" + stat.index + "].name" %>，至于<s:iterator>标志的用法可以参考我之前的文章《常用的Struts 2.0的标志（Tag）介绍》。

转换错误处理

不知道大家在运行上面的例子时，有没有填错日期或数字情况，又或者您有没有思考过这种情况？如果还没有尝试的朋友可以试一下，在第一行的Price和Date of production中输入英文字母，然后按“Submit”提交。你会看到页面为空白，再看一下服务器的控制台输出，有如下语句： 警告: No result defined for action tutorial.ProductConfirm and result input，它提示我们没有为Action定义输入结果，所以，我们应该在源代码文件夹下的struts.xml中的ProductConfirm Action中加入以下代码：

<result name="input">/AddProducts.jsp</result>

重新加载应用程序，刷新浏览器重新提交请求，这时页面返回AddProducts.jsp，格式错误的输入框的值被保留，如下图6所示：
图6 没有提示的错返回页面
图6 没有提示的错返回页面

当然，我们还可以在页面上加上错误提示信息，通过在AddProducts.jsp的“<body>”后，加入下面代码可以实现：

<div style="color:red">
<s:fielderror />
</div>

刷新浏览器，重新提交请求，出现如图7所示页面：
图7 带提示的错返回页面
图7 带提示的错返回页面

以上的功能的都是通过Struts 2.0里的一个名为conversionError的拦截器（interceptor）工作，它被注册到默认拦截器栈（default interceptor stack）中。Struts 2.0在转换出错后，会将错误放到ActionContext中，在conversionError的作用是将这些错误封装为对应的项错误（field error），因此我们可以通过<s:fielderror />来将其在页面上显示出来。另外，大家看第二和第三行的Price都被赋为0.0的值，而第一行则保留其错误值。这同样是conversionError的功劳——没有出错的行调用的products[index].price（默认值为0.0），而出错的行则会被赋为页面所提交的错误值，这样可以提供更好的用户体验。

总结

Struts 2.0的转换器简化的WEB应用程序的模型，为我们的编程带来极大的方便。


在Struts 2

在Struts 2.0中实现表单数据校验（Validation）

All Input Is Evil!
-Writing secure code

在写前几篇文章的时候，有些朋友建议我的写一篇关于表单数据校验的文章。 正如文章的开头所引用的《Writing Secure Code》的名言：“所有的输入都是罪恶的”，所以我们应该对所有的外部输入进行校验。而表单是应用程序最简单的入口，对其传进来的数据，我们必须进行校验。

转换与校验（Conversion & Validation）

其实上篇文章，我本来是打算写表单数据校验的内容，但是经过再三思考后，还是决定先写Struts 2.0转换器的内容。原因是我认为转换是校验的基础，只有在数据被正确地转换成其对应的类型后，我们才可以对其取值范围进行校验。看个例子相信大家可以更清楚。现在我们就来改造一下《转换器（Converter）——Struts 2.0中的魔术师》的第一个例子。

首先，从Action开始，修改后的代码如下：

package tutorial;

import java.util.Locale;

import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.util.LocalizedTextUtil;

public class HelloWorld extends ActionSupport [image] {
 private String msg;
 private Locale loc = Locale.US;

 public String getMsg() [image] {
 return msg;
}

public Locale getLoc() [image] {
 return loc;
}

public void setLoc(Locale loc) [image] {
 this .loc = loc;
}

@Override
 public void validate() [image] {
System.out.println( " Calling validate() [image] " );
 if ( ! (loc.equals(Locale.US) || loc.equals(Locale.CHINA))) [image] {
addFieldError( " loc " , getText( " validation.loc " ));
}
}

public void validateExecute() [image] {
System.out.println( " Calling validateExecute() by reflection [image] " );
}

@Override
 public String execute() [image] {
System.out.println( " Calling execute() [image] " );
 // LocalizedTextUtil是Struts 2.0中国际化的工具类，<s:text>标志就是通过调用它实现国际化的
msg = LocalizedTextUtil.findDefaultText( " HelloWorld " , loc);
 return SUCCESS;
}
}

然后，修改Struts.xml中Action的定义指明输入地址：

< action name ="HelloWorld" class ="tutorial.HelloWorld" >
< result > /HelloWorld.jsp </ result >
< result name ="input" > /HelloWorld.jsp </ result >
</ action >

接着，在HelloWorld.jsp中加入错误提示：

<% @ page contentType = " text/html; charset=UTF-8 " %>
<% @taglib prefix = " s " uri = " /struts-tags " %>
< html >
< head >
< title > Hello World </ title >
</ head >
< body >
< div style ="color:red;" >
< s:fielderror />
</ div >
< s:form action ="HelloWorld" theme ="simple" >
Locale: < s:textfield name ="loc" /> &nbsp; < s:submit />
</ s:form >
< h2 >< s:property value ="msg" /></ h2 >
</ body >
</ html >

再修改LocaleConverter.java文件，将内容改为：

package tutorial;

import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;

public class LocaleConverter extends ognl.DefaultTypeConverter [image] {
@Override
 public Object convertValue(Map context, Object value, Class toType) [image] {
 if (toType == Locale. class ) [image] {
System.out.println( " Converting String to Locale [image] " );
String locale = ((String[]) value)[ 0 ];
 return new Locale(locale.substring( 0 , 2 ), locale.substring( 3 ));
} else if (toType == String. class ) [image] {
System.out.println( " Converting Locale to String [image] " );
Locale locale = (Locale) value;
 return locale.toString();
}
return null ;
}
}

之后，修改国际化资源文件，内容为：

HelloWorld = 你好，世界！
invalid.fieldvalue.loc = Locale必须为\ " xx_XX\ " 的格式
validation.loc = 区域必须为中国或美国

globalMessages_zh_CN.properties

HelloWorld = Hello World!
invalid.fieldvalue.loc = Locale must like \ " xx_XX\ "
validation.loc = Locale must be China
or USA

globalMessages_en_US.properties

发布运行应用程序，在浏览器中键入http://localhost:8080/Struts2_Validation/HelloWorld.action，在Locale中输入zh_CN，按“Submit”提交，效果如上篇文章所示。而在服务器控制台有如下输出：

Converting String to Locale...
Calling validateExecute() by reflection...
Calling validate()...
Calling execute()...
Converting Locale to String...

上述的输出说明了Struts 2.0的数据校验工作方式，它需要经过下面几个步骤：

1. 通过转换器将请求参数转换成相应的Bean属性；

2. 判断转换过程是否出现异常。如果有，则将其保存到ActionContext中，conversionError拦截器再封装为fieldError；如果没有，进行下一步；

3. 通过反射（Reflection）来调用validateXxx()方法（其中，Xxx表示Action的方法名）；

4. 调用validate()方法；

5. 如果经过上述步骤没有出现fieldError，则调用Action方法；如果有，则会跳过Action方法，通过国际化将fieldError输出到页面。

不喜欢看文字的朋友，可以参考下面的图1。

图1 校验顺序图
图1 校验顺序图

看到这里可能大家会疑问：“这么多地方可以校验表单数据，到底我应该在那里做呢？”有选择是好事，但抉择的过程往往是痛苦的，往往让人不知所措。如果大家参照以下几点建议，相信会比较容易地做出正确的抉择。

1. 如果需要转换的数据，通常做法在转换的时候做格式的校验，在Action中的校验方法中校验取值。假如用户填错了格式，我们可以通过在资源文件配置invalid.fieldvalue.xxx（xxx为属性名）来提示用户正确的格式，不同的阶段出错显示不同的信息。具体做法请参考上面的例子；

2. 至于用validate()还是validateXxx()，我推荐使用validate()。原因是validateXxx()使用了反射，相对来说性能稍差，而validate()则是通过接口com.opensymphony.xwork2.Validateable调用。当然如果你的表单数据取值是取决于特定Action方法，则应该使用validateXxx()。

在运行上面的例子时，在Locale中输入zh并提交时出现图2所示页面。

图2 错误格式
图2 错误格式

在Locale中输入de_DE时，出现如图3所示页面。

图3 取值错误
图3 取值错误

使用Struts 2.0的校验框架

上一节的内容都是关于如何编程实现校验，这部分工作大都是单调的重复。更多情况下，我们使用Struts 2.0的校验框架，通过配置实现一些常见的校验。

我学习编程有个习惯——喜欢先看输出结果，再看代码实现。这样学的好处是先看结果可以刺激学习的激情，也可以在看代码前自已思考一下如何实现，然后带着问题去看代码，那就清晰多了。因此下面我们先来做演示。

首先，在tutorial包下新建ValidationAction.java，代码如下：

package tutorial;

import com.opensymphony.xwork2.ActionSupport;

public class ValidationAction extends ActionSupport [image] {
 private String reqiuredString;

 public String getReqiuredString() [image] {
 return reqiuredString;
}

public void setReqiuredString(String reqiuredString) [image] {
 this .reqiuredString = reqiuredString;
}

@Override
 public String execute() [image] {
 return SUCCESS;
}
}

然后，配置上述所建的Ation，代码片段如下：

< action name ="ValidationAction" class ="tutorial.ValidationAction" >
< result > /Output.jsp </ result >
< result name ="input" > /Input.jsp </ result >
</ action >

接着，创建Input.jsp和Output.jsp，内容分别如下：

<% @ page contentType = " text/html; charset=UTF-8 " %>
<% @taglib prefix = " s " uri = " /struts-tags " %>
< html >
< head >
< title > Hello World </ title >
<!-- 此标志的作用是引入Struts 2.0的常用的Javascript和CSS -->
< s:head />
</ head >
< body >
< s:form action ="ValidationAction" >
< s:textfield name ="reqiuredString" label ="Required String" />
< s:submit />
</ s:form >
</ body >
</ html >

Input.jsp

<% @ page contentType = " text/html; charset=UTF-8 " %>
<% @taglib prefix = " s " uri = " /struts-tags " %>
< html >
< head >
< title > Hello World </ title >
</ head >
< body >
Required String: < s:property value ="reqiuredString" />
</ body >
</ html >

Output.jsp

再接下来，在tutorial包下创建ValidationAction的校验配置文件Xxx-validation.xml（Xxx为Action的类名），在本例中该文件名ValidationAction-validation.xml，内容如下：

<? xml version="1.0" encoding="UTF-8" ?>
<! DOCTYPE validators PUBLIC
"-//OpenSymphony Group//XWork Validator 1.0//EN"
"http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd" >
< validators >
< field name ="reqiuredString" >
< field-validator type ="requiredstring" >
< message > This string is required </ message >
</ field-validator >
</ field >
</ validators >

发布运行应用程序，在地址栏中键入http://localhost:8080/Struts2_Validation/Input.jsp，出现如图4所示页面。

图4 Input.jsp

直接点击“Submit”提交表单，出现图5所示的页面。

图5 错误提示
图5 错误提示

在Required String中随便填点东西，转到Output.jsp页面，如图6所示。

图6 Output.jsp

通过上面的例子，大家可以看到使用该校验框架十分简单方便。不过，上例还有两点不足：

1. 还没有国际化错误消息；

2. 没有实现客户端的校验。

当然，要完善以上不足，对于Struts 2.0来说，只是小菜一碟。

1. 在Xxx-validation.xml文件中的<message>元素中加入key属性；

2. 在Input.jsp中的<s:form>标志中加入validate="true"属性，就可以在用Javascript在客户端校验数据。

下面是具体的实现，首先在国际化资源文件中加入错误消息，然后按照上面说明实现。因为要使用Javascript在客户端显示出错信息，所以在加载Input.jsp页面时，Struts 2.0需要获得国际化的字符串，故我们需要使用Action来访问Input.jsp页面，具体实现请参考《在Struts 2.0中国际化(i18n)您的应用程序》的最后部分。最后发布运行应用程序，直接在页面中点击“Submit”，表单没有被提交并出现错误提示，如图7所示：

图7 客户端校验
图7 客户端校验

校验框架是通过validation拦截器实现，该拦载被注册到默认的拦截器链中。它在conversionError拦截器之后，在validateXxx()之前被调用。这里又出现了一个选择的问题：到底是应该在action中通过validateXxx()或validate()实现校验，还是使用validation拦截器？绝大多数情况，我建议大家使用校验框架，只有当框架满足不了您的要求才自已编写代码实现。

配置文件查找顺序

在上面的例子中，我们通过创建ValidationAction-validation.xml来配置表单校验。Struts 2.0的校验框架自动会读取该文件，但这样就会引出一个问题——如果我的Action继承其它的Action类，而这两个Action类都需要对表单数据进行校验，那我是否会在子类的配置文件（Xxx-validation.xml）中复制父类的配置吗？

答案是不，因为Struts 2.0的校验框架跟《在Struts 2.0中国际化(i18n)您的应用程序》提到的“资源文件查找顺序”相似，有特定的配置文件查找顺序。不同的是校验框架按照自上而下的顺序在类层次查找配置文件。假设以下条件成立：

1. 接口 Animal;

2. 接口 Quadraped 扩展了 Animal;

3. 类 AnimalImpl 实现了 Animal;

4. 类 QuadrapedImpl 扩展了 AnimalImpl 实现了 Quadraped;

5. 类 Dog 扩展了 QuadrapedImpl;

如果Dog要被校验,框架方法会查找下面的配置文件（其中别名是Action在struts.xml中定义的别名）:

1. Animal-validation.xml

2. Animal-别名-validation.xml

3. AnimalImpl-validation.xml

4. AnimalImpl-别名-validation.xml

5. Quadraped-validation.xml

6. Quadraped-别名-validation.xml

7. QuadrapedImpl-validation.xml

8. QuadrapedImpl-别名-validation.xml

9. Dog-validation.xml

10. Dog-别名-validation.xml

已有的校验器

Struts 2.0已经为您实现很多常用的校验了，以下在jar的default.xml中的注册的校验器。

< validators >
< validator name ="required" class ="com.opensymphony.xwork2.validator.validators.RequiredFieldValidator" />
< validator name ="requiredstring" class ="com.opensymphony.xwork2.validator.validators.RequiredStringValidator" />
< validator name ="int" class ="com.opensymphony.xwork2.validator.validators.IntRangeFieldValidator" />
< validator name ="double" class ="com.opensymphony.xwork2.validator.validators.DoubleRangeFieldValidator" />
< validator name ="date" class ="com.opensymphony.xwork2.validator.validators.DateRangeFieldValidator" />
< validator name ="expression" class ="com.opensymphony.xwork2.validator.validators.ExpressionValidator" />
< validator name ="fieldexpression" class ="com.opensymphony.xwork2.validator.validators.FieldExpressionValidator" />
< validator name ="email" class ="com.opensymphony.xwork2.validator.validators.EmailValidator" />
< validator name ="url" class ="com.opensymphony.xwork2.validator.validators.URLValidator" />
< validator name ="visitor" class ="com.opensymphony.xwork2.validator.validators.VisitorFieldValidator" />
< validator name ="conversion" class ="com.opensymphony.xwork2.validator.validators.ConversionErrorFieldValidator" />
< validator name ="stringlength" class ="com.opensymphony.xwork2.validator.validators.StringLengthFieldValidator" />
< validator name ="regex" class ="com.opensymphony.xwork2.validator.validators.RegexFieldValidator" />
</ validators >

关于每个校验器的具体用法，请参考以下链接：
http://wiki.javascud.org/display/ww2cndoc/Validation
该链接中还有一些很有的信息，请大家仔细阅读。

总结

使用校验框架既可以方便地实现表单数据校验，又能够将校验与Action分离，故我们应该尽可能使用校验框架。在使用校验框架时，请不要忘记通过在资源文件加入invalid.fieldvalue.xxx字符串，显示适合的类型转换出错信息；或者使用conversion校验器。


Struts 2的基石——拦截器（Interceptor）

首先，要跟大家道个歉，前一阵子为给客户个一个DEMO，忙得不可开交，所以很久没有更新Blog。提到这个DEMO我想顺便跟大家分享一下心得——如果大家希望快速开发，一个类似Struts 2这样的简单方便的WEB框架必不可少。我们在开发DEMO使用的还是Struts 1.2.8，而且没有不使用任何EL（表达式语言），导致页面出现无数类似“<%= ((Integer) request.getAttribute("xx")).intValue()%6 %>”的代码。Struts 1.x的Form Bean的麻烦使得有部分同事直接使用request.getParameter(String arg)，继而引入另一种麻烦。诸如此类的问题，在DEMO这样时间紧迫的项目凸显了Struts 1.x对快速开发的无能为力。不过没办法，由于我们项目中的几个资深员工除了Struts 1.x外，对其它的WEB框架似乎不大感兴趣。

言归正传，Interceptor（以下译为拦截器）是Struts 2的一个强有力的工具，有许多功能（feature）都是构建于它之上，如国际化、转换器，校验等。

什么是拦截器

拦截器，在AOP（Aspect-Oriented Programming）中用于在某个方法或字段被访问之前，进行拦截然后在之前或之后加入某些操作。拦截是AOP的一种实现策略。

在Webwork的中文文档的解释为——拦截器是动态拦截Action调用的对象。它提供了一种机制可以使开发者可以定义在一个action执行的前后执行的代码，也可以在一个action执行前阻止其执行。同时也是提供了一种可以提取action中可重用的部分的方式。

谈到拦截器，还有一个词大家应该知道——拦截器链（Interceptor Chain，在Struts 2中称为拦截器栈Interceptor Stack）。拦截器链就是将拦截器按一定的顺序联结成一条链。在访问被拦截的方法或字段时，拦截器链中的拦截器就会按其之前定义的顺序被调用。

实现原理

Struts 2的拦截器实现相对简单。当请求到达Struts 2的ServletDispatcher时，Struts 2会查找配置文件，并根据其配置实例化相对的拦截器对象，然后串成一个列表（list），最后一个一个地调用列表中的拦截器，如图1所示。

图１
图１　拦截器调用序列图

已有的拦截器

Struts 2已经为您提供丰富多样的，功能齐全的拦截器实现。大家可以到struts2-all-2.0.1.jar或struts2-core-2.0.1.jar包的struts-default.xml查看关于默认的拦截器与拦截器链的配置。

在本文使用是Struts 2的最新发布版本2.0.1。需要下载的朋友请点击以下链接：
http://apache.justdn.org/struts/binaries/struts-2.0.1-all.zip

以下部分就是从struts-default.xml文件摘取的内容：

< 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 name ="conversionError" class ="org.apache.struts2.interceptor.StrutsConversionErrorInterceptor" />
< interceptor name ="createSession" class ="org.apache.struts2.interceptor.CreateSessionInterceptor" />
< interceptor name ="debugging" class ="org.apache.struts2.interceptor.debugging.DebuggingInterceptor" />
< interceptor name ="external-ref" class ="com.opensymphony.xwork2.interceptor.ExternalReferencesInterceptor" />
< interceptor name ="execAndWait" class ="org.apache.struts2.interceptor.ExecuteAndWaitInterceptor" />
< interceptor name ="exception" class ="com.opensymphony.xwork2.interceptor.ExceptionMappingInterceptor" />
< interceptor name ="fileUpload" class ="org.apache.struts2.interceptor.FileUploadInterceptor" />
< interceptor name ="i18n" class ="com.opensymphony.xwork2.interceptor.I18nInterceptor" />
< interceptor name ="logger" class ="com.opensymphony.xwork2.interceptor.LoggingInterceptor" />
< interceptor name ="model-driven" class ="com.opensymphony.xwork2.interceptor.ModelDrivenInterceptor" />
< interceptor name ="scoped-model-driven" class ="com.opensymphony.xwork2.interceptor.ScopedModelDrivenInterceptor" />
< interceptor name ="params" class ="com.opensymphony.xwork2.interceptor.ParametersInterceptor" />
< interceptor name ="prepare" class ="com.opensymphony.xwork2.interceptor.PrepareInterceptor" />
< interceptor name ="static-params" class ="com.opensymphony.xwork2.interceptor.StaticParametersInterceptor" />
< interceptor name ="scope" class ="org.apache.struts2.interceptor.ScopeInterceptor" />
< interceptor name ="servlet-config" class ="org.apache.struts2.interceptor.ServletConfigInterceptor" />
< interceptor name ="sessionAutowiring" class ="org.apache.struts2.spring.interceptor.SessionContextAutowiringInterceptor" />
< interceptor name ="timer" class ="com.opensymphony.xwork2.interceptor.TimerInterceptor" />
< interceptor name ="token" class ="org.apache.struts2.interceptor.TokenInterceptor" />
< interceptor name ="token-session" class ="org.apache.struts2.interceptor.TokenSessionStoreInterceptor" />
< interceptor name ="validation" class ="com.opensymphony.xwork2.validator.ValidationInterceptor" />
< interceptor name ="workflow" class ="com.opensymphony.xwork2.interceptor.DefaultWorkflowInterceptor" />
< interceptor name ="store" class ="org.apache.struts2.interceptor.MessageStoreInterceptor" />
< interceptor name ="checkbox" class ="org.apache.struts2.interceptor.CheckboxInterceptor" />
< interceptor name ="profiling" class ="org.apache.struts2.interceptor.ProfilingActivationInterceptor" />

配置和使用拦截器

在struts-default.xml中已经配置了以上的拦截器。如果您想要使用上述拦截器，只需要在应用程序struts.xml文件中通过“<include
file="struts-default.xml" />”将struts-default.xml文件包含进来，并继承其中的struts-default包（package），最后在定义Action时，使用“<interceptor-ref
name="xx" />”引用拦截器或拦截器栈（interceptor stack）。一旦您继承了struts-default包（package），所有Action都会调用拦截器栈 ——defaultStack。当然，在Action配置中加入“<interceptor-ref
name="xx" />”可以覆盖defaultStack。

下面是关于拦截器timer使用的例子。首先，新建Action类tuotrial/TimerInterceptorAction.java，内容如下：

package tutorial;

import com.opensymphony.xwork2.ActionSupport;

public class TimerInterceptorAction extends ActionSupport [image] {
@Override
 public String execute() [image] {
 try [image] {
 // 模拟耗时的操作
Thread.sleep( 500 );
} catch (Exception e) [image] {
e.printStackTrace();
}
return SUCCESS;
}
}

配置Action，名为Timer，配置文件如下：

<! DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd" >
< struts >
< include file ="struts-default.xml" />
< package name ="InterceptorDemo" extends ="struts-default" >
< action name ="Timer" class ="tutorial.TimerInterceptorAction" >
< interceptor-ref name ="timer" />
< result > /Timer.jsp </ result >
</ action >
</ package >
</ struts >

至于Timer.jsp可以随意写些什么到里面。发布运行应用程序，在浏览器的地址栏键入http://localhost:8080/Struts2_Interceptor/Timer.action，在出现Timer.jsp页面后，查看服务器的后台输出。

2006 - 12 - 6 14 : 27 : 32 com.opensymphony.xwork2.interceptor.TimerInterceptor doLog
信息: Executed action [ //Timer!execute ] took 2859 ms.

在您的环境中执行Timer!execute的耗时，可能上述的时间有些不同，这取决于您PC的性能。但是无论如何，2859 ms与500 ms还是相差太远了。这是什么原因呢？其实原因是第一次加载Timer时，需要进行一定的初始工作。当你重新请求Timer.action时，以上输出会变为：

2006 - 12 - 6 14 : 29 : 18 com.opensymphony.xwork2.interceptor.TimerInterceptor doLog
信息: Executed action [ //Timer!execute ] took 500 ms.

OK，这正是我们期待的结果。上述例子演示了拦截器timer的用途——用于显示执行某个action方法的耗时，在我们做一个粗略的性能调试时，这相当有用。

自定义拦截器

作为“框架（framework）”，可扩展性是不可或缺的，因为世上没有放之四海而皆准的东西。虽然，Struts 2为我们提供如此丰富的拦截器实现，但是这并不意味我们失去创建自定义拦截器的能力，恰恰相反，在Struts 2自定义拦截器是相当容易的一件事。

大家在开始着手创建自定义拦截器前，切记以下原则：
拦截器必须是无状态的，不要使用在API提供的ActionInvocation之外的任何东西。

要求拦截器是无状态的原因是Struts 2不能保证为每一个请求或者action创建一个实例，所以如果拦截器带有状态，会引发并发问题。

所有的Struts 2的拦截器都直接或间接实现接口com.opensymphony.xwork2.interceptor.Interceptor。除此之外，大家可能更喜欢继承类com.opensymphony.xwork2.interceptor.AbstractInterceptor。

以下例子演示通过继承AbstractInterceptor，实现授权拦截器。

首先，创建授权拦截器类tutorial.AuthorizationInterceptor，代码如下：

package tutorial;

import java.util.Map;

import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;

public class AuthorizationInterceptor extends AbstractInterceptor [image] {

@Override
 public String intercept(ActionInvocation ai) throws Exception [image] {
Map session = ai.getInvocationContext().getSession();
String role = (String) session.get( " ROLE " );
 if ( null != role) [image] {
Object o = ai.getAction();
 if (o instanceof RoleAware) [image] {
RoleAware action = (RoleAware) o;
action.setRole(role);
}
return ai.invoke();
} else [image] {
 return Action.LOGIN;
}
}

}

以上代码相当简单，我们通过检查session是否存在键为“ROLE”的字符串，判断用户是否登陆。如果用户已经登陆，将角色放到Action中，调用Action；否则，拦截直接返回Action.LOGIN字段。为了方便将角色放入Action，我定义了接口tutorial.RoleAware，代码如下：

package tutorial;

public interface RoleAware [image] {
 void setRole(String role);
}

接着，创建Action类tutorial.AuthorizatedAccess模拟访问受限资源，它作用就是通过实现RoleAware获取角色，并将其显示到ShowUser.jsp中，代码如下：

package tutorial;

import com.opensymphony.xwork2.ActionSupport;

public class AuthorizatedAccess extends ActionSupport implements RoleAware [image] {
 private String role;

 public void setRole(String role) [image] {
 this .role = role;
}

public String getRole() [image] {
 return role;
}

@Override
 public String execute() [image] {
 return SUCCESS;
}
}

以下是ShowUser.jsp的代码：

<% @ page contentType = " text/html; charset=UTF-8 " %>
<% @taglib prefix = " s " uri = " /struts-tags " %>
< html >
< head >
< title > Authorizated User </ title >
</ head >
< body >
< h1 > Your role is: < s:property value ="role" /></ h1 >
</ body >
</ html >

然后，创建tutorial.Roles初始化角色列表，代码如下：

package tutorial;

import java.util.Hashtable;
import java.util.Map;

public class Roles [image] {
 public Map < String, String > getRoles() [image] {
Map < String, String > roles = new Hashtable < String, String > ( 2 );
roles.put( " EMPLOYEE " , " Employee " );
roles.put( " MANAGER " , " Manager " );
 return roles;
}
}

接下来，新建Login.jsp实例化tutorial.Roles，并将其roles属性赋予<s:radio>标志，代码如下：

<% @ page contentType = " text/html; charset=UTF-8 " %>
<% @taglib prefix = " s " uri = " /struts-tags " %>
< html >
< head >
< title > Login </ title >
</ head >
< body >
< h1 > Login </ h1 >
Please select a role below:
< s:bean id ="roles" name ="tutorial.Roles" />
< s:form action ="Login" >
< s:radio list ="#roles.roles" value ="'EMPLOYEE'" name ="role" label ="Role" />
< s:submit />
</ s:form >
</ body >
</ html >

创建Action类tutorial.Login将role放到session中，并转到Action类tutorial.AuthorizatedAccess，代码如下：

package tutorial;

import java.util.Map;

import org.apache.struts2.interceptor.SessionAware;

import com.opensymphony.xwork2.ActionSupport;

public class Login extends ActionSupport implements SessionAware [image] {
 private String role;
private Map session;

 public String getRole() [image] {
 return role;
}

public void setRole(String role) [image] {
 this .role = role;
}

public void setSession(Map session) [image] {
 this .session = session;
}

@Override
 public String execute() [image] {
session.put( " ROLE " , role);
 return SUCCESS;
}
}

最后，配置struts.xml文件，内容如下：

<! DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd" >
< struts >
< include file ="struts-default.xml" />
< package name ="InterceptorDemo" extends ="struts-default" >
< interceptors >
< interceptor name ="auth" class ="tutorial.AuthorizationInterceptor" />
</ interceptors >
< action name ="Timer" class ="tutorial.TimerInterceptorAction" >
< interceptor-ref name ="timer" />
< result > /Timer.jsp </ result >
</ action >
< action name ="Login" class ="tutorial.Login" >
< result type ="chain" > AuthorizatedAccess </ result >
</ action >
< action name ="AuthorizatedAccess" class ="tutorial.AuthorizatedAccess" >
< interceptor-ref name ="auth" />
< result name ="login" > /Login.jsp </ result >
< result name ="success" > /ShowRole.jsp </ result >
</ action >
</ package >
</ struts >

发布运行应用程序，在浏览器地址栏中输入：http://localhost:8080/Struts2_Interceptor/AuthorizatedAccess.action。由于此时，session还没有键为“ROLE”的值，所以返回Login.jsp页面，如图2所示：

图２ Login.jsp

选中Employee，点击Submit，出现图3所示页面：

图3 ShowRole.jsp

总结

拦截器是Struts 2比较重要的一个功能。通过正确地使用拦截器，我们可以编写高可复用的代码


在Struts 2中实现IoC

IoC(Inversion of Control，以下译为控制反转）随着Java社区中轻量级容器（Lightweight Contianer）的推广而越来越为大家耳熟能详。在此，我不想再多费唇舌来解释“什么是控制反转”和“为什么需要控制反转”。因为互联网上已经有非常多的文章对诸如此类的问题作了精彩而准确的回答。大家可以去读一下Rod Johnson和Juergen Hoeller合著的《Expert one-on-one J2EE Development without EJB》或Martin Fowler所写的《Inversion of Control Containers and the Dependency Injection pattern》。

言归正传，本文的目的主要是介绍在Struts 2中实现控制反转。

历史背景

众所周知，Struts 2是以Webwork 2作为基础发展出来。而在Webwork 2.2之前的Webwork版本，其自身有一套控制反转的实现，Webwork 2.2在Spring 框架的如火如荼发展的背景下，决定放弃控制反转功能的开发，转由Spring实现。值得一提的是，Spring确实是一个值得学习的框架，因为有越来越多的开源组件（如iBATIS等）都放弃与Spring重叠的功能的开发。因此，Struts 2推荐大家通过Spring实现控制反转。

具体实现

首先，在开发环境中配置好Struts 2的工程。对这部分仍然有问题的朋友，请参考我的早前的文章。

然后，将所需的Spring的jar包加入到工程的构建环境（Build Path）中，如下图1所示：

图1 所依赖的Spring的jar包
图1 所依赖的Spring的jar包

本文使用的是Spring 2.0，Spring强烈建议大家在使用其jar包时，只引用需要的包，原因是Spring是一个功能非常强大的框架，其中有些功能是您不需要的；而且Spring提倡的是“按需所取”，而不是EJB的“爱我就要爱我的一切”。当然，如果你怕麻烦或者是不清楚每个包的作用，引用一个Spring的总包也未尝不可。

接下来，就要修改WEB-INF\web.xml文件了，内容为：

<? xml version="1.0" encoding="UTF-8" ?>
< web-app version ="2.4" xmlns ="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation ="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" >

< display-name > Struts 2 IoC Demo </ display-name >

< filter >
< filter-name > struts-cleanup </ filter-name >
< filter-class >
org.apache.struts2.dispatcher.ActionContextCleanUp
</ filter-class >
</ filter >

< filter >
< filter-name > struts2 </ filter-name >
< filter-class >
org.apache.struts2.dispatcher.FilterDispatcher
</ filter-class >
</ filter >

< filter-mapping >
< filter-name > struts-cleanup </ filter-name >
< url-pattern > /* </ url-pattern >
</ filter-mapping >

< filter-mapping >
< filter-name > struts2 </ filter-name >
< url-pattern > /* </ url-pattern >
</ filter-mapping >

< listener >
< listener-class >
org.springframework.web.context.ContextLoaderListener
</ listener-class >
</ listener >

< welcome-file-list >
< welcome-file > index.html </ welcome-file >
</ welcome-file-list >
</ web-app >

清单1 WEB-INF\web.xml

大家一看便知道，主要是加入Spring的ContextLoaderListener监听器，方便Spring与Web容器交互。

紧接着，修改Struts.properties文件，告知Struts 2运行时使用Spring来创建对象（如Action等），内容如下：

struts.objectFactory = spring

清单2 classes\struts.properties

再下来，遵循Spring的原则——面向接口编程，创建接口ChatService，代码如下：

package tutorial;

import java.util.Set;

public interface ChatService [image] {
Set < String > getUserNames();
}

清单3 tutorial.ChatService.java

然后，再创建一个默认实现ChatServiceImpl，代码如下：

package tutorial;

import java.util.HashSet;
import java.util.Set;

public class ChatServiceImpl implements ChatService [image] {

 public Set < String > getUserNames() [image] {
Set < String > users = new HashSet < String > ();
users.add( " Max " );
users.add( " Scott " );
users.add( " Bob " );
 return users;
}

}

清单4 tutorial.ChatServiceImpl.java

接下来，就该新建Action了。tutorial.ChatAction.java的代码如下：

package tutorial;

import java.util.Set;

import com.opensymphony.xwork2.ActionSupport;

public class ChatAction extends ActionSupport [image] {
 private static final long serialVersionUID = 8445871212065L ;

private ChatService chatService;
 private Set < String > userNames;

 public void setChatService(ChatService chatService) [image] {
 this .chatService = chatService;
}

public Set < String > getUserNames() [image] {
 return userNames;
}

@Override
 public String execute() [image] {
userNames = chatService.getUserNames();
 return SUCCESS;
}

}

清单5 tutorial.ChatAction.java

ChatAction类使用属性（Getter/Setter）注入法取得ChatService对象。

然后，配置Spring的applicationContext.xml（位于WEB-INF下）文件，内容如下：

<? xml version="1.0" encoding="UTF-8" ?>
< beans xmlns ="http://www.springframework.org/schema/beans"
xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd" >
< bean id ="chatService" class ="tutorial.ChatServiceImpl" />
< bean id ="chatAction" class ="tutorial.ChatAction" scope ="prototype" >
< property name ="chatService" >
< ref local ="chatService" />
</ property >
</ bean >
</ beans >

清单6 WEB-INF\applicationContext.xml

上述代码有二点值得大家注意的：

1. Struts 2会为每一个请求创建一个Action对象，所以在定义chatAction时，使用scope="prototype"。这样Spring就会每次都返回一个新的ChatAction对象了；

2. 因为ChatServiceImpl被配置为默认的scope（也即是singleton，唯一的），所以在实现时应保证其线程安全（关于编写线程安全的代码的讨论已经超出本文的范围，更超出了本人的能力范围，大家可以参考Addison Wesley Professional出版的《Java Concurrency in Practice》）。

接下来，在classes/struts.xml中配置Action，内容如下：

<! DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd" >
< struts >
< include file ="struts-default.xml" />

< package name ="Struts2_IoC" extends ="struts-default" >
< action name ="Chat" class ="chatAction" >
< result > /UserList.jsp </ result >
</ action >
</ package >
</ struts >

清单7 classes\struts.xml

这里的Action和平常不同的就是class属性，它对应于Spring所定义的bean的id，而不是它的类全名。

最后，让我们看看/UserList.jsp，内容如下：

<% @ page contentType = " text/html; charset=UTF-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>
< html >
< head >
< title > User List </ title >
</ head >

< body >
< h2 > User List </ h2 >
< ol >
< s:iterator value ="userNames" >
< li >< s:property /></ li >
</ s:iterator >
</ ol >
</ body >
</ html >

清单8 /UserList.jsp

大功告成，分布运行应用程序，在浏览器中键入http://localhost:8080/Struts2_IoC/Chat.action，出现如图2所示页面：

图2 /ListUser.jsp

总结

通过Spring在Struts 2上实现控制反转是强烈推荐的做法，当然您也可以组合其它的实现（如Pico等）。


在Struts 2中实现文件上传

前一阵子有些朋友在电子邮件中问关于Struts 2实现文件上传的问题， 所以今天我们就来讨论一下这个问题。

实现原理

Struts 2是通过Commons FileUpload文件上传。Commons FileUpload通过将HTTP的数据保存到临时文件夹，然后Struts使用fileUpload拦截器将文件绑定到Action的实例中。从而我们就能够以本地文件方式的操作浏览器上传的文件。

具体实现

前段时间Apache发布了Struts 2.0.6 GA，所以本文的实现是以该版本的Struts作为框架的。以下是例子所依赖类包的列表：

依赖类包的列表
清单1 依赖类包的列表

首先，创建文件上传页面FileUpload.jsp，内容如下：

<% @ page language = " java " contentType = " text/html; charset=utf-8 " pageEncoding = " utf-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>

<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
< head >
< title > Struts 2 File Upload </ title >
</ head >
< body >
< s:form action ="fileUpload" method ="POST" enctype ="multipart/form-data" >
< s:file name ="myFile" label ="Image File" />
< s:textfield name ="caption" label ="Caption" />
< s:submit />
</ s:form >
</ body >
</ html >

清单2 FileUpload.jsp

在FileUpload.jsp中，先将表单的提交方式设为POST，然后将enctype设为multipart/form-data，这并没有什么特别之处。接下来，<s:file/>标志将文件上传控件绑定到Action的myFile属性。

其次是FileUploadAction.java代码：

package tutorial;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;

import org.apache.struts2.ServletActionContext;

import com.opensymphony.xwork2.ActionSupport;

public class FileUploadAction extends ActionSupport [image] {
 private static final long serialVersionUID = 572146812454l ;
 private static final int BUFFER_SIZE = 16 * 1024 ;

 private File myFile;
 private String contentType;
 private String fileName;
 private String imageFileName;
 private String caption;

 public void setMyFileContentType(String contentType) [image] {
 this .contentType = contentType;
}

public void setMyFileFileName(String fileName) [image] {
 this .fileName = fileName;
}

public void setMyFile(File myFile) [image] {
 this .myFile = myFile;
}

public String getImageFileName() [image] {
 return imageFileName;
}

public String getCaption() [image] {
 return caption;
}

public void setCaption(String caption) [image] {
 this .caption = caption;
}

private static void copy(File src, File dst) [image] {
 try [image] {
InputStream in = null ;
OutputStream out = null ;
 try [image] {
in = new BufferedInputStream( new FileInputStream(src), BUFFER_SIZE);
out = new BufferedOutputStream( new FileOutputStream(dst), BUFFER_SIZE);
 byte [] buffer = new byte [BUFFER_SIZE];
 while (in.read(buffer) > 0 ) [image] {
out.write(buffer);
}
} finally [image] {
 if ( null != in) [image] {
in.close();
}
if ( null != out) [image] {
out.close();
}
}
} catch (Exception e) [image] {
e.printStackTrace();
}
}

private static String getExtention(String fileName) [image] {
 int pos = fileName.lastIndexOf( " . " );
 return fileName.substring(pos);
}

@Override
 public String execute() [image] {
imageFileName = new Date().getTime() + getExtention(fileName);
File imageFile = new File(ServletActionContext.getServletContext().getRealPath( " /UploadImages " ) + " / " + imageFileName);
copy(myFile, imageFile);
 return SUCCESS;
}

}

清单3 tutorial/FileUploadAction.java

在FileUploadAction中我分别写了setMyFileContentType、setMyFileFileName、setMyFile和setCaption四个Setter方法，后两者很容易明白，分别对应FileUpload.jsp中的<s:file/>和<s:textfield/>标志。但是前两者并没有显式地与任何的页面标志绑定，那么它们的值又是从何而来的呢？其实，<s:file/>标志不仅仅是绑定到myFile，还有myFileContentType（上传文件的MIME类型）和myFileFileName（上传文件的文件名，该文件名不包括文件的路径）。因此，<s:file name="xxx" />对应Action类里面的xxx、xxxContentType和xxxFileName三个属性。

FileUploadAction作用是将浏览器上传的文件拷贝到WEB应用程序的UploadImages文件夹下，新文件的名称是由系统时间与上传文件的后缀组成，该名称将被赋给imageFileName属性，以便上传成功的跳转页面使用。

下面我们就来看看上传成功的页面：

<% @ page language = " java " contentType = " text/html; charset=utf-8 " pageEncoding = " utf-8 " %>
<% @ taglib prefix = " s " uri = " /struts-tags " %>

<! DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" >
< html xmlns ="http://www.w3.org/1999/xhtml" >
< head >
< title > Struts 2 File Upload </ title >
</ head >
< body >
< div style ="padding: 3px; border: solid 1px #cccccc; text-align: center" >
< img src ='UploadImages/<s:property value ="imageFileName" /> ' />
< br />
< s:property value ="caption" />
</ div >
</ body >
</ html >

清单4 ShowUpload.jsp

ShowUpload.jsp获得imageFileName，将其UploadImages组成URL，从而将上传的图像显示出来。

然后是Action的配置文件：

<? 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 >
< package name ="fileUploadDemo" extends ="struts-default" >
< action name ="fileUpload" class ="tutorial.FileUploadAction" >
< interceptor-ref name ="fileUploadStack" />
< result name ="success" > /ShowUpload.jsp </ result >
</ action >
</ package >
</ struts >

清单5 struts.xml

fileUpload Action显式地应用fileUploadStack的拦截器。

最后是web.xml配置文件：

<? xml version="1.0" encoding="UTF-8" ?>
< web-app id ="WebApp_9" version ="2.4"
xmlns ="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation ="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" >

< display-name > Struts 2 Fileupload </ display-name >

< filter >
< filter-name > struts-cleanup </ filter-name >
< filter-class >
org.apache.struts2.dispatcher.ActionContextCleanUp
</ filter-class >
</ filter >

< filter >
< filter-name > struts2 </ filter-name >
< filter-class >
org.apache.struts2.dispatcher.FilterDispatcher
</ filter-class >
</ filter >

< filter-mapping >
< filter-name > struts-cleanup </ filter-name >
< url-pattern > /* </ url-pattern >
</ filter-mapping >

< filter-mapping >
< filter-name > struts2 </ filter-name >
< url-pattern > /* </ url-pattern >
</ filter-mapping >

< welcome-file-list >
< welcome-file > index.html </ welcome-file >
</ welcome-file-list >

</ web-app >

清单6 WEB-INF/web.xml

发布运行应用程序，在浏览器地址栏中键入：http://localhost:8080/Struts2_Fileupload/FileUpload.jsp，出现图示页面：

清单7 FileUpload页面

选择图片文件，填写Caption并按下Submit按钮提交，出现图示页面：

清单8 上传成功页面

更多配置

在运行上述例子，如果您留心一点的话，应该会发现服务器控制台有如下输出：

Mar 20 , 2007 4 : 08 : 43 PM org.apache.struts2.dispatcher.Dispatcher getSaveDir
INFO: Unable to find 'struts.multipart.saveDir' property setting. Defaulting to javax.servlet.context.tempdir
Mar 20 , 2007 4 : 08 : 43 PM org.apache.struts2.interceptor.FileUploadInterceptor intercept
INFO: Removing file myFile C:\Program Files\Tomcat 5.5 \work\Catalina\localhost\Struts2_Fileupload\upload_251447c2_1116e355841__7ff7_00000006.tmp

清单9 服务器控制台输出

上述信息告诉我们，struts.multipart.saveDir没有配置。struts.multipart.saveDir用于指定存放临时文件的文件夹，该配置写在struts.properties文件中。例如，如果在struts.properties文件加入如下代码：

struts.multipart.saveDir = /tmp

清单10 struts配置

这样上传的文件就会临时保存到你根目录下的tmp文件夹中（一般为c:\tmp），如果此文件夹不存在，Struts 2会自动创建一个。

错误处理

上述例子实现的图片上传的功能，所以应该阻止用户上传非图片类型的文件。在Struts 2中如何实现这点呢？其实这也很简单，对上述例子作如下修改即可。

首先修改FileUpload.jsp，在<body>与<s:form>之间加入“<s:fielderror />”，用于在页面上输出错误信息。

然后修改struts.xml文件，将Action fileUpload的定义改为如下所示：

< action name ="fileUpload" class ="tutorial.FileUploadAction" >
< interceptor-ref name ="fileUpload" >
< param name ="allowedTypes" >
image/bmp,image/png,image/gif,image/jpeg
</ param >
</ interceptor-ref >
< interceptor-ref name ="defaultStack" />
< result name ="input" > /FileUpload.jsp </ result >
< result name ="success" > /ShowUpload.jsp </ result >
</ action >

清单11 修改后的配置文件

显而易见，起作用就是fileUpload拦截器的allowTypes参数。另外，配置还引入defaultStack它会帮我们添加验证等功能，所以在出错之后会跳转到名称为“input”的结果，也即是FileUpload.jsp。

发布运行应用程序，出错时，页面如下图所示：

清单12 出错提示页面

上面的出错提示是Struts 2默认的，大多数情况下，我们都需要自定义和国际化这些信息。通过在全局的国际资源文件中加入“struts.messages.error.content.type.not.allowed=The file you uploaded is not a image”，可以实现以上提及的需求。对此有疑问的朋友可以参考我之前的文章《在Struts 2.0中国际化(i18n)您的应用程序》。

实现之后的出错页面如下图所示：

清单13 自定义出错提示页面

同样的做法，你可以使用参数“maximumSize”来限制上传文件的大小，它对应的字符资源名为：“struts.messages.error.file.too.large”。

字符资源“struts.messages.error.uploading”用提示一般的上传出错信息。

多文件上传

与单文件上传相似，Struts 2实现多文件上传也很简单。你可以将多个<s:file />绑定Action的数组或列表。如下例所示。

< s:form action ="doMultipleUploadUsingList" method ="POST" enctype ="multipart/form-data" >
< s:file label ="File (1)" name ="upload" />
< s:file label ="File (2)" name ="upload" />
< s:file label ="FIle (3)" name ="upload" />
< s:submit />
</ s:form >

清单14 多文件上传JSP代码片段

如果你希望绑定到数组，Action的代码应类似：

private File[] uploads;
 private String[] uploadFileNames;
 private String[] uploadContentTypes;

 public File[] getUpload() [image] { return this .uploads; }
public void setUpload(File[] upload) [image] { this .uploads = upload; }

public String[] getUploadFileName() [image] { return this .uploadFileNames; }
public void setUploadFileName(String[] uploadFileName) [image] { this .uploadFileNames = uploadFileName; }

public String[] getUploadContentType() [image] { return this .uploadContentTypes; }
public void setUploadContentType(String[] uploadContentType) [image] { this .uploadContentTypes = uploadContentType; }

清单15 多文件上传数组绑定Action代码片段

如果你想绑定到列表，则应类似：

private List < File > uploads = new ArrayList < File > ();
 private List < String > uploadFileNames = new ArrayList < String > ();
 private List < String > uploadContentTypes = new ArrayList < String > ();

 public List < File > getUpload() [image] {
 return this .uploads;
}
public void setUpload(List < File > uploads) [image] {
 this .uploads = uploads;
}

public List < String > getUploadFileName() [image] {
 return this .uploadFileNames;
}
public void setUploadFileName(List < String > uploadFileNames) [image] {
 this .uploadFileNames = uploadFileNames;
}

public List < String > getUploadContentType() [image] {
 return this .uploadContentTypes;
}
public void setUploadContentType(List < String > contentTypes) [image] {
 this .uploadContentTypes = contentTypes;
}

清单16 多文件上传列表绑定Action代码片段

总结

在Struts 2中实现文件上传的确是轻而易举，您要做的只是使用<s:file />与Action的属性绑定。这又一次有力地证明了Struts 2的简单易用。


在Struts 2中实现CRUD

CRUD是Create（创建）、Read（读取）、Update（更新）和Delete（删除）的缩写，它是普通应用程序的缩影。如果您掌握了某框架的CRUD编写，那么意味可以使用该框架创建普通应用程序了，所以大家使用新框架开发OLTP（Online Transaction Processing）应用程序时，首先会研究一下如何编写CRUD。这类似于大家在学习新编程语言时喜欢编写“Hello World”。

本文旨在讲述Struts 2上的CRUD开发，所以为了例子的简单易懂，我不会花时间在数据库的操作上。取而代之的是一个模拟数据库的哈希表（Hash Map）。

具体实现

首先，让我们看看的“冒牌”的DAO（Data Access Object，数据访问对象），代码如下：

package tutorial.dao;

import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import tutorial.model.Book;

public class BookDao [image] {
private static final BookDao instance;
private static final ConcurrentMap<String, Book> data;

static [image] {
instance = new BookDao();
data = new ConcurrentHashMap<String, Book>();
data.put("978-0735619678", new Book("978-0735619678", "Code Complete, Second Edition", 32.99));
data.put("978-0596007867", new Book("978-0596007867", "The Art of Project Management", 35.96));
data.put("978-0201633610", new Book("978-0201633610", "Design Patterns: Elements of Reusable Object-Oriented Software", 43.19));
data.put("978-0596527341", new Book("978-0596527341", "Information Architecture for the World Wide Web: Designing Large-Scale Web Sites", 25.19));
data.put("978-0735605350", new Book("978-0735605350", "Software Estimation: Demystifying the Black Art", 25.19));
}

private BookDao() [image] {}

public static BookDao getInstance() [image] {
return instance;
}

public Collection<Book> getBooks() [image] {
return data.values();
}

public Book getBook(String isbn) [image] {
return data.get(isbn);
}

public void storeBook(Book book) [image] {
data.put(book.getIsbn(), book);
}

public void removeBook(String isbn) [image] {
data.remove(isbn);
}

public void removeBooks(String[] isbns) [image] {
for(String isbn : isbns) [image] {
data.remove(isbn);
}
}
}

清单1 src/tutorial/dao/BookDao.java

以上代码相信不用解释大家也清楚，我使用ConcurrentMap数据结构存储Book对象，这主要是为了方便检索和保存Book对象；另外，我还将data变量设为静态唯一来模拟应用程序的数据库。

接下来是的数据模型Book类，代码如下：

package tutorial.model;

public class Book [image] {
private String isbn;
private String title;
private double price;

public Book() [image] {
}

public Book(String isbn, String title, double price) [image] {
this.isbn = isbn;
this.title = title;
this.price = price;
}

public String getIsbn() [image] {
return isbn;
}

public void setIsbn(String isbn) [image] {
this.isbn = isbn;
}

public double getPrice() [image] {
return price;
}

public void setPrice(double price) [image] {
this.price = price;
}

public String getTitle() [image] {
return title;
}

public void setTitle(String title) [image] {
this.title = title;
}
}

清单2 src/tutorial/model/Book.java

Book类有三个属性isbn,、title和price分别代表书籍的编号、名称和价格，其中编号用于唯一标识书籍（相当数据库中的主键）。

然后，我们再来看看Action类的代码：

package tutorial.action;

import java.util.Collection;

import tutorial.dao.BookDao;
import tutorial.model.Book;

import com.opensymphony.xwork2.ActionSupport;

public class BookAction extends ActionSupport [image] {
private static final long serialVersionUID = 872316812305356L;

private String isbn;
private String[] isbns;
private Book book;
private Collection<Book> books;
private BookDao dao = BookDao.getInstance();

public Book getBook() [image] {
return book;
}

public void setBook(Book book) [image] {
this.book = book;
}

public String getIsbn() [image] {
return isbn;
}

public void setIsbn(String isbn) [image] {
this.isbn = isbn;
}

public String[] getIsbns() [image] {
return isbns;
}

public void setIsbns(String[] isbns) [image] {
this.isbns = isbns;
}

public Collection<Book> getBooks() [image] {
return books;
}

public void setBooks(Collection<Book> books) [image] {
this.books = books;
}

public String load() [image] {
book = dao.getBook(isbn);
return SUCCESS;
}

public String list() [image] {
books = dao.getBooks();
return SUCCESS;
}

public String store() [image] {
dao.storeBook(book);
return SUCCESS;
}

public String remove() [image] {
if(null != isbn) [image] {
dao.removeBook(isbn);
} else [image] {
dao.removeBooks(isbns);
}
return SUCCESS;
}
}

清单3 src/tutorial/action/BookAction.java

BookAction类中属性isbn用于表示待编辑或删除的书籍的编号，属性isbns用于表示多个待删除的书籍的编号数组，属性book表示当前书籍，属性books则表示当前的书籍列表。BookAction有四个Action方法分别是load、list、store和remove，也即是CRUD都集中在BookAction中实现。

再下来是Action的配置代码：

<?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>
<package name="Struts2_CRUD_DEMO" extends="struts-default" namespace="/Book">
<action name="List" class="tutorial.action.BookAction" method="list">
<result>List.jsp</result>
</action>
<action name="Edit" class="tutorial.action.BookAction" method="load">
<result>Edit.jsp</result>
</action>
<action name="Store" class="tutorial.action.BookAction" method="store">
<result type="redirect">List.action</result>
</action>
<action name="Remove" class="tutorial.action.BookAction" method="remove">
<result type="redirect">List.action</result>
</action>
</package>
</struts>

清单4 src/struts.xml

以上的配置中，我使用了四个Action定义。它们都在“/Book”名值空间内。这样我就可以分别通过“http://localhost:8080/Struts2_CRUD/Book/List.action”、“http://localhost:8080/Struts2_CRUD/Book/Edit.action”、“http://localhost:8080/Struts2_CRUD/Book/Store.action”和“http://localhost:8080/Struts2_CRUD/Book/Remove.action”来调用BookAction的四个Action方法进行CRUD操作。当然，这只是个人喜好，你大可以只定义一个Action（假设其名称为“Book”），之后通过“http://localhost:8080/Struts2_CRUD/Book!list.action”的方式来访问，详细做法请参考《Struts 2.0的Action讲解》。另外，我由于希望在完成编辑或删除之后回到列表页，所以使用类型为redirect（重定向）的result。

下面是列表页面的代码：

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Book List</title>
<style type="text/css">
table {
border: 1px solid black;
border-collapse: collapse;
}

table thead tr th {
border: 1px solid black;
padding: 3px;
background-color: #cccccc;
}

table tbody tr td {
border: 1px solid black;
padding: 3px;
}
</style>
</head>
<body>
<h2>Book List</h2>
<s:form action="Remove" theme="simple">
<table cellspacing="0">
<thead>
<tr>
<th>Select</th>
<th>ISBN</th>
<th>Title</th>
<th>Price</th>
<th>Operation</th>
</tr>
</thead>
<tbody>
<s:iterator value="books">
<tr>
<td><input type="checkbox" name="isbns" value='<s:property value="isbn" />' /></td>
<td><s:property value="isbn" /></td>
<td><s:property value="title" /></td>
<td>$<s:property value="price" /></td>
<td>
<a href='<s:url action="Edit"><s:param name="isbn" value="isbn" /></s:url>'>
Edit
</a>
&nbsp;
<a href='<s:url action="Remove"><s:param name="isbn" value="isbn" /></s:url>'>
Delete
</a>
</td>
</tr>
</s:iterator>
</tbody>
</table>
<s:submit value="Remove" /><a href="Edit.jsp">Add Book</a>
</s:form>
</body>
</html>

清单5 WebContent\Book\List.jsp

以上代码，值得注意的是在<s:form>标签，我设置了theme属性为“simple”，这样可以取消其默认的表格布局。之前，有些朋友问我“如果不希望提交按钮放在右边应该怎样做？”，上述做汗是答案之一。当然，更佳的做法自定义一个theme，并将其设为默认应用到整个站点，如此一来就可以得到统一的站点风格。我会在以后的文章中会对此作详细的描述。

编辑或添加书籍的页面代码如下：

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Book</title>
</head>
<body>
<h2>
<s:if test="null == book">
Add Book
</s:if>
<s:else>
Edit Book
</s:else>
</h2>
<s:form action="Store" >
<s:textfield name="book.isbn" label="ISBN" />
<s:textfield name="book.title" label="Title" />
<s:textfield name="book.price" label="Price" />
<s:submit />
</s:form>
</body>
</html>

清单6 WebContent/Book/Edit.jsp

如果book为null，则表明该页面用于添加书籍，反之则为编辑页面。

为了方便大家运行示例，我把web.xml的代码也贴出来，如下：

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_9" version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

<display-name>Struts 2 Fileupload</display-name>

<filter>
<filter-name>struts2</filter-name>
<filter-class>
org.apache.struts2.dispatcher.FilterDispatcher
</filter-class>
</filter>

<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>

</web-app>

清单7 WebContent/WEB-INF/web.xml

大功告成，下面发布运行应用程序，在浏览器中键入：http://localhost:8080/Struts2_CRUD/Book/List.action，出现如下图所示页面：

清单8 列表页面

点击“Add Book”，出现如下图所示页面：

清单9 添加书籍页面

后退回到列表页面，点击“Edit”，出现如下图所示页面：

清单10 编辑书籍页面

总结

本文只是粗略地了介绍Struts 2的CRUD实现方法，所以有很多功能没有实现，如国际化和数据校验等。大家可以在上面例子的基础将其完善，当作练习也不错。如果过程中有不明白之处，可以参考我早前的文章或给我发E-Mail：max.m.yuan@gmail.com。


Struts 2中的OGNL

本人是一个EL（Expression Language，以下译为表达式语言）的支持者。因为我对<% %>写法极为反感，忘记了在那本书上看到的一句话——“使用标志（Tag）的一个目的就是避免在JSP页面中出现过多的<%%>的语句，使页面与后台代码分离。”

表达式语言主要有以下几大好处：

1. 避免(MyType) request.getAttribute()和myBean.getMyProperty()之类的语句，使页面更简洁；

2. 支持运算符（如+-*/），比普通的标志具有更高的自由度和更强的功能；

3. 简单明了地表达代码逻辑，使用代码更可读与便于维护。

Struts 2中的表达式语言

Struts 2支持以下几种表达式语言：

1. OGNL（Object-Graph Navigation Language），可以方便地操作对象属性的开源表达式语言；

2. JSTL（JSP Standard Tag Library），JSP 2.0集成的标准的表达式语言；

3. Groovy，基于Java平台的动态语言，它具有时下比较流行的动态语言（如Python、Ruby和Smarttalk等）的一些起特性；

4. Velocity，严格来说不是表达式语言，它是一种基于Java的模板匹配引擎，具说其性能要比JSP好。

Struts 2默认的表达式语言是OGNL，原因是它相对其它表达式语言具有下面几大优势：

1. 支持对象方法调用，如xxx.doSomeSpecial()；

2. 支持类静态的方法调用和值访问，表达式的格式为@[类全名（包括包路径）]@[方法名 | 值名]，例如：@java.lang.String@format('foo %s', 'bar')或@tutorial.MyConstant@APP_NAME；

3. 支持赋值操作和表达式串联，如price=100, discount=0.8, calculatePrice()，这个表达式会返回80；

4. 访问OGNL上下文（OGNL context）和ActionContext；

5. 操作集合对象。

OGNL的用法

OGNL是通常要结合Struts 2的标志一起使用，如<s:property value="xx" />等。大家经常遇到的问题是#、%和$这三个符号的使用。下面我想通过例子讲述这个问题：

首先新建名为Struts2_OGNL的Web工程，配置开发环境。之前很多朋友在使用Struts 2的过程中都遇到乱码问题。当然乱码问题由来已久，而且涉及多方面的知识，所以并非三言两语可以说明白，而且互联网上也已经有很多这方便的文章，大家可以Google一下。不过，如果你在开发的过程，多注意一下，避免乱码问题也不难。乱码多数是由于编码与解码所使用的方式不同造成的，所以我建议大家将编码方式都设为“utf-8”，如<%@  page language="java"
contentType="text/html; charset=utf-8"
pageEncoding="utf-8" %>。另外，在配置web.xml时使用ActionContextCleanUp过滤器（Filter），如下面代码所示：

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_9" version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

<display-name>Struts 2 OGNL</display-name>

<filter>
<filter-name>struts-cleanup</filter-name>
<filter-class>
org.apache.struts2.dispatcher.ActionContextCleanUp
</filter-class>
</filter>

<filter-mapping>
<filter-name>struts-cleanup</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<filter>
<filter-name>struts2</filter-name>
<filter-class>
org.apache.struts2.dispatcher.FilterDispatcher
</filter-class>
</filter>

<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>

</web-app>

清单1 WebContent/WEB-INF/web.xml

“#”主要有三种用途：

1. 访问OGNL上下文和Action上下文，#相当于ActionContext.getContext()；下表有几个ActionContext中有用的属性：

名称 | 作用 | 例子
parameters | 包含当前HTTP请求参数的Map | #parameters.id[0]作用相当于request.getParameter("id")
request | 包含当前HttpServletRequest的属性（attribute)的Map | #request.userName相当于request.getAttribute("userName")
session | 包含当前HttpSession的属性（attribute）的Map | #session.userName相当于session.getAttribute("userName")
application | 包含当前应用的ServletContext的属性（attribute）的Map | #application.userName相当于application.getAttribute("userName")
attr | 用于按request > session > application顺序访问其属性（attribute） | #attr.userName相当于按顺序在以上三个范围（scope）内读取userName属性，直到找到为止

2. 用于过滤和投影（projecting)集合，如books.{?#this.price<100}；

3. 构造Map，如#{'foo1':'bar1', 'foo2':'bar2'}。

下面让我们它们的具体写法，首先是Action类代码：

package tutorial.action;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

import org.apache.struts2.interceptor.ServletRequestAware;
import org.apache.struts2.interceptor.SessionAware;
import org.apache.struts2.util.ServletContextAware;

import tutorial.model.Book;

import com.opensymphony.xwork2.ActionSupport;

public class OgnlAction extends ActionSupport implements ServletRequestAware, SessionAware, ServletContextAware [image] {
private static final long serialVersionUID = 1L;

private HttpServletRequest request;
private Map<String, String> session;
private ServletContext application;
private List<Book> books;

public void setServletRequest(HttpServletRequest request) [image] {
this.request = request;
}

@SuppressWarnings("unchecked")
public void setSession(Map session) [image] {
this.session = session;
}

public void setServletContext(ServletContext application) [image] {
this.application = application;
}

public List<Book> getBooks() [image] {
return books;
}

@Override
public String execute() [image] {
request.setAttribute("userName", "Max From request");
session.put("userName", "Max From session");
application.setAttribute("userName", "Max From application");

books = new LinkedList<Book>();
books.add(new Book("978-0735619678", "Code Complete, Second Edition", 32.99));
books.add(new Book("978-0596007867", "The Art of Project Management", 35.96));
books.add(new Book("978-0201633610", "Design Patterns: Elements of Reusable Object-Oriented Software", 43.19));
books.add(new Book("978-0596527341", "Information Architecture for the World Wide Web: Designing Large-Scale Web Sites", 25.19));
books.add(new Book("978-0735605350", "Software Estimation: Demystifying the Black Art", 25.19));

return SUCCESS;
}
}

清单2 src/tutorial/action/OgnlAction.java

以上代码分别在request、session和application的范围内添加“userName”属性，然后再在JSP页面使用OGNL将其取回。我还创建了Book对象的列表用于演示“用于过滤和投影（projecting)集合”的功能，至于Book的代码大家可以在我前一文章《在Struts 2中实现CRUD》看到。

下面是Ognl.jsp的代码，内容如下：

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Struts OGNL Demo</title>
</head>
<body>
<h3>访问OGNL上下文和Action上下文</h3>
<p>parameters: <s:property value="#parameters.userName" /></p>
<p>request.userName: <s:property value="#request.userName" /></p>
<p>session.userName: <s:property value="#session.userName" /></p>
<p>application.userName: <s:property value="#application.userName" /></p>
<p>attr.userName: <s:property value="#attr.userName" /></p>
<hr />
<h3>用于过滤和投影（projecting)集合</h3>
<p>Books more than $35</p>
<ul>
<s:iterator value="books.{?#this.price > 35}">
<li><s:property value="title" /> - $<s:property value="price" /></li>
</s:iterator>
</ul>
<p>The price of "Code Complete, Second Edition" is: <s:property value="books.{?#this.title=='Code Complete, Second Edition'}.{price}[0]"/></p>
<hr />
<h3>构造Map</h3>
<s:set name="foobar" value="#{'foo1':'bar1', 'foo2':'bar2'}" />
<p>The value of key "foo1" is <s:property value="#foobar['foo1']" /></p>
</body>
</html>

清单3 WebContent/Ognl.jsp

以上代码值得注意的是“<s:property value="books.{?#this.title=='Code Complete, Second Edition'}.{price}[0]"/>”，因为“books.{?#this.title=='Code Complete, Second Edition'}.{price}”返回的值是集合类型，所以要用“[索引]”来访问其值。

最后是Struts 2的配置文件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>
<constant name="struts.devMode" value="true" />
<package name="Struts2_OGNL_DEMO" extends="struts-default">
<action name="Ognl" class="tutorial.action.OgnlAction">
<result>/Ognl.jsp</result>
</action>
</package>
</struts>

清单4 src/struts.xml

发布运行应用程序，结果如下所示：

点击查看放大图片
清单5 示例运行结果1

“%”符号的用途是在标志的属性为字符串类型时，计算OGNL表达式的值。例如在Ognl.jsp中加入以下代码：

<hr />
<h3>%的用途</h3>
<p><s:url value="#foobar['foo1']" /></p>
<p><s:url value="%{#foobar['foo1']}" /></p>

清单6 演示%用途的代码片段

刷新页面，结果如下所示：

清单7 示例运行结果2

“$”有两个主要的用途

1. 用于在国际化资源文件中，引用OGNL表达式，例子请参考《在Struts 2.0中国际化(i18n)您的应用程序》

2. 在Struts 2配置文件中，引用OGNL表达式，如

<action name="AddPhoto" class="addPhoto">
<interceptor-ref name="fileUploadStack" />
<result type="redirect">ListPhotos.action?albumId=${albumId}</result>
</action>

清单8 演示$用途的代码片段

总结

OGNL是一种功能很大的表达式语言，熟悉它可以使我们的开发变得更快捷。


Strus 2的新表单标志的使用

Struts 2为大家提供了不少常用的很酷的表单标志，简化了我们程序员的工作。不过，由于这些都是新标志，大家可能在使用上还存在不少疑问。本文将就朋友们的回复、留言或Email上的问题，分别对这些酷标志进行讲述。

表单标志使用小技巧

Struts 2的表单标志在输出（render）HTML时，使用了模板的概念，增加了复杂性（因为它不像Struts 1.x的表单标志，它通常都是一个标志对应HTML的一个元素），因此大家在使用时，需要一些技巧：

1. Struts 2的UI标志的表单标志默认是以表格布局，按钮是右对齐的。如果你不喜欢此风格，你可以简单地将<s:form />标志的“theme”属性设为“simple”，然后用以往的做法自已布局表单元素（注意：此法有利有弊，弊就是当你将“theme”属性设为“simple”时，表单标志以最简单方式输出HTML，所以你可能失去一些默认输出提供的便利，如：友好的错误信息的显示，或客户端的表单验证等）。当然更好的做法是通过CSS或自定义主题（theme）然后应用到整个应用程序，这样可以获得一致的页面风格，加强用户体验（我会在以后的文章对此进行讲解）；

2. 当你在页面上加入某些标志（如：<s:doubleselect />等）时，应该通过action来访问页面，而不是通过*.jsp的URL直接访问。

下面我将分别对这些标志进行讲述：

1、<s:checkboxlist />

大家对<s:checkboxlist />的最大的疑问可能是：“如何在默认情况下，选中某些checkbox？”

答案其实很简单，只需要将其“value”属性设为你的要选中的值，如以代码所示：

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Struts 2 Cool Tags - &lt;s:checkboxlist/ &gt;</title>
<s:head />
</head>
<body>
<h2>&lt;s:checkboxlist/&gt;</h2>
<s:form action="Store" >
<s:checkboxlist name="skills1"
label="Skills 1"
list="{ 'Java', '.Net', 'RoR', 'PHP' }"
value="{ 'Java', '.Net' }" />
<s:checkboxlist name="skills2"
label="Skills 2"
list="#{ 1:'Java', 2: '.Net', 3: 'RoR', 4: 'PHP' }"
listKey="key"
listValue="value"
value="{ 1, 2, 3 }"/>
</s:form>
</body>
</html>

清单1 WebContent/checkboxlist.jsp

分布运行应用程序，在浏览器中键入：http://localhost:8080/Struts2_CoolTags/checkboxlist.jsp，出现如下图所示页面：

清单2 checkboxlist.jsp页面

2、<s:doubleselect />

大家看Struts 2的showcase的例子，<s:doubleselect />的用法如下所示：

    <s:doubleselect
tooltip="Choose Your State"
label="State"
name="region" list="{'North', 'South'}"
value="'South'"
doubleValue="'Florida'"
doubleList="top == 'North' ? {'Oregon',
'Washington'} : {'Texas',
'Florida'}"
doubleName="state"
headerKey="-1"
headerValue="---------- Please Select
----------"
emptyOption="true" />

清单3 Showcase中<s:doubleselect />

很多朋友问：“上面的‘list’属性只有两个值，如果我有三个或更多的值，‘doublelist’属性应该如何设定呢？”

我建议的做法是先定义一个Map类型的对象，键为“list”的集合，值则为“doubleList”的集合，然后“doubleList”的OGNL写成“#myMap[top]”，如以下代码所示：

<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Struts 2 Cool Tags - &lt;s:doubeselect/ &gt;</title>
<s:head />
</head>
<body>
<h2>&lt;s:doubleselect/&gt;</h2>
<s:form action="Store" >
<s:set name="foobar"
value="#{'Java': {'Spring', 'Hibernate', 'Struts 2'}, '.Net': {'Linq', ' ASP.NET 2.0'}, 'Database': {'Oracle', 'SQL Server', 'DB2', 'MySQL'}}" />
<s:doubleselect list="#foobar.keySet()"
doubleName="technology"
doubleList="#foobar[top]"
label="Technology" />
</s:form>
</body>
</html>

清单4 WebContent/doubleselect.jsp

分布运行应用程序，在浏览器中键入：http://localhost:8080/Struts2_CoolTags/doubleselect.action，出现如下图所示页面：

清单5 doubleselect.jsp页面

3、<s: token />

这个标志可能大家不常用，不过本人认为它还是挺有用的。在使用Struts 1.x时，因为跳转通常是用Forward（而不是Redirect）实现的，所以当用户完成请求后，按“F5”刷新页面时，就会重新提交上次的请求，这样经常会出错。要解决这个问题，<s:token />可以帮你忙。

实现原理

在页面加载时，<s: token />产生一个GUID（Globally Unique Identifier，全局唯一标识符）值的隐藏输入框如：

<input type="hidden" name="struts.token.name" value="struts.token"/>
<input type="hidden" name="struts.token" value="BXPNNDG6BB11ZXHPI4E106CZ5K7VNMHR"/>

清单6 <s:token />的HTML输出

同时，将GUID放到会话（session）中；在执行action之前，“token”拦截器将会话token与请求token比较，如果两者相同，则将会话中的token删除并往下执行，否则向actionErrors加入错误信息。如此一来，如果用户通过某种手段提交了两次相同的请求，两个token就会不同。

具体实现

首先看一下Action的代码：

package tutorial;

import com.opensymphony.xwork2.ActionSupport;

public class CoolTagAction extends ActionSupport [image] {
private static final long serialVersionUID = 6820659617470261780L;

private String message;

public String getMessage() [image] {
return message;
}

public void setMessage(String message) [image] {
this.message = message;
}

@Override
public String execute() [image] {
System.out.println("Executing action, your message is " + message);
return SUCCESS;
}
}

清单7 src/tutorial/CoolTagAction.java

以上代码一目了然，再看看JSP的写法：

%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Struts 2 Cool Tags - &lt;s:token/ &gt;</title>
<s:head />
</head>
<body>
<h2>&lt;s:token/&gt;</h2>
<s:actionerror />
<s:form action="Token" >
<s:textfield name="message" label="Message" />
<s:token />
<s:submit />
</s:form>
</body>
</html>

清单8 WebContent/token.jsp

JSP也很简单，就是加入<s:token />标志。接下来是Actoin配置的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>
<package name="Struts2_COOL_TAGS_DEMO" extends="struts-default">
<action name="Token" class="tutorial.CoolTagAction">
<interceptor-ref name="defaultStack" />
<interceptor-ref name="token" />
<result name="invalid.token">/token.jsp</result>
<result>/token.jsp</result>
</action>
<action name="*">
<result>/{1}.jsp</result>
</action>
</package>
</struts>

清单9 src/struts.xml

以上XML片段值注意的是加入了“token”拦截器和“invalid.token”结果，因为“token”拦截器在会话token与请求token不一致时，将会直接返回“invalid.token”结果。

发布运行应用程序，在浏览器中键入：http://localhost:8080/Struts2_CoolTags/token.jsp，出现如下图所示页面：

清单10 正常显示的token.jsp页面

随便填点东西并提交页面，一切正常返回以上页面，然后按“F5”刷新页面，在弹出的对话框中点击“Retry”，出现如下图所示页面：

重复提交出错显示
清单11 重复提交出错显示

4、<s:datetimepicker />、<s:optiontransferselect />和<s:updownselect />

这几个标志的使用相对简单，所以我想小举一例即可，以下是JSP的代码：

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Struts 2 Cool Tags - Others</title>
<s:head />
</head>
<body>
<h2>Others</h2>
<s:form action="Store" >
<s:datetimepicker name="birthday" label="Birthday" />
<s:updownselect
label = "Favourite Countries"
list="#{'england':'England', 'america':'America', 'germany':'Germany'}"
name="prioritisedFavouriteCountries"
headerKey="-1"
headerValue="--- Please Order Them Accordingly ---"
emptyOption="true" />
<s:optiontransferselect
label="Favourite Cartoons Characters"
name="leftSideCartoonCharacters"
leftTitle="Left Title"
rightTitle="Right Title"
list="{'Popeye', 'He-Man', 'Spiderman'}"
multiple="true"
headerKey="headerKey"
headerValue="--- Please Select ---"
emptyOption="true"
doubleList="{'Superman', 'Mickey Mouse', 'Donald Duck'}"
doubleName="rightSideCartoonCharacters"
doubleHeaderKey="doubleHeaderKey"
doubleHeaderValue="--- Please Select ---"
doubleEmptyOption="true"
doubleMultiple="true" />
</s:form>
</body>
</html>

清单12 WebContent\others.jsp页面

发布运行应用程序，在浏览器中键入：http://localhost:8080/Struts2_CoolTags/others.jsp，出现如下图所示页面：

点击查看大图
清单13 其它表单标志页面

总结

Struts 2在标志上的确比Struts 1.x丰富了许多，同时模板机制也给程序员带来不少方便（如果你不太喜欢个性化的风格）。另外，Struts 2还有一些AJAX（如<s: autocompleter />等）的标志和非表单的UI标志（如<s: tree />等），我会在以后的文章中讲述其使用。


Struts 2与AJAX（第一部分）

在当今——Web 2.0概念铺天盖地的Internet环境下，简易的AJAX集成对于一个成功的WEB框架来说是不可或缺的。因此，Struts 2其中的一个重要的功能（Feature）就是“First-class AJAX support - Add interactivity and flexibility with AJAX tags that look and feel just like standard Struts tags（大意：一流的AJAX支持——通过AJAX标志增加互动性和灵活性，而且使用这些AJAX标志与普通的Struts标志同样简单）”。

实现原理

基于不重新发明轮子的原则，Struts 2并没有开发新的AJAX框架，而是使用时下Java EE平台中比较流行的AJAX框架——Dojo和DWR。

最近在Musachy Barroso等同志的无私奉献下，开发了Struts 2的JSON插件（Plugin），极大地方便了我们输出JSON结果（Result）。

JSON插件（Plugin）

在Struts 2的showcase中的AJAX部分，JSON的结果输出是通过Freemaker模板实现。这种方法在简易性和灵活性上都比不上JSON插件，所以JSON插件值得向大家五星推荐。

下面让我们看一个JSON插件的例子。

首先到以下网址http://code.google.com/p/jsonplugin/downloads/list下载JSON插件的JAR包，并将其加入你的WebContent\WEB-INF\lib下。

接下是本例子的Action代码：

package tutorial;

import java.util.ArrayList;
import java.util.List;

import com.googlecode.jsonplugin.annotations.JSON;
import com.opensymphony.xwork2.ActionSupport;

public class JsonPluginAction extends ActionSupport [image] {
private static final long serialVersionUID = -6784977600668791997L;

private int bookId;
private String title;
private double price;
private List<String> comments;
private transient String secret1;
private String secret2;

@JSON(name="ISBN")
public int getBookId() [image] {
return bookId;
}

public void setBookId(int bookId) [image] {
this.bookId = bookId;
}

public List<String> getComments() [image] {
return comments;
}

public void setComments(List<String> comments) [image] {
this.comments = comments;
}

public double getPrice() [image] {
return price;
}

public void setPrice(double price) [image] {
this.price = price;
}

public String getTitle() [image] {
return title;
}

public void setTitle(String title) [image] {
this.title = title;
}

@Override
public String execute() [image] {
bookId = 15645912;
title = "Max On Java";
price = 0.9999d;
comments = new ArrayList<String>(3);
comments.add("It's no bad!");
comments.add("WOW!");
comments.add("No comment!");
secret1 = "You can't see me!";
secret2 = "I am invisible!";
return SUCCESS;
}
}

清单1 src/tutorial/JsonPluginAction.java

以上代码值得注意的是，通过@JSON的JAVA注释（Annotation），我们可以改变JSON结果的属性名称，另外带有transient修饰符与没有Getter方法的字段（field）都不会被串行化为JSON。

然后，我们来配置一下此Action，代码如下：

<?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>
<package name="Struts2_AJAX_DEMO" extends="json-default">
<action name="JsonPlugin" class="tutorial.JsonPluginAction">
<result type="json" />
</action>
</package>
</struts>

清单2 src/struts.xml

上面配置文件的“package”元素和以往不同的是，它扩展了“json-default”而不是“struts-default”。“json-default”是在jsonplugin-0.11.jar包里的struts-plugin.xml中定义的。该文件同时定义了“json”的结果类型，有兴趣的朋友可以打开此文件看看。

发布运行应用程序，在浏览器中键入：http://localhost:8080/Struts2_Ajax/JsonPlugin.action，出现下载文件对话框，原因是JSON插件将HTTP响应（Response）的MIME类型设为“application/json”。把文件下载下来，用记事本打开，内容如下：

{"ISBN":15645912,"comments":["It's no bad!","WOW!","No comment!"],"price":0.9999,"title":"Max On Java"}

清单3 例子1输出的JSON串

当然这还不是一个完整的AJAX的例子，下面让我们写一个HTML文件将其完成，HTML代码如下：

<!DOCTYPE html PUBLIC
"-//W3C//DTD
XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
<title>JSON Plugin</title>
<script type="text/javascript">
var bXmlHttpSupport
= (typeof XMLHttpRequest != "undefined" || window.ActiveXObject);

if (typeof XMLHttpRequest == "undefined"
&& window.ActiveXObject) {
function XMLHttpRequest()
{
var arrSignatures = ["MSXML2.XMLHTTP.5.0",
"MSXML2.XMLHTTP.4.0",
"MSXML2.XMLHTTP.3.0",
"MSXML2.XMLHTTP",
"Microsoft.XMLHTTP"];

for (var i=0;
i < arrSignatures.length;
i++) {
try {
var oRequest
= new ActiveXObject(arrSignatures[i]);
return oRequest;
} catch (oError) { /*ignore*/ }
}

throw new Error("MSXML is not installed
on your system.");
}
}

function retrieveBook()
{
if(bXmlHttpSupport)
{
var sUrl = 'JsonPlugin.action';
var oRequest = new XMLHttpRequest();
oRequest.onreadystatechange
= function() {
if(oRequest.readyState == 4) {
var oBook = eval('(' + oRequest.responseText
+ ')');
var bookHolder
= document.getElementById('bookHolder');
var sBook =
'<p><b>ISBN: </b>' + oBook.ISBN +
'</p>';
sBook += ('<p><b>Title: </b>' + oBook.title + '</p>');
sBook += ('<p><b>Price: </b>$' + oBook.price + '</p>');
sBook += ('<b><i>Comments:
</i></b><hr/>');
for(i = 0; i < oBook.comments.length; i++) {
sBook += ('<p><b>#' + (i + 1) + ' </b>' + oBook.comments[i] + '</p>');
}
bookHolder.innerHTML = sBook;
}
};
oRequest.open('POST',
sUrl);
oRequest.send(null);
}
}
</script>
</head>
<body>
<input type="button" value="Retrieve
Book" onclick="retrieveBook()" />
<div id="bookHolder"></div>
</body>
</html>

清单4 WebContent/JsonPlugin.html

以上代码中，我没有使用任何的AJAX的Javascript包，而是参考《Professional Javascript For Web Developer》手工创建XHR（XMLHttpRequest），并在XHR完成后使用eval()方法将JSON字符串变为JSON对象。需要注意的是，要调用eval函数时，必须使用“(”和“)”将JSON字符串括起来，否则会出错的。

打开http://localhost:8080/Struts2_Ajax/JsonPlugin.html，点击“Retrieve Book”按钮，页面如下图所示：

图1 JsonPlugin.html页面输出

Struts 2与Dojo

Dojo是开源Javascript工具包，它引了Widget的概念，方便了Javascript面向对象编程（OOP），改进Javascript的事件模型。在此我不打算对此进行深入的讲解，有兴趣的朋友的可以找网上找一些关于Dojo的资料学习。

Struts 2基于Dojo编写一些AJAX标志（在Dojo中称为Widget），要使用这些标志的AJAX功能，需要将标志的“theme”属性设为“ajax”。同时，亦需要将加入在<head>与</head>之间加入<s:head theme="ajax" />。当使用这些标志的AJAX功能，有些属性可能会经常用到，所以我会对这些属性稍作解释。

名称 | 描述
href | XHR（XMLHttpRequest）请求的地址
listenTopics | 监听的Dojo话题（Topic）以触发自身，如可以在可以通过发布（Publish）相应的话题，通知<s:autocompleter />重新加载其备选项（Options）
notifyTopics | 完成远程调用后，发出通知，触发相应的Javascript函数或Dojo Widget
formId | 需要提交到服务器的表单的ID
formFilter | 过滤表单字段的Javascript函数名称
indicator | 在XHR处理过程中，包含用户提示的信息的HTML元素的ID，如图片或DIV等

表1 常用的AJAX标志属性

这些标志包括：<s:a />、<s: submit />、<s:autocompleter />和<s:tree />等，下面我将分别讲解。

1、<s:a />和<s:submit />

这两个标志方便了我们的调用XHR实现AJAX，所以上面的HTML如果使用了这两标志将会变得更简单，因为我们不用再去理会繁锁的XHR创建和设定的工作。下面是示例代码：

<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<%@ taglib prefix="s" uri="/struts-tags"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>JSON Plugin</title>
<s:head theme="ajax" />
<script type="text/javascript">
dojo.addOnLoad(function() {
dojo.event.topic.subscribe('retrieveBook', this, function(data, type, e){
if(type == 'load') {
showBook(data);
} else if(type == 'error') {
alert('Can not retrieve the book');
}
});
});

function showBook(strBook) {
var oBook = eval('(' + strBook + ')');
var bookHolder = document.getElementById('bookHolder');
var sBook = '<p><b>ISBN: </b>' + oBook.ISBN + '</p>';
sBook += ('<p><b>Title: </b>' + oBook.title + '</p>');
sBook += ('<p><b>Price: </b>$' + oBook.price + '</p>');
sBook += ('<b><i>Comments: </i></b><hr/>');
for(i = 0; i < oBook.comments.length; i++) {
sBook += ('<p><b>#' + (i + 1) + ' </b>' + oBook.comments[i] + '</p>');
}
bookHolder.innerHTML = sBook;
}
</script>
</head>
<body>
<s:url id="bookUrl" value="/JsonPlugin.action" />
<s:submit href="%{bookUrl}" theme="ajax" indicator="indicator"
value="Retrieve Book" align="left" notifyTopics="retrieveBook" />
<s:a theme="ajax" href="%{bookUrl}" indicator="indicator"
notifyTopics="retrieveBook">Retrieve Book</s:a>
<img id="indicator"
src="${pageContext.request.contextPath}/images/indicator.gif"
alt="Loading [image] " style="display:none" />
<div id="bookHolder"></div>
</body>
</html>

清单5 WebContent/LinkButton.jsp

可能上述代码还不够简洁，因为我将HTML格式化的工作都放在Javascript中完成。但如果你的XHR返回的是HTML片段，你可以简单地将<s:a />或<s:submit />的“targets”属性设为“bookHolder”即可，详情大家可以参考Struts 2 Showcase。至于返回HTML片段，可以通过Action + Freemaker完成。

2、<s:autocompleter />

Autocomplete是比较经典的AJAX应用，虽然谷歌已经停止使用这一功能，但就Autocompleter自身而言的确是很酷的。下面是一个<s:autocompleter />的例子。

首先，我要伪造一些字符串数据，代码如下：

package tutorial;

import java.util.ArrayList;
import java.util.List;

public final class Datas [image] {
public static final List<String> NAMES;
static [image] {
NAMES = new ArrayList<String>();
NAMES.add("Alabama");
NAMES.add("Alaska");
NAMES.add("American Samoa");
NAMES.add("Arizona");
NAMES.add("Arkansas");
NAMES.add("Armed Forces Europe");
NAMES.add("Armed Forces Pacific");
NAMES.add("Armed Forces the Americas");
NAMES.add("California");
NAMES.add("Colorado");
NAMES.add("Connecticut");
NAMES.add("Delaware");
NAMES.add("District of Columbia");
NAMES.add("Federated States of Micronesia");
NAMES.add("Florida");
NAMES.add("Georgia");
NAMES.add("Guam");
NAMES.add("Hawaii");
NAMES.add("Idaho");
NAMES.add("Illinois");
NAMES.add("Indiana");
NAMES.add("Iowa");
NAMES.add("Kansas");
NAMES.add("Kentucky");
NAMES.add("Louisiana");
NAMES.add("Maine");
NAMES.add("Marshall Islands");
NAMES.add("Maryland");
NAMES.add("Massachusetts");
NAMES.add("Michigan");
NAMES.add("Minnesota");
NAMES.add("Mississippi");
NAMES.add("Missouri");
NAMES.add("Montana");
NAMES.add("Nebraska");
NAMES.add("Nevada");
NAMES.add("New Hampshire");
NAMES.add("New Jersey");
NAMES.add("New Mexico");
NAMES.add("New York");
NAMES.add("North Carolina");
NAMES.add("North Dakota");
NAMES.add("Northern Mariana Islands");
NAMES.add("Ohio");
NAMES.add("Oklahoma");
NAMES.add("Oregon");
NAMES.add("Pennsylvania");
NAMES.add("Puerto Rico");
NAMES.add("Rhode Island");
NAMES.add("South Carolina");
NAMES.add("South Dakota");
NAMES.add("Tennessee");
NAMES.add("Texas");
NAMES.add("Utah");
NAMES.add("Vermont");
NAMES.add("Virgin Islands, U.S.");
NAMES.add("Virginia");
NAMES.add("Washington");
NAMES.add("West Virginia");
NAMES.add("Wisconsin");
NAMES.add("Wyoming");
}
}

清单6 src/tutorial/Datas.java

然后是用于获取和过滤数据的Action，代码如下：

package tutorial;

import java.util.ArrayList;
import java.util.List;

import com.opensymphony.xwork2.ActionSupport;

public class AutocompleterAction extends ActionSupport [image] {
private static final long serialVersionUID = -8201401726773589361L;

private List<String[]> names;
private String start;

public void setStart(String start) [image] {
this.start = start;
}

public List<String[]> getNames() [image] {
return names;
}

@Override
public String execute() [image] {
names = new ArrayList<String[]>();
if(start == null || "".equals(start.trim())) [image] {
start = "a";
}
for(String s : Datas.NAMES) [image] {
if(s.toLowerCase().startsWith(start.toLowerCase())) [image] {
names.add(new String[] [image] { s, s });
}
}
return SUCCESS;
}
}

清单7 src/tutorial/AutocmpleterAction.java

上述Action会以JSON的形式返回以start开头的Datas.NAMES的中字符串，以下是此Action的配置：

<action name="Autocompleter" class="tutorial.AutocompleterAction">
<result type="json">
<param name="root">names</param>
</result>
</action>

清单8 Autocompleter Action的配置代码片段

在JSON类型结果的参数中加入“root”参数可以设定输出JSON结果的根，以上述情况为例，如果没有“root”参数，输出将为“{ "names": [ ["xxx", "xxx"]...] }”，加了之后变就会成“[ ["xxx", "xxx"]...] ”。接下来，让我们看看页面的代码：

<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<%@ taglib prefix="s" uri="/struts-tags"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Struts 2 AJAX - Autocompleter</title>
<s:head theme="ajax" />
</head>
<body>
<h2>
Autocompleter
</h2>
<s:form action="autocompleterForm">
<s:textfield label="abc" name="abc" />
<tr>
<td class="tdLabel">
<label class="label">
No AJAX Autocompleter:
</label>
</td>
<td>
<s:autocompleter theme="simple" name="user"
list="@tutorial.Datas@NAMES" />
</td>
</tr>
<tr>
<td class="tdLabel">
<label class="label">
AJAX Autocompleter:
</label>
</td>
<td>
<s:url id="dataUrl" value="/Autocompleter.action" />
<s:autocompleter theme="ajax" name="start" href="%{dataUrl}"
loadOnTextChange="true" loadMinimumCount="1" indicator="indicator"
autoComplete="false" showDownArrow="false" />
<img id="indicator"
src="${pageContext.request.contextPath}/images/indicator.gif"
alt="Loading [image] " style="display:none" />
</td>
</tr>
</s:form>
</body>
</html>

清单9 WebContent/Autocompleter.jsp

上述页面包含两个<s:autocompleter />标志，前者使用“simple”模板，所以不具有AJAX功能，它的数据将以HTML方式输出到最终页面里；而后者则使用了“ajax”模板，每当输入框的值发生改变时，它都向URL“/Autocompleter.action”发送请求，Action根据请求中的start参数的值，返回相当的JSON，在请求完成后页面通过回调函数改变输入框的下拉提示，效果如下图所示：

图2 Autocompleter.jsp页面输出

3、<s:tree />

树是是比较常用的数据结构，因为它可以很好地体现真实世界中对象之间的关系。<s:tree />的使用也相对简单，但需要说明的是——Struts 2.0.6 GA版本的<s:tree />是有BUG的，大家可以点击这个链接https://issues.apache.org/struts/browse/WW-1813了解详细的情况。这个BUG主要是在<s:tree />的通过“treeCollapsedTopic”、“treeExpandedTopic”和“treeSelectedTopic”设定的话题（Topic）都没有起作用，上述链接相应给出了解决方法，但我认为该方法太麻烦（需要自己重新编译和打包Struts 2），所以下面的例子，我将另辟徯径，请参考以下代码。

<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<%@ taglib prefix="s" uri="/struts-tags"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Struts 2 AJAX - Tree</title>
<s:head theme="ajax" debug="true" />
<script type="text/javascript">
function treeNodeSelected(arg) {
alert(arg.source.title + ' selected');
}
dojo.addOnLoad(function() {
var s = dojo.widget.byId('parentId').selector;
dojo.event.connect(s, 'select', 'treeNodeSelected');
});
</script>
</head>
<body>
<h2>
Tree
</h2>
<div style="float:left; margin-right: 50px;">
<s:tree label="parent" id="parentId" theme="ajax"
templateCssPath="/struts/tree.css" showRootGrid="true"
showGrid="true">
<s:treenode theme="ajax" label="child1" id="child1Id">
<s:treenode theme="ajax" label="grandchild1" id="grandchild1Id" />
<s:treenode theme="ajax" label="grandchild2" id="grandchild2Id" />
<s:treenode theme="ajax" label="grandchild3" id="grandchild3Id" />
</s:treenode>
<s:treenode theme="ajax" label="child2" id="child2Id" />
<s:treenode theme="ajax" label="child3" id="child3Id" />
<s:treenode theme="ajax" label="child4" id="child4Id" />
<s:treenode theme="ajax" label="child5" id="child5Id">
<s:treenode theme="ajax" label="gChild1" id="gChild1Id" />
<s:treenode theme="ajax" label="gChild2" id="gChild2Id" />
</s:treenode>
</s:tree>
</div>
</body>
</html>

清单10 WebContent/Tree.jsp

因为Dojo的树控件，即使在没有设定“selector”情况下，也会自动生成一个默认的Selector，所以只要将其事件绑定到特定的事件处理函数即可。

打开http://localhost:8080/Struts2_Ajax/Tree.jsp，点击任一树节点，页面如下图所示：

图3 Tree.jsp页面输出

总结

我原本打算用一篇文章写完这个“Struts 2与AJAX”。不过在写的过程中，发现内容越来越多。如果勉强写成一篇，朋友们读起来也会很麻烦，所以我决定分开几部分，本文为第一部分。

另外，之前有的朋友建议我建一个Google的讨论组，方便大家讨论问题。我觉得这个提议非常好，一直以来都是“一人写，大家留言”，这种相对单向的方式不免有所欠缺，而且本人所知也有限，开个讨论组大家可以相互讨论，共同进步。

因为Struts2已经被创建，所以申请了“struts2cn”，有兴趣的朋友，欢迎加入。


Struts 2与AJAX（第二部分）

在上一篇文章《Struts 2与AJAX（第一部分）》，我已经简单地介绍了<s:tree />的一些用法，接下来我将继续深入讲解<s:tree />的使用和通过DWR实现AJAX校验。

更多<s:tree />

在Struts 2的showcase中有两个<s:tree />的例子，分别是静态树与动态树。所谓的静态树即是在编写JSP代码时通过<s:treenode />生成树节点。我的上一篇文章的例子就是一个典型的静态树。而动态树则是在程序运行期间，Struts 2 运行时（Runtime）根据程序中的数据动态创建树节点。虽然在两个例子中<s:tree />的theme属性都为“ajax”，但是从严格意义上来说，这两种树都不属于AJAX树，因为它们都是在输出页面时将全部节点加载到其中，而不是在父节点展开时通过XHR（XMLHttpRequest）获取节点数据。

动态树

下面我们先看一下动态树的例子，接着再一步步地将其改造为名副其实的AJAX 树。下例将会把WEB应用程序的目录树展现在JSP页面中。因此，我需要先包装一下java.io.File 类，代码如下：

package tutorial;

import java.io.File;

public class FileWrapper [image] {
private File file;

public FileWrapper(String path) [image] {
file = new File(path);
}

public FileWrapper(File file) [image] {
this.file = file;
}

public String getId() [image] {
return "file_" + file.hashCode();
}

public String getName() [image] {
return file.getName();
}

public String getAbsolutePath() [image] {
return file.getAbsolutePath();
}

public FileWrapper[] getChildren() [image] {
File[] files = file.listFiles();
if(files != null && files.length > 0) [image] {
int length = files.length;
FileWrapper[] wrappers = new FileWrapper[length];
for(int i = 0; i < length; ++i) [image] {
wrappers[i] = new FileWrapper(files[i]);
}
return wrappers;
}
return new FileWrapper[0];
}
}

清单1 src/tutorial/FileWrapper.java

之所以需要对File类进行如此包装，是因为<s:tree />用于动态树时，rootNode、nodeIdProperty、nodeTitleProperty 和 childCollectionProperty等属性都必填的。

然后是Action类的代码如下：

package tutorial;

import javax.servlet.http.HttpServletRequest;

import org.apache.struts2.interceptor.ServletRequestAware;

import com.opensymphony.xwork2.ActionSupport;

public class DynamicTreeAction extends ActionSupport implements ServletRequestAware [image] {
private static final long serialVersionUID = 1128593047269036737L;

private HttpServletRequest request;
private FileWrapper root;

public void setServletRequest(HttpServletRequest request) [image] {
this.request = request;
}

public FileWrapper getRoot() [image] {
return root;
}

@Override
public String execute() [image] {
root = new FileWrapper(request.getSession().getServletContext().getRealPath("/"));
return SUCCESS;
}
}

清单2 src/tutorial/DynamicTreeAction.java

上述代码取得WEB应用程序的根目录的绝对路径后，初始化FileWrapper对象root。该对象将为JSP页面的<s:tree />的根节点。如下代码所示：

<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<%@ taglib prefix="s" uri="/struts-tags"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Struts 2 AJAX - More Tree</title>
<s:head theme="ajax" debug="true" />
<script type="text/javascript">
/* <![CDATA[ */
function treeNodeSelected(arg) {
alert(arg.source.title + ' selected');
}

function treeNodeExpanded(arg) {
alert(arg.source.title + ' expanded');
}

function treeNodeCollapsed(arg) {
alert(arg.source.title + ' collapsed');
}

dojo.addOnLoad(function() {
var t = dojo.widget.byId('appFiles');
dojo.event.topic.subscribe(t.eventNames.expand, treeNodeExpanded);
dojo.event.topic.subscribe(t.eventNames.collapse, treeNodeCollapsed);

var s = t.selector;
dojo.event.connect(s, 'select', 'treeNodeSelected');
});
/* ]]> */
</script>
</head>
<body>
<h2>
Dynamic Tree Example
</h2>
<div style="float:left; margin-right: 50px;">
<s:tree id="appFiles" theme="ajax" rootNode="root"
nodeTitleProperty="name" nodeIdProperty="id"
childCollectionProperty="children" />
</div>
</body>
</html>

清单3 WebContent/Tree.jsp

因为<s:tree />的treeCollapsedTopic和treeExpandedTopic属性都没有起作用，所以如果我们想要监听这两个事件，就必须使用上述代码的方法。

最后是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>
<package name="Struts2_AJAX_DEMO" extends="struts-default">
<action name="DynamicTree" class="tutorial.DynamicTreeAction">
<result>Tree.jsp</result>
</action>
</package>
</struts>

清单4 src/struts.xml

发布运行应用程序，在浏览器地址栏中键入http://localhost:8080/Struts2_Ajax2/DynamicTree.action，有如下图所示页面：

图1 动态树示例

AJAX 树

正如我在文章开头所说，Struts 2所提供的静态树和动态树都不是严格意义上的AJAX树。下面就让我们来实现一个如假包换的AJAX树。首先要说明的是，Struts 2的<s:tree />默认是不支持这种按需加载数据的AJAX树。不过因为它是基于Dojo的树控件（Widget）所以要扩展也很方便。

Dojo 通过名为“TreeRPCController”的控件实现 AJAX 树，它会监听被控制树的事件。当发生展开节点的事件时，TreeRPCController就会向URL发送XHR请求，该URL由TreeRPCController的RPCUrl 属性定义。XHR请求格式类似如下格式：

http://localhost:8080/Struts2_Ajax2/AjaxTree.action?action=getChildren&data={"node":{"widgetId":"file_226092423","objectId":"C:\\Program Files\\Tomcat 5.5\\webapps\\Struts2_Ajax2","index":0,"isFolder":true},"tree":{"widgetId":"appFiles","objectId":""}}&dojo.preventCache=1182913465392

清单5 XHR样本

显而易见，请求中包含三个参数，分别是action为“getChildren”（固定值），data一个包含当前节点与树信息的JSON串和dojo.preventCache随机串，用于缓存不同节点的请求响应（父节点只会在第一次被展开时到服务器端加载数据，之后都是从浏览器的缓存中读取数据，可以提高应用程序性能）。

首先我要先写一个加载树节点数据的Action类，代码如下：

package tutorial;

import java.util.Map;

import com.googlecode.jsonplugin.JSONExeption;
import com.googlecode.jsonplugin.JSONUtil;

public class AjaxTreeAction extends DynamicTreeAction [image] {
private static final long serialVersionUID = 3970019751740942311L;

private String action;
private String data;
private FileWrapper[] wrappers;

public void setAction(String action) [image] {
this.action = action;
}

public void setData(String data) [image] {
this.data = data;
}

public FileWrapper[] getWrappers() [image] {
return wrappers;
}

@Override
public String execute() [image] {
if("getChildren".equals(action)) [image] {
try [image] {
Object o = JSONUtil.deserialize(data);
String path = ((Map) ((Map) o).get("node")).get("objectId").toString();
wrappers = new FileWrapper(path).getChildren();
} catch (JSONExeption e) [image] {
e.printStackTrace();
}
return "ajax";
}
return super.execute();
}
}

清单6 src/tutorial/AjaxTreeAction.java

上述代码可能需要解释一下：

1. action属性对应于XHR中的action，如果它为“getChildren”时，则需要进行加载子节点操作。否则，会读取树的根节点，并返回JSP页面；

2. 通过上面XHR的分析，大家可以知道data是代表树和当前节点的JSON串，故应将其反串行化为Map对象，并将其 objectId属性取出。通常情况下，Dojo树的objectId属性代表服务器端的对象的标识，在本例中为文件夹的绝对路径；

3. wrappers属性表示当前文件夹下的文件数组，它被传送到Freemarker页面，翻译为Dojo树节点数组的JSON串。

下面是Freemarker页面的代码：

[
<#list wrappers as r>
{ "title": "${r.name}", "isFolder": <#if r.children?size gt 0>true<#else>false</#if>, "id": "${r.id}", "objectId": "${r.absolutePath?js_string}" }<#if r_has_next>,</#if>
</#list>
]

清单7 WebContent/AjaxTree.ftl

以上代码中<#list></#lsit>的写法是Freemarker中遍历集合的写法；而<#if r.children?size gt 0>判断“r”对象的children属性是否为空；r.absolutePath?js_string 就是将“r”的absolutePath属性的值输出为Javascript 的字串符形式；<#if r_has_next></#if>判断集合是否有下一项数据。如果希望更详细地了解Freemarker的使用，请参考该手册。

接下来，让我们看看Action的配置代码片段：

<action name="AjaxTree" class="tutorial.AjaxTreeAction">
<result>AjaxTree.jsp</result>
<result name="ajax" type="freemarker">AjaxTree.ftl</result>
</action>

清单8 src/struts.xml配置片段

最后是JSP页面代码：

<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<%@ taglib prefix="s" uri="/struts-tags"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Struts 2 AJAX - More Tree</title>
<s:head theme="ajax" debug="true" />
<script type="text/javascript">
/* <![CDATA[ */
function treeNodeSelected(arg) {
alert(arg.source.title + ' selected');
}

dojo.addOnLoad(function() {
var t = dojo.widget.byId('appFiles');
var s = t.selector;
dojo.event.connect(s, 'select', 'treeNodeSelected');
});
/* ]]> */
</script>
</head>
<body>
<h2>
AJAX Tree Example
</h2>
<div style="float:left; margin-right: 50px;">
<script type="text/javascript">
/* <![CDATA[ */
dojo.require("dojo.lang.*");
dojo.require("dojo.widget.*");
dojo.require("dojo.widget.Tree");
dojo.require("dojo.widget.TreeRPCController");
/* ]]> */
</script>
<div dojoType="TreeRPCController" widgetId="treeController"
DNDcontroller="create" RPCUrl="<s:url />"></div>
<div dojoType="Tree" widgetId="appFiles" toggle="fade" controller="treeController">
<div dojoType="TreeNode" title='<s:property value="root.name" />'
widgetId='<s:property value="root.id" />'
isFolder='<s:property value="root.children.length > 0" />'
objectId='<s:property value="root.absolutePath" />'>
</div>
</div>
</div>
</body>
</html>

清单9 WebContent/AjaxTree.jsp

由于上面所提及的原因，我在上述的代码中并没有使用<s:tree />标志，而是使用了Dojo的写法——创建 widgetId 为“treeController”的 TreeRPCController 并将设为树的控制器。

发布运行应用程序，在浏览器地址栏中键入http://localhost:8080/Struts2_Ajax2/AjaxTree.action，点开某个节点，在节点加载的过程中，加号图标变成时钟状图标，如下图所示页面：

图2 AJAX树示例

自定义<s:tree />的AJAX的主题（theme）

Struts 2的标志过人之外在于它允许开发人员自定义标志的页面输出。要做到这一点，你所需要做的只是创建一个自定义的theme并将其应用到相应标志。下面就让我自定义一个真正的AJAX的<s:tree/>的theme。

首先，你的源文件的根目录下新建包“template.realajax”。

然后，在上一步所建的包中新建“tree.ftl”文件，内容如下：

<script type="text/javascript">
/* <![CDATA[ */
dojo.require("dojo.lang.*");
dojo.require("dojo.widget.*");
dojo.require("dojo.widget.Tree");
dojo.require("dojo.widget.TreeRPCController"); <#-- Added by Max -->
/* ]]> */
</script>
<#-- Added by Max -->
<div dojoType="TreeRPCController"
widgetId="${parameters.id?html}_controller"
DNDcontroller="create"
RPCUrl="<@s.url />">
</div>
<#-- End -->
<div dojoType="Tree"
<#if parameters.blankIconSrc?exists>
gridIconSrcT="<@s.url value='${parameters.blankIconSrc}' encode="false" includeParams='none'/>"
</#if>
<#if parameters.gridIconSrcL?exists>
gridIconSrcL="<@s.url value='${parameters.gridIconSrcL}' encode="false" includeParams='none'/>"
</#if>
<#if parameters.gridIconSrcV?exists>
gridIconSrcV="<@s.url value='${parameters.gridIconSrcV}' encode="false" includeParams='none'/>"
</#if>
<#if parameters.gridIconSrcP?exists>
gridIconSrcP="<@s.url value='${parameters.gridIconSrcP}' encode="false" includeParams='none'/>"
</#if>
<#if parameters.gridIconSrcC?exists>
gridIconSrcC="<@s.url value='${parameters.gridIconSrcC}' encode="false" includeParams='none'/>"
</#if>
<#if parameters.gridIconSrcX?exists>
gridIconSrcX="<@s.url value='${parameters.gridIconSrcX}' encode="false" includeParams='none'/>"
</#if>
<#if parameters.gridIconSrcY?exists>
gridIconSrcY="<@s.url value='${parameters.gridIconSrcY}' encode="false" includeParams='none'/>"
</#if>
<#if parameters.gridIconSrcZ?exists>
gridIconSrcZ="<@s.url value='${parameters.gridIconSrcZ}' encode="false" includeParams='none'/>"
</#if>
<#if parameters.expandIconSrcPlus?exists>
expandIconSrcPlus="<@s.url value='${parameters.expandIconSrcPlus}' includeParams='none'/>"
</#if>
<#if parameters.expandIconSrcMinus?exists>
expandIconSrcMinus="<@s.url value='${parameters.expandIconSrcMinus?html}' includeParams='none'/>"
</#if>
<#if parameters.iconWidth?exists>
iconWidth="<@s.url value='${parameters.iconWidth?html}' encode="false" includeParams='none'/>"
</#if>
<#if parameters.iconHeight?exists>
iconHeight="<@s.url value='${parameters.iconHeight?html}' encode="false" includeParams='none'/>"
</#if>
<#if parameters.toggleDuration?exists>
toggleDuration=${parameters.toggleDuration?c}
</#if>
<#if parameters.templateCssPath?exists>
templateCssPath="<@s.url value='${parameters.templateCssPath}' encode="false" includeParams='none'/>"
</#if>
<#if parameters.showGrid?exists>
showGrid="${parameters.showGrid?default(true)?string}"
</#if>
<#if parameters.showRootGrid?exists>
showRootGrid="${parameters.showRootGrid?default(true)?string}"
</#if>
<#if parameters.id?exists>
id="${parameters.id?html}"
</#if>
<#if parameters.treeSelectedTopic?exists>
publishSelectionTopic="${parameters.treeSelectedTopic?html}"
</#if>
<#if parameters.treeExpandedTopic?exists>
publishExpandedTopic="${parameters.treeExpandedTopic?html}"
</#if>
<#if parameters.treeCollapsedTopic?exists>
publishCollapsedTopic="${parameters.treeCollapsedTopic?html}"
</#if>
<#if parameters.toggle?exists>
toggle="${parameters.toggle?html}"
</#if>
controller="${parameters.id?html}_controller" <#-- Added by Max -->
>
<#if parameters.label?exists>
<div dojoType="TreeNode" title="${parameters.label?html}"
<#if parameters.nodeIdProperty?exists>
id="${stack.findValue(parameters.nodeIdProperty)}"
<#else>
id="${parameters.id}_root"
</#if>
>
<#elseif parameters.rootNode?exists>
${stack.push(parameters.rootNode)}
<#-- Edited by Max -->
<div dojoType="TreeNode"
title="${stack.findValue(parameters.nodeTitleProperty)}"
widgetId="${stack.findValue(parameters.nodeIdProperty)}"
isFolder="<#if stack.findValue(parameters.childCollectionProperty)?size gt 0>true<#else>false</#if>"
objectId="${stack.findValue(parameters.nameValue)}">
</div>
<#-- End -->
<#assign oldNode = stack.pop()/> <#-- pop the node off of the stack, but don't show it -->
</#if>

清单10 src/template/realajax/tree.ftl

对上述稍作解释，上述代码主要在原版的src/template/ajax/tree.ftl的基础上添加了TreeRPCController的控件，并只输出根节点。由于<s:tree />没有类似nodeObjectIdProperty的属性，所以我用了value属性表示objectId对应的属性名称。

接着新建tree-close.ftl文件，内容和原版的一样，如下所示：

<#if parameters.label?exists></div></#if></div>

清单11 src/template/realajax/tree-close.ftl

再下来就应该是将theme应用到<s:tree />，如下代码所示：

<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<%@ taglib prefix="s" uri="/struts-tags"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Struts 2 AJAX - More Tree</title>
<s:head theme="ajax" debug="true" />
<script type="text/javascript">
/* <![CDATA[ */
function treeNodeSelected(arg) {
alert(arg.source.title + ' selected');
}

dojo.addOnLoad(function() {
var t = dojo.widget.byId('appFiles');
var s = t.selector;
dojo.event.connect(s, 'select', 'treeNodeSelected');
});
/* ]]> */
</script>
</head>
<body>
<h2>
AJAX Tree Example
</h2>
<div style="float:left; margin-right: 50px;">
<s:tree id="appFiles" theme="realajax" rootNode="root"
nodeTitleProperty="name" nodeIdProperty="id"
childCollectionProperty="children" value="absolutePath" />
</div>
</body>
</html>

清单12 WebContent/AjaxTreeTheme.jsp

上述代码中<s:tree />的用法，除了theme改为“realajax”和多了value="absolutePath"外，几乎和静态树中的一样。

为了不影响前一个例子，我们为该JSP文件配置类型相同的Action，如下代码所示：

<action name="AjaxTreeTheme" class="tutorial.AjaxTreeAction">
<result>AjaxTreeTheme.jsp</result>
<result name="ajax" type="freemarker">AjaxTree.ftl</result>
</action>

清单13 src/struts.xml配置片段

发布运行应用程序，在浏览器地址栏中键入http://localhost:8080/Struts2_Ajax2/AjaxTreeTheme.action，结果如图2所示。

总结

通过上述例子，大家知道Struts 2 的AJAX 标志是基于Dojo控件开发的，所以如果大家希望熟练地使用这些标志，最好去了解一下Dojo。

本来还打算介绍一下Struts 2与DWR，不过看看文章的篇幅似乎足够自成一篇了，因此DWR相关的内容要留待下文继续了。


Struts 2与AJAX（第三部分）

很久没有更新BLOG了，前一段时间公司的项目比较忙，另外我还和一位出版社的朋友谈写书的事情，所以一直没有时间，完成《Struts 2与AJAX》。后来写书的事情吹了，趁今天有点空闲就把它完成。

在大家看这部分文章之前，我想对于写书的事情说两句，或者应该叫发牢骚才对。通过这次写书失败的经历，我明白为什么国内的IT书籍多数是滥于充数、粗制滥造、缺乏经典。其实说白了就是一个“钱”字作怪。为了市场，很多编辑可能会“建议”你去“抄考”一些国内相对畅销的同类书籍，例如写Struts就一定要按所谓的MVC进行目录分类，美其名曰“容易入门”。我认为“MVC”的概念虽然重要，但对初学者而言，需要对编程有一定的了解才容易明白此概念。另外，为了“实用”，不惜使用相同的技术重复编写不同的范例。可能是我不太了解读者的心理吧。

言归正传，在上两部分的《Struts 2与AJAX》中我介绍了Struts 2与DOJO结合实现AJAX的知识，本文将介绍在Struts 2中使用DWR实现AJAX表单校验。

什么是DWR

DWR（Direct Web Remoting）是在Java EE中较流行的AJAX框架，它的最大优势就是可以像使用本地的Javascript函数一样，调用服务器上的Java方法。如下图所示：

DWR工作原理
图1 DWR工作原理

其实DWR原理也不复杂，它先在web.xml中配置一个Servlet，映射到特定的路径（通常是%CONTEXT_PATH%/dwr/*）。这个Servlet的作用就是初始化要暴露给Javascript调用的Java类（通过dwr.xml进行配置），并生成相应的代理的Javascript类代码。在XHR请求到来的时候，Servlet负责将请求的参数变成对应的Java对象，并以其为参数调用目标Java方法，并将返回值转化为Javascript代码。详情请参考：http://getahead.ltd.uk/dwr/

Struts 2与DWR

在Struts 2.0.x中使用DWR实现AJAX表单校验。在大家掌握了DWR的原理后，下面我想详细介绍一下实现的步骤。

首先，到以下站点https://dwr.dev.java.net/files/documents/2427/47455/dwr.jar下载DWR的 1.1.4 版本的JAR包。需要注意的是，DWR虽然已经发布2.0版本，但它与1.1.4有很大的区别，所以请大家不要使用2.0版本，否则会出现异常的；

接着，新建WEB工程，将下图所示的JAR包加入到工程的“Build Path”中；

依赖的JAR包
图2 依赖的JAR包

接下来，配置web.xml文件，内容如下：

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_9" version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">

<display-name>Struts 2 AJAX Part 3</display-name>

<filter>
<filter-name>struts-cleanup</filter-name>
<filter-class>
org.apache.struts2.dispatcher.ActionContextCleanUp
</filter-class>
</filter>

<filter-mapping>
<filter-name>struts-cleanup</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<filter>
<filter-name>struts2</filter-name>
<filter-class>
org.apache.struts2.dispatcher.FilterDispatcher
</filter-class>
</filter>

<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 开始DWR配置 -->
<servlet>
<servlet-name>dwr</servlet-name>
<servlet-class>uk.ltd.getahead.dwr.DWRServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>true</param-value>
</init-param>
</servlet>

<servlet-mapping>
<servlet-name>dwr</servlet-name>
<url-pattern>/dwr/*</url-pattern>
</servlet-mapping>
<!-- 结束DWR配置 -->
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>

</web-app>

清单1 WebContent/WEB-INF/web.xml

然后是DWR的配置文件：

<?xml version="1.0" encoding="UTF-8"?>

<!-- START SNIPPET: dwr -->
<!DOCTYPE dwr PUBLIC
"-//GetAhead Limited//DTD Direct Web Remoting 1.0//EN"
"http://www.getahead.ltd.uk/dwr/dwr10.dtd">

<dwr>
<allow>
<create creator="new" javascript="validator">
<param name="class" value="org.apache.struts2.validators.DWRValidator"/>
</create>
<convert converter="bean" match="com.opensymphony.xwork2.ValidationAwareSupport"/>
</allow>

<signatures>
<![CDATA[
import java.util.Map;
import org.apache.struts2.validators.DWRValidator;

DWRValidator.doPost(String, String, Map<String, String>);
]]>
</signatures>
</dwr>
<!-- END SNIPPET: dwr -->

清单2 WebContent/WEB-INF/dwr.xml

通过以上配置，我们可以将DWRValidator中的方法暴露为Javascript可以调用的远程接口。

在正确完成以上步骤之后，我们发布运行一下应用程序，在浏览器地址栏中输入http://localhost:8080/Struts2_Ajax3/dwr/，应该会出现如下页面：

DWR Servlet默认输出页面
图3 DWR Servlet默认输出页面

接下来，我们要开始编写Action类了，代码如下：

package tutorial;

import com.opensymphony.xwork2.ActionSupport;

public class AjaxValidation extends ActionSupport [image] {
private static final long serialVersionUID = -7901311649275887920L;

private String name;
private String password;
private int age;

public int getAge() [image] {
return age;
}

public void setAge(int age) [image] {
this.age = age;
}

public String getName() [image] {
return name;
}

public void setName(String name) [image] {
this.name = name;
}

public String getPassword() [image] {
return password;
}

public void setPassword(String password) [image] {
this.password = password;
}

@Override
public String execute() [image] {
return SUCCESS;
}
}

清单3 src/tutorial/AjaxValidation.java

上述代码一目了然，相信大家已经很熟悉了。下面，我们再来看看表单校验的配置代码：

<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator 1.0.2//EN" "http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
<validators>
<validator type="regex">
<param name="fieldName">password</param>
<param name="expression">
<![CDATA[(?!^[0-9]*$)(?!^[a-zA-Z]*$)^([a-zA-Z0-9]{8,10})$]]>
</param>
<message>Password must be between 8 and 10 characters, contain at least one digit and one alphabetic character, and must not contain special characters</message>
</validator>
<field name="name">
<field-validator type="requiredstring">
<message>You must enter a name</message>
</field-validator>
</field>
<field name="age">
<field-validator type="int">
<param name="min">18</param>
<param name="max">127</param>
<message>Age must be between 18 and 127</message>
</field-validator>
</field>
</validators>

清单4 src/tutorial/AjaxValidation-validation.xml

对于AjaxValidation类的name、password和age三个字段，我分别用了非空、正规表达式和范围验证。正规表达式(?!^[0-9]*$)(?!^[a-zA-Z]*$)^([a-zA-Z0-9]{8,10})$的作用是保证密码由至少包括一个数字和一个字母，且不能含有符号的长度为8到10的字符串组成。它也是所谓强密码（Strong Password）的普通实现。

接下来的是JSP的代码，内容如下：

<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<%@ taglib prefix="s" uri="/struts-tags"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Struts 2 AJAX - Validation</title>
<s:head theme="ajax" />
</head>
<body>
<h2>
AJAX Validation Using DWR
</h2>
<s:form method="post" validate="true" theme="ajax">
<s:textfield label="Name" name="name" />
<s:password label="Password" name="password" />
<s:textfield label="Age" name="age" />
<s:submit />
</s:form>
</body>
</html>

清单5 WebContent/AjaxValidation.jsp

以上代码也不复杂，不过需要的是注意的是除了要加入<s:head theme="ajax" />外，<s:form />也必须加入validate="true" theme="ajax"的属性。

最后是Struts 2的配置文件，内容如下所示：

<?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>
<package name="Struts2_AJAX_DEMO" extends="struts-default">
<action name="AjaxValidation" class="tutorial.AjaxValidation">
<result name="input">AjaxValidation.jsp</result>
<result>AjaxValidation.jsp</result>
</action>
</package>
</struts>

清单6 src/struts.xml

最后发布运应用程序，在浏览器地址栏中输入http://localhost:8080/Struts2_Ajax3/AjaxValidation!input.action出现如下图所示页面：

AjaxValidation页面输出
图4 AjaxValidation页面输出

在文本框中输入错误的值使页面出现错误提示信息，如下图所示：

AjaxValidation页面错误提示
图5 AjaxValidation页面错误提示

可能有朋友会问怎么知道这是通过AJAX进行校验的呢？在这里我向大家推荐一个AJAX开发必备的工具——Firebug。Firebug是Firefox的一个功能强大的插件，它可以准确地输出和定位Javascript的错误、通过直观的方式查看HTML文档的DOM及其样式、所见即所得的编辑方式，更值得一赞的是它可以方便地对Javascript进行跟踪和调试，如果你希望这进一步了解这个工具，请安装Firefox 2.0以上版本，并使用它浏览以下网址http://www.getfirebug.com。

在安装完成Firebug之后，在Firefox中打开http://localhost:8080/Struts2_Ajax3/AjaxValidation!input.action，按“F12”键找开Firebug窗口，如果你第一次使用Firebug，请点击其窗口中的链接“Enable Firebug”激活插件。之后，点击“Net”，并在出现的菜单中点击选中“XHR”。然后将光标移入文本框，再将光标移出使文本框失去焦点，你可以看到Firebug窗口会多出一项记录，如下图所示：

Firebug中查看XHR请求
图6 Firebug中查看XHR请求

这就证明你在文本框失去焦出时，Struts 2会发送XHR请求到服务器以对该文本框值进行校验。有兴趣的朋友可以通过Firebug，研究XHR的请求与响应，这样可以加深对DWR工作原理的理解。

何时使用AJAX表单校验

虽然在Struts 2实现AJAX表单校验是一件非常简单的事，但我建议大家不要在所有的场合都使用这个功能，原因可以分为以下几个方面：

1. AJAX校验在服务器上进行数据校验，可能会比较耗时；

2. AJAX校验可能会过于频繁，加重服务器的负载；

3. 一些普通的校验，只需要使用纯Javascript便可以实现。

读到这里，有的朋友可能会问：“那么什么时候才应该使用AJAX表单校验呢？”答案其实很简单，当我们的校验在页面加载时还不能够确定的情况下，就应该使用这个功能。例如，注册用户时，校验用户名是否已经存在；或者校验涉及过多的页务逻辑等。

现在让我们来改造一下上述例子，对于name我们可以使用AJAX校验，但对于其它的字段应该使用纯Javascript的校验。

在tutorial.AjaxValidation类加入如下方法：

@Override
public void validate() [image] {
Set<String> users = new HashSet<String>();
users.add("max");
users.add("scott");
if(users.contains(name)) [image] {
addFieldError("name", "The user name has been used!");
}
}

清单7 src/tutorial/AjaxValidation.java代码片段

用于模拟用户注册的场境，当然在真实情况应该在数据库中检查用户是否存在。

接下来再修改JSP文件，将<s:form />里面的内容改为如下所示代码：

<s:form method="post" validate="true" theme="ajax_xhtml">
<s:textfield label="Name" name="name" theme="ajax" />
<s:password label="Password" name="password" theme="xhtml" />
<s:textfield label="Age" name="age" theme="xhtml" />
<s:submit theme="xhtml" />
</s:form>

清单8 WebContent/AjaxValidation.jsp代码片段

对比早前的JSP代码，大家可以看出我将<s:form />的theme改成了“ajax_xhtml”，这个theme不是Struts 2自带，需要自定义。另外，除了Name使用了ajax的theme之外，其它的表单标签的theme都为xhtml，如此一来便可以实现只有当Name文本框失去焦点时才发生AJAX表单校验。

接下来，应该是我们的自定义ajax_xhtml的theme了。在源代码文件夹下新建包“template.ajax_xhtml”，然后在其中加入form.ftl和form-close.ftl文件，内容分别如下：

<#if parameters.validate?exists>
<script type="text/javascript" src="${base}/struts/validationClient.js"></script>
<script type="text/javascript" src="${base}/dwr/interface/validator.js"></script>
<script type="text/javascript" src="${base}/dwr/engine.js"></script>
<script type="text/javascript" src="${base}/struts/ajax/validation.js"></script>
</#if>
<#include "/${parameters.templateDir}/xhtml/form-validate.ftl" />
<#include "/${parameters.templateDir}/simple/form.ftl" />
<#include "/${parameters.templateDir}/xhtml/control.ftl" />

清单9 src/template/ajax_xhtml/form.ftl

上述的文件与xhtml theme中的form.ftl文件相似，我只是加入了AJAX表单校验所用的Javascript库，以便theme为ajax的表单标签使用。

<#include "/${parameters.templateDir}/xhtml/control-close.ftl" />
<#include "/${parameters.templateDir}/simple/form-close.ftl" />
<#include "/${parameters.templateDir}/xhtml/form-close-validate.ftl" />

清单10 src/template/ajax_xhtml/form-close.ftl

这个文件与xhtml theme中的form-close.ftl文件相同。

最后发布运行应用程序，大家可以发现在Password与Age的校验，只有在表单提交时才发生，而且是纯Javascript的校验。不过，以上代码还不是很完善，在行为上有些BUG。

总结

Struts 2相比一些其它的框架，在实现AJAX方面的确简单很多。更激动人心的是Struts 2的标签库支持基于模板的输出，使得开发者可以跟据自身的需要方便地改变标签的行为。

在将要发布的Struts 2.1版本中，AJAX表单校验将不再使用DWR，统一使用DOJO实现，详情请参考：http://struts.apache.org/2.0.9/docs/ajax-validation.html。


Struts2中用Spring实现IoC

2007年7月8日

最近一直通过WebWork in Action来学习Struts2，发现WebWork中相关的IoC容器类已经被删去了。原来由于使用Spring的IoC容器的用户更多，所以从WebWork2.2开始，他们就删去了自己的IoC功能，转而支持Spring。Struts2也继承了这一特点。

Spring的IoC是一种资源管理的方法，有助于将具体实现和接口分离，从而实现解耦合。当实现接口的类改变时，不需要在其他类中修改相关接口的初始化，只要修改一个XML文件即可。

要在Struts中使用IoC，我们需要导入以下类库

  • struts2-core-2.0.8.jar
  • xwork-2.0.3.jar
  • struts2-spring-plugin-2.0.8.jar
  • spring-beans.jar
  • spring-context.jar
  • spring-core.jar
  • spring-web.jar

首先修改web.xml文件，增加一个监听器：

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

接下来可以创建一个我们需要功能的接口

package com.classicning.struts2.service;

import com.classicning.struts2.role.User;

public interface ILoginService {

public boolean login(User user);

}

这个接口定义了一个布尔型的登录验证功能，登录验证成功返回true。

接下来创建一个默认的实现，它通过与一组因硬编码的信息对比验证用户信息。（这个显然是不合适的，这么做只是举一个简短的例子而已）

package com.classicning.struts2.service;

import com.classicning.struts2.role.User;

public class LoginServiceHardCoded implements ILoginService{
@Override
public boolean login(User user){
boolean loginable = false;
if (user.getUsername().equalsIgnoreCase("admin") && user.getPassword.equals("tomcat123")){
loginable = true;
}
return loginable;
}

}

创建LoginAction，使用这个LoginService的功能。这里提供了setILoginService方法，IoC容器将会调用这个方法，将适当的实现类注入。因此LoginAction中就不需要声明ILoginService的实现，当LoginService的实现改变时（比如变成LoginServiceXML，LoginServiceDB），就不需要对LoginAction进行任何修改。

package com.classicning.struts2.actions;

import com.opensymphony.xwork2.ActionSupport;

import com.classicning.struts2.service.ILoginService;

public class LoginAction extends ActionSupport {

private static final long serialVersionUID = 1L;
private String message;
private User user;
private ILoginService service;

@Override
public String execute(){

if(!service.login(user)){
addFieldError("user.password",getText("login.wrong"));
return LOGIN;
}

message = getText("login.hello");
message += getText("login.showname")+":"+user.getUsername();
return SUCCESS;
}

public String getMessage() {
return message;
}

public void setService(ILoginService service){
this.service = service;
}

public void setUser(User user){
this.user = user;
}

}

在WEB-INF中创建applicationContext.xml，进行配置

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns ="http://www.springframework.org/schema/beans"
xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd" >

<bean id="loginService" class="com.classicning.struts2.service.LoginServiceHardCoded"/>

<bean id="loginAction" class="com.classicning.struts2.actions.LoginAction" scope="prototype">
<property name="service">
<ref local="loginService"/>
</property>
</bean>
</beans>

loginAction的scope被配置为prototype，在每个请求中都要创建新实例；而loginService为默认设置，将仅保存一个实例。

最后配置Struts.xml，配置action

<?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>
<package name="com.classicning.struts2" extends="struts-default" namespace="/">
<action name="login" class="loginAction">
<result name="success">hello.jsp</result>
<result name="input">index.jsp</result>
</action>
</package>

</struts>

唯一需要注意的是class属性中，需要填入的是在spring的配置中bean的id，而不是类的位置。

这样就可以进行测试了。而在关于是否要修改properties文件的问题上，其实在struts2-spring-plugin-2.0.8.jar的struts-plugin.xml中已经包含了这样一句：

<constant name="struts.objectFactory" value="spring" />

因此也就无需重复声明了。


关键字: Struts Struts2 COC

摘要：介绍Struts2中的零配置（Zero Configuration），以及如何用COC来更好地简化Struts2的配置。在第一章，我使用Maven来创建一个起点项目；第二章，以该项目为例，讲解如何使用Struts2的零配置；第三章，论述第二章中的实现方式的缺陷，然后讲解如何使用COC来改进这些缺陷，并进一步简化Struts2的配置。附件是这篇文章用到的示例代码。

一、从零开始

这里，我将建立一个新的示例项目，作为讲解的起点。我使用JDK 6、Maven 2、Eclipse 3.3来建立这个示例，如果读者对Maven2不熟也没关系，这只是个示例。
首先，运行下边的命令：
mvn archetype:create -DgroupId=demo.struts -DartifactId=demo-struts-coc -DarchetypeArtifactId=maven-archetype-webapp
这会建立如下的目录结构：
|- POM.xml
|- src
|- main
|- resources
|- webapp
|- index.jsp
|- WEB-INF
|- web.xml
然后我们在src/main目录下新建一个名为java的目录，用来放置java代码。在src下建立test目录，并在test目录下建立java目录，用来放置测试代码。另外，我这个示例不想使用JSP，所以我将src/main/webapp目录下的index.jsp改为index.html。
现在，需要配置该项目要用到哪些lib。在POM.xml中加入struts2-core：

xml 代码

  1. <dependency>
  2. <groupId>org.apache.struts</groupId>
  3. <artifactId>struts2-core</artifactId>
  4. <version>2.0.9</version>
  5. </dependency>

另外，我想在Eclipse里使用jetty来启动项目并进行测试，所以在POM.xml中再加入jetty、jetty-util、servlet-api等的依赖，详情见附件。
我希望使用Eclipse来作为这个项目的IDE，所以，我在命令行状态下，进入这个项目所在的目录，运行：
mvn eclipse:eclipse
然后使用Eclipse导入这个项目。如果你是第一次用Eclipse导入用Maven生成的项目，那你需要在Eclipse里配置一个名叫M2_REPO的Variable，指向你的Maven 2的repository目录。缺省情况下，它应该位于${user.home}/.m2/repository。
OK！现在我们已经可以在Eclipse中进行工作了。
修改src/main/webapp/WEB-INF/web.xml，加入struts2的FilterDispatcher并设置filter-mapping。在这个示例中我将url-pattern设为"/app/*"，也就是说，url的匹配是基于路径来做的。这只是我的个人喜好而已，你也可以将它设成"*"。
既然是在讲struts2的零配置，当然是可以不要任何配置文件的。但是为了更好地进行“配置”，我还是建立了struts.xml文件（在src/main/resources目录下）。我不喜欢url最后都有个action后缀，现在，我在struts.xml中配置struts.action.extension，将这个后缀去掉：

xml 代码

  1. <struts>
  2. <constant name="struts.action.extension" value="" />
  3. </struts>

然后我在src/test/java下建立demo/RunJetty.java文件，main方法如下：

java 代码

  1. public static void main(String[] args) throws Exception {
  2. Server server = new Server(8080); //也可以改成其它端口
  3. File rootDir = new File(RunJetty.class.getResource("/").getPath()).getParentFile().getParentFile();
  4. String webAppPath = new File(rootDir, "src/main/webapp").getPath();
  5. new WebAppContext(server, webAppPath, "/");
  6. server.start();
  7. }

现在，在Eclipse里运行或调试这个RunJetty.java，用浏览器打开http://localhost:8080/看看吧。如果不出问题，应该可以访问到webapp目录下的index.html了。有了Jetty，你还在用MyEclipse或其它插件么？

二、零配置

首先要澄清一点，这里说的零配置并不是一点配置都没有，只是说配置很少而已。
Struts2（我只用过Struts 2.0.6和2.0.9，不清楚其它版本是否支持零配置）引入了零配置的新特性，元数据可以通过规则和注解来表达：A "Zero Configuration" Struts application or plugin uses no additional XML or properties files. Metadata is expressed through convention and annotation.
目前，这个新特性还在测试阶段，但经过一段时间的使用，我觉得这个特性已经可用。下面我讲一下如何使用它。
1. Actions的定位
以前需要在xml配置文件中配置Action的name和class，如果使用零配置，所带来的一个问题就是如何定位这些Action。我们需要在web.xml中找到struts2的filter的配置，增加一个名为actionPackages的init-param，它的值是一个以逗号分隔的Java包名列表，比如：demo.actions1,demo.actions2。struts2将会扫描这些包（包括这些包下边的子包），在这些包下，所有实现了Action接口的或者是类名以“Action”结尾的类都会被检查到，并被当做Action。
以前，我们写Action必须要实现Action接口或者继承ActionSupport。但是，上面提到的类名以"Action"结尾的类并不需要这样做，它可以是一个POJO，Struts2支持POJO Action！
下面是actionPackages的配置示例：

xml 代码

  1. <filter>
  2. <filter-name>struts2</filter-name>
  3. <filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
  4. <init-param>
  5. <param-name>actionPackages</param-name>
  6. <param-value>demo.actions1,demo.actions2</param-value>
  7. </init-param>
  8. </filter>

2. 示例
现在我们建立demo.actions1.app.person和demo.actions2.app.group两个包，在demo.actions1.app.person包下建立ListPeopleAction.java，在demo.actions2.app.group下建立ListGroupAction.java。作为示例，这两个类只是包含一个execute方法，返回"success"或"error"，其它什么都不做：

java 代码

  1. public String execute() {
  2. return "success";
  3. }

在Filter的配置中，我指定actionPackages为demo.actions1,demo.actions2，当系统启动时，Struts2就会在这两个包下扫描到demo.actions1.app.person.ListPeopleAction和demo.actions2.app.group.ListGroupAction。

3. Action and Package name
Struts2扫描到Action后，从actionPackages指定的包开始，子包名会成为这个Action的namespace，而Action的name则由这个Action的类名决定。将类名首字母小写，如果类名以Action结尾，则去掉"Action"后缀，形成的名字就是这个Action的名字。在如上所述的示例中，actionPackages指定为demo.actions1,demo.actions2，那么你可以这样访问demo.actions1.app.person.ListPeopleAction：
http://localhost:8080/app/person/listPeople

4. Results
Struts2是通过"Result"和"Results"两个类级别的annotations来指定Results的。
作为示例，我们在webapp目录下建两个html文件：success.html和error.html，随便写点什么内容都可以。现在假设我们访问/app/person/listPeople时，或Action返回success就转到success.html页面，若是error就转到error.html页面，这只需要在ListPeopleAction类上加上一段注解就可以了：

java 代码

  1. @Results({
  2. @Result(name="success", type=NullResult.class, value = "/success.html", params = {}),
  3. @Result(name="error", type=NullResult.class, value = "/error.html", params = {})
  4. })
  5. public class ListPeopleAction {
  6. public String execute() {
  7. return "success";
  8. }
  9. }

同上，我们给ListGroupAction也加上注解。
现在，我们已经完成了一个零配置的示例。我们并没有在xml文件里配置ListPeopleAction和ListGroupAction，但它们已经可以工作了！
用Eclipse运行RunJetty，然后用浏览器访问http://localhost:8080/app/person/listPeople和http://localhost:8080/app/group/listGroup看看，是不是正是success.html（或error.html）的内容？

5. Namespaces
如上所述，namespace由包名所形成，但我们可以使用"Namespace"注解来自己指定namespace。

6. Parent Package
这个配置用得较少。Struts2提供一个"ParentPackage"注解来标识Action应该是属于哪个package。

三、使用COC

如上所述，Struts2用注解来实现零配置。然而，这不是我喜欢的方式。在我看来，这不过是将配置从XML格式换成了注解方式，并不是真的零配置。而且，这种方式也未必比XML形式的配置更好。另外，对元数据的修改必然会导致项目的重新编译和部署。还有，现在的Struts2版本似乎对Result注解中的params的处理有些问题。
其实，Struts2的actionPackages配置已经使用了COC，那为什么不能为Results也实现COC，从而去除这些每个Action都要写的注解？
在严谨的项目中，package、action的名称和页面的路径、名称一定存在着某种关系。比如，页面的路径可能和package是对应的，页面的名称可能和action的名称是对应的，或是根据某种法则运算得到。我们知道webwork2和struts2有个配置叫global-results。我们为什么不能根据这些对应规则写个Result，将它配到global-results中，从而真正免去result的配置？
事实上，我推荐Struts2的使用者只用Struts2输出XML或JSON，放弃UI，页面这层还是使用标准的HTML、CSS和一些JS组件来展现。许多人反映Struts2慢，确实，Struts2是慢，很慢！慢在哪儿？很大一部分因素是UI这层引起的，特别是使用了过多的Struts2的tag，并使用了ajax theme。但是，如果我们放弃了Struts2的笨拙的UI，Result只输出XML或JSON，UI则使用标准的HTML+CSS，使用JS组件（DOJO、Adobe Spry Framework、YUI-Ext等）来操作Struts2的输出数据，情况将会如何？我们会得到一个高性能、高可配的、UI和应用服务器的职责分割更为明确、合理的、更易于静态化部署的开发组合。
这似乎是阉割了Struts2，但是这样阉割过的Struts2摆脱了性能低下的包袱，更轻、更现代化。
有些扯远了，言归正传，不管是让Struts2输出XML或JSON，还是输出页面，我们都有办法根据项目的规则写一个Result，将它配到global-results中，从而大大减少Result的配置。
假设我们让Struts2只输出JSON，有个jsonplugin可以做这件事。使用JsonResult时，不再需要知道页面的位置、名称等信息，它仅仅是数据输出，那么我们就可以将这个Result配成全局的，大部分Action将不再需要Result的配置。
作为示例，我假设我的例子中输出的两个html页面（success.html和error.html）是JSON，我们看看怎么免去我例子中的两个Action的Result注解。
首先，我们删去ListPeopleAction和ListGroupAction两个Action的注解，并修改struts.xml文件，加入：

xml 代码

  1. <package name="demo-default" extends="struts-default">
  2. <global-results>
  3. <result name="success">/success.html</result>
  4. </global-results>
  5. </package>

请记住这只是一个示例，为了方便，我没在项目中加入jsonplugin来作真实的演示，我只是假设这个success是json输出，读者可以自行使用jsonplugin来作实验。

现在，离成功不远了，但是项目仍然不能正常运行。我们的Action返回success，但并不会匹配到global-results中配置。为什么呢？因为，我们这里是把global-results配置到"demo-default"这个package下的，而Struts2根据actionPackages找到的Action不会匹配到这个package上。解决办法也很简单，还记得上面讲到的Parent Package吧？给Action加个注解，指定ParentPackage为"demo-default"。但这样可不是我喜欢的，其实有更好的办法，我们在struts.xml中加个constant就好了：

xml 代码

  1. <constant name="struts.configuration.classpath.defaultParentPackage" value="demo-default" />

现在，大功告成！运行RunJetty来测试下吧！你可以访问/app/person/listPeople，可以访问/app/group/listGroup，而所有的配置仅仅是web.xml和struts.xml中的几行，我们的Java代码中也没有加注解。如果再加上几百个Action呢？配置仍然就这几行。
可是，某些Action确实需要配置怎么办？对这些Action，你可以加注解，也可以针对这些Action来写些XML配置。一个项目中，大部分Action的配置是可以遵从一定规则的，可以使用规则来简化配置，只有少部分需要配置，这就是COC。

注：附件demo-struts-annotations.zip是使用注解实现零配置的示例代码，附件demo-struts-coc.zip是使用global-results后的示例代码。
另外，我以前写过一篇文章《改写Restful2ActionMapper让Struts2支持REST风格的URL映射》，这里所说的零配置并不适用于支持REST。也就是说，你要用REST风格的URL映射，你就必须配置。不过还好，使用REST风格后，配置并不复杂。
另外我见到不少人使用Spring来配置和管理Action，其实完全没有必要！设置struts.objectFactory 等于spring就可以了，如果在Action中有setService1，这个service1在Spring中有配置的话，它会自动注入的。Javaeye论坛中早有相关的讨论。


本文通过展示3种链接操作来说明如何使用标签。

author: ZJ 07-6-4

Blog: http://zhangjunhd.blog.51cto.com/

Web应用区别于普通的站点之处在于Web应用可以创建一个动态的响应。为了使从一个页面中链接一个动态数据变得简单，Struts2框架提供了一系列的标签。

Struts标签的一种用法是创建链接到其他Web资源，特别是针对那些在本地应用中的资源。

1．普通链接

Web程序中最普通的应用是链接到其他页面，下面看Welcome.jsp。

<%@ page contentType="text/html; charset=UTF-8" %>

<%@ taglib prefix="s" uri="/struts-tags" %>

<html>

<head>

<title>Welcome</title>

<link href="<s:url value="/css/tutorial.css"/>" rel="stylesheet"

type="text/css"/>

</head>

<body>

<h3>Commands</h3>

<ul>

<li><a href="<s:url action="Login_input"/>">Sign On</a></li>

<li><a href="<s:url action="Register"/>">Register</a></li>

</ul>

</body>

</html>

1.1说明

1．<%@ taglib prefix="s" uri="/struts-tags" %>

此句表示导入struts标签，并以s为前缀。即以s为前缀的标签均来自struts标签库。

2．<link href="<s:url
value="/css/tutorial.css"/>" rel="stylesheet"
type="text/css"/>

此句表示利用url标签导入一个路径，链接到一个文件，注意此路径为项目下的绝对路径。

3．<a href="<s:url action="Login_input"/>">Sign On</a>

此句表示利用url标签链接到一个action。

1.2注册action

我们在struts.xml中注册一个action来显示welcome.jsp。

<action name="Welcome">

<result>/example/Welcome.jsp</result>

</action>

注意此action注册在package example下，所以在地址栏中敲入http://localhost:8080/StrutsHelloWorld/example/Welcome.action（StrutsHelloWorld是project名），显示如下。

2．使用通配符

对于上面的action注册，我们也可以用下面的语句代替。

<action name="*">

<result>/example/{1}.jsp</result>

</action>

此句的意思是，如果在没有找到匹配的action名称的情况下，默认调用action名称.jsp。第一句中星号指任意，而第二句中{1}指代第一句中星号指代的内容。

举个例子，如果在地址栏中敲入http://localhost:8080/StrutsHelloWorld/example/1.action，则系统查找struts.xml，发现没有name为1的action，即最后调用name为星号的这个action，根据此action，将输出/example/1.jsp。

或者读者可以直接点击Welcome.jsp中的两个超链接，系统将会报错找不到Login_input.jsp和Register.jsp。因为这两个action还没有注册，也没有相应的jsp文件。

3．带参数的链接

超链接后面带有参数大家不会陌生，诸如www.apache.com?language=ch。这个链接后面带有一个language参数，其值为ch。你可以通过request.getParameter(“language”)找到参数值。下面演示在struts2中如何设置带参数的链接。看HelloWorld.jsp。

<%@ taglib prefix="s" uri="/struts-tags"%>

<html>

<head>

<title>Hello World!</title>

</head>

<body>

<h2><s:property value="message" /></h2>

<h3>Languages</h3>

<ul>

<li>

<s:url id="url" action="HelloWorld">

<s:param name="request_locale">en</s:param>

</s:url>

<s:a href="%{url}">English</s:a>

</li>

<li>

<s:url id="url" action="HelloWorld">

<s:param name="request_locale">es</s:param>

</s:url>

<s:a href="%{url}">Espanol</s:a>

</li>

</ul>

</body>

</html>

3.1说明

1．<s:url id="url" action="HelloWorld">

<s:param name="request_locale">en</s:param>

</s:url>

此段表示设置一个url标签指向名为HelloWorld的action，此标签带一个id取名为url，后面会用到。带一个参数request_locale，其值为en。

2．<s:a href="%{url}">English</s:a>

此句用到了struts的超链接标签，连接的地址即为1中url，效果如下图所示。点击English，发出的信息为：http://localhost:8080/StrutsHelloWorld/example/HelloWorld.action？request_locale=en

3.2注册action到struts.xml

<struts>

<package name="example" namespace="/example"

extends="struts-default">

<action name="HelloWorld" >

<result>/example/HelloWorld.jsp</result>

</action>

... ...

出自 51CTO.COM博客


1、Struts、Webwork、Struts2比较
在JAVA EE中作为MVC的优秀框架有很多，例如：常见的有：Struts、WebWork、Tapestry、JSF等，在这些优秀的框架之中，个人比较习惯使的还是：Struts和WebWork，对于JSF使的不多，虽然SUN公司主推JSF，但观察其身边的搞开发的朋友，使用此技术的确实是少之又少，Tapestry也是apache公司推出的，但个人认为是极其难使的，而且版本之间的差别较大，文档也不是很多，使用较费劲，而对于Struts而言，相信不少人都清楚，毕竟Struts已经成功的成为了MVC框架的一个标准，如果不去搞Struts，那么连入这个行业都很难。WebWork作为XWork的一个新的发展物，在JIVE论坛上取得了巨大的成功，拌着时间的发展，WebWork使用会越来越多，发展潜力较大。
1.1、Struts
Struts的推出时间，在网上查了查确实没有查到，在我印象中应该是03年接触到Struts概念的，一开始只是在使用简单的MVC进行开发，Struts刚出现的时候确实比较麻烦，工具也很难找到，一下子就来了这么多的JAR包，配置文件也很难写，觉得这不像是个技术，不过慢慢的使使也就习惯了，但写到最后发现在Struts里编写那个ActionForm真是费劲，一开始觉得这样做确实有好处，但是慢慢的发现里面的代码要一下子写好多，所以开始有些厌烦Struts了，但是Struts依然还是有很多好处的，例如：
• 在JSP页面里提供了丰富的标签，可以直接输出List中的内容，也可以通过<bean>标签访问里面类里的属性
• 通过Struts的标签前台的Scriptlet减少了很多，页面看起来很舒服（不过在不会的人眼中，这样的页面简直是一种折磨）
在Struts 1.2之中，Struts有了一些改变，其中我个人认为比较好用的就是加入了DispatcherAction的操作，这样在一个Action上处理多种情况变的很简单，用户不用像最早那样需要通过一个参数来判断到底使用的那种操作了，在Struts 1.2之后我开始使用验证框架了，但是没用它做过什么项目，验证框架确实很大的提供了方便，但个人感觉其根本还是在补救Struts中的ActionForm，所以说ActionForm是Struts中天生的软肋。

1.2、Webwork2

在Java中JIVE论坛的大名算是赫赫有名的，在JIVE论坛中就使用了WebWork技术，也正是因为WebWork在JIVE论坛中的应用，才使得Webwork框架被世人所注意。
在WebWork之中，没有了Struts中的ActionForm，相反的提出了许多Interceptor的概念，增加了很多的拦截器，方便用户对输入信息进行验证，开发人员也没有必要再去编写各种烦人的验证代码了。

1.3、Struts2

编辑web.xml文件，在文件之中配置Struts2的应用

web.xml：
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<filter>
<filter-name>struts2</filter-name>
<filter-class>
org.apache.struts2.dispatcher.FilterDispatcher
</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>

建立新的Action，所有的Action继承自ActionSupport类

HelloAction.java：
package org.lxh.struts2;

import com.opensymphony.xwork2.ActionSupport;

public class HelloAction extends ActionSupport {
private static final String MESSAGE = "hello.msg";

private String message;

@Override
public String execute() throws Exception {
this.setMessage(super.getText(MESSAGE));
return ActionSupport.SUCCESS;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}

新建struts.xml文件

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>
<package name="example"
extends="struts-default">
<action name="HelloWorld" class="org.lxh.struts2.HelloAction">
<result name="success">/hello.jsp</result>
</action>
</package>
</struts>

新建package.properties文件

package.properties：
hello.msg = HELLO WORLD!!!

建立hello.jsp

hello.jsp：
<%@ page language="java" pageEncoding="GBK"%>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<title>My JSP 'hello.jsp' starting page</title>
</head>

<body>
<h2><s:property value="message"/></h2>
</body>
</html>

注意：
1、 如果用的是Tomcat5.0.28.拷贝xalan目录下的xercesImpl.jar和xml-apis.jar替换掉Tomcat\common\endorsed下的两个jar文件。
2、 还需要将xalan下的xalan.jar和serializer.jar拷贝到Tomcat\common\lib中

建立WEB项目

将lib目录下的以下*.jar文件拷贝到WEB项目之中。
• struts2-core.jar
• xwork.jar
• ognl.jar
• freemarker.jar
• commons-logging.jar


Struts2学习进阶
——序言

最近开始学习Struts2，为记录Struts2的学习过程，特写下《Struts2学习进阶》，在该系列文章中，开头的几篇为翻译struts2自带教程的文章，根据在学习过程中的一些问题会有所调整。

一． 环境说明

1． Struts版本：2.0.8

下载地址：http://struts.apache.org/download.cgi

笔者下载的是struts-2.0.8-all

2． JDK版本：JDK1.5

3． Tomat版本：5.5.28

4． MySql版本：5.0

5． Eclipse版本：3.2

6． MyEclipse版本：5.1.1

除无特别说明，笔者的程序都基于以上环境。

二． 其它说明

1． 在建立Struts2的Web工程中，需将struts-2.0.8\lib下的各包放入web工程的lib目录下；

2． 该学习进阶文档为作者自学时学习记录，与大家共享之，也愿各位同仁提出宝贵意见；

3． 有疑问可留言或发Email至：xiexingxing1121@126.com。

望与大家同进步。


说明：本文翻译自struts-2

说明：本文翻译自struts-2.0.8-all\struts-2.0.8\docs\docs\hello-world.html，并根据笔者在学习过程中的实践增加了部分内容。

当你提交一个HTML表单到这个框架中的时候，输入并不是被发送到服务页，而是被发送到你提供的Java类，这些类被称为Action。在这些Action执行完后，选择某一个资源来呈现返回结果，这个资源一般是页面，但也可以是PDF文件，或者是Excel文件，亦或是Java applet窗口。

假设你想要创建一个“Hello World”的例子来呈现欢迎信息，在你准备好开发环境后，为了创建一个“Hello World”的例子，你需要做如下三件事情：

1. 创建一个jsp页面来呈现欢迎信息；

2. 创建一个Action类来创建信息；

3. 在配置文件中配置action和页面的映射关系。

注意：为了创建这个组件，我们将工作流分成几乎无人不晓的三部分：视图、模型和控制器。分离这三部分的原因是当系统变得越来月复杂的时候，我们能够更好的管理。

一． 准备工作

建立web工程，其中工程名为tutorial，在WebRoot下引入struts2的lib下的如下4个包：

commons-logging-1.1.jar

freemarker-2.3.8.jar

ognl-2.6.11.jar

struts2-core-2.0.8.jar

xwork-2.0.3.jar

在web.xml文件中增加struts2的FilterDispatcher，修改后的web.xml如下：

<?xml version="1.0" encoding="UTF-8"?>

<web-app id="WebApp_9" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>tutorial</display-name>
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>

<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>

二． 代码

首先，我们需要一个jsp页面来呈现信息，HelloWorld.jsp页面代码如下所示：

<% [image] @ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<title>Hello World!</title>
</head>
<body>
<h2><s:property value="message" /></h2>
</body>
</html>

接着，我们需要一个Action类来创建信息，代码如下：

package tutorial;
import com.opensymphony.xwork2.ActionSupport;
public class HelloWorld extends ActionSupport [image] {
public static final String MESSAGE = "Struts is up and running [image] ";
public String execute() throws Exception [image] {
setMessage(MESSAGE);
return SUCCESS;
}
private String message;
public void setMessage(String message) [image] {
this.message = message;
}
public String getMessage() [image] {
return message;
}
}

第三步，我们需要在配置文件中进行相应配置来将两者联系起来。让我们编辑src下的struts.xml文件，该文件内容如下：

<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
"http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
<package name="tutorial" extends="struts-default">
<action name="HelloWorld" class="tutorial.HelloWorld">
<result>/HelloWorld.jsp</result>
</action>
<!-- Add your actions here -->
</package>
</struts>

代码已经完成，让我们来尝试运行它。部署该应用程序并打开http://localhost:8080/tutorial/HelloWorld.action，我们可以看到标题栏为：Hello World，并且显示“Struts is up and running!”的页面呈现在我们面前。

三． 代码是如何工作的？

你的浏览器向Web服务器发送了的http://localhost:8080/tutorial/HelloWorld.action（注：tutorial为部署在Tomcat中的的项目名称）的URL请求后，服务器做了如下工作：

1. 容器接收到了Web服务器对资源HelloWorld.action的请求，根据web.xml中的配置，服务器将包含有.action后缀的请求转到org.apache.struts2.dispatcher.FilterDispatcher类进行处理。这个FilterDispatcher是框架的一个进入点；

2. 框架在struts.xml配置文件中找到名为HelloWorld的action对应的类。框架初始化该Action并且执行该Action类的execute方法；

3. execute方法将信息放入message变量中，并返回成功。框架检查配置以查看当返回成功时对应的页面。框架告诉容器来获得请求返回的结果页面HelloWorld.jsp;

4. 在HelloWorld.jsp执行完后，<s:property value="message" />标签调用HelloWorld的Action类中的getMessage方法来获得message的值，并将合并了值的页面呈现给用户；

5. 一个纯粹的HTML页面显示在用户的浏览器上。

四． 测试Action

测试Action很简单，以下是上述HelloWorld的Action类的测试类的代码：

package tutorial;
import junit.framework.TestCase;
import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ActionSupport;
public class HelloWorldTest extends TestCase [image] {
public void testHelloWorld() throws Exception [image] {
HelloWorld hello_world = new HelloWorld();
String result = hello_world.execute();
assertTrue("Expected a success result!",
ActionSupport.SUCCESS.equals(result));
assertTrue("Expected the default message!",
HelloWorld.MESSAGE.equals(hello_world.getMessage()));
}
}

五． 需要记住的东西

本框架利用Action来处理HTML的表单以及其余请求。Action返回一个结果的名字字符串，例如SUCCESS、ERROR以及INPUT等，从struts.xml中获取映射信息。一个给定的结果字符串将选择一个页面或其他资源（图片或PDF）来返回给用户。

当一个jsp被载入的时候，通常有一些动态变化的元素需要Action来载入。为了更加容易的显示动态数据，本框架提供了一些可以跟HTML配合使用的标签。

六． 问题汇总

对于出现启动失败，例如Cannot locate the chosen ObjectFactory implementation: spring等错误的朋友，请仔细检查你引入的包。关于需引入的包，请参见第一节（准备工作）。也希望遇到其余问题的朋友留言或发Email给我，在此进行汇总。


说明：本文大部分内容翻译自struts-2

说明：本文大部分内容翻译自struts-2.0.8-all\struts-2.0.8\docs\docs\using-tags.html，并将struts2的标签作为附录补充，标签尚有部分未补充完。

在上篇（Hello World篇）中，我们呈现了一个简单的欢迎界面。在本篇中，我们将创建链接到应用其他Action的链接。

Web应用程序与传统的Web站点有所不同，在Web应用程序中我们可以创建动态的返回。为了更容易的在页面上获得动态数据，本框架提供了一些标签集。其中有些标签模拟了标准的HTML标签，但是提供了一些额外功能。还有一些框架提供了非标准的、但非常有用的控制功能。

Struts标签的一个使用是创建一个到另外的Web资源的访问链接，特别是到本地的其他资源的链接。

说明：虽然HTML为创建超链接提供了简单的标签，HTML标签常常需要我们包括一些冗余的信息。并且HTML标签不能很容易的得到本框架的动态数据。

一． 链接标签

在Web应用程序中的一个常用的功能是链接到其他页面，现在让我们为前面章节的欢迎界面添加到其他Action的链接。

二． 代码

显示注册与登录的jsp的代码如下：

<%@ taglib prefix="s" uri="/struts-tags" %>

<html>

<head>

<title>Welcome</title>

<link href="<s:url value="/css/tutorial.css"/>" rel="stylesheet"

type="text/css"/>

</head>

<body>

<h3>Commands</h3>

<ul>

<li><a href="<s:url action="Register"/>">Register</a></li>

<li><a href="<s:url action="Logon"/>">Sign On</a></li>

</ul>

</body>

</html>

运行效果如下：

另一个常用的功能是使用链接来改变语言，在前章的HelloWorld页面中，让我们改变用户的语言，并根据对应的应用程序资源来呈现信息。代码如下：

<body>

<h2><s:property value="message"/></h2>

<h3>Languages</h3>

<ul>

<li>

<s:url id="url" action="HelloWorld">

<s:param name="request_locale">en</s:param>

</s:url>

<s:a href="%{url}">English</s:a>

</li>

<li>

<s:url id="url" action="HelloWorld">

<s:param name="request_locale">es</s:param>

</s:url>

<s:a href="%{url}">Espanol</s:a>

</li>

</ul>

</body>

运行效果如下：

一． 代码是如何工作的？

上述例子中的“%{url}”将会被s:url标签来求得对应的值。在Welcome和HelloWorld页中，我们使用了两种不同的链接标签。我们可以创建如下标签：

l 资源链接

l 直接的链接

l 带参数的链接

下面让我们分别看一下它们的使用：

1. 资源链接

首先在jsp的HEAD元素中，我们使用url标签来将资源引入到页面中，代码如下：

<link href="<s:url value="/css/tutorial.css"/>"

rel="stylesheet" type="text/css"/>

注意：引用时绝对的。我们可以移动该页面到其他路经而不用担心相对路径。

2．直接链接

我们可以使用链接标签来定向到Action，实例代码如下：

<li><a href="<s:url action="Register"/>">Register</a></li>

当链接运作的时候，链接标签能够自动追加正确的扩展，因此我们不需要在应用程序中嵌入。这个标签也将会用会话id来编码链接的URL，因此Java的会话在请求之间能够保持。

3．带有参数的链接

在上述的有关改变语言的HelloWorld页中，我们可以使用带有参数信息的url标签来创建带有参数的链接，该段代码如下：

<s:url id="url" action="Welcome">

<s:param name="request_locale">en</s:param>

</s:url>

<s:a href="%{url}">English</s:a>

param标签将会在Welcome的Action的url后增加信息：“?request_locale=en”。这个标签接着将“url”引用注入到超链接中。

说明：通过增加param标签能够增加任意数量的标签。

二． 通配符映射

在上述例子的Welcome页面中，除了链接之外尚未添加任何内容，我们不需要添加Action类。但是，我们仍然需要添加一个映射，以便我们可以使用action URI（如果我们向映射到action，而不是页面，我们在后面也可以很容易的添加Action类），映射代码如下：

<action name="Welcome" >

<result>/tutorial/Welcome.jsp</result>

</action>

当我们创建应用程序的时候，我们常常需要直接链接到页页面，为了使原型更加容易，我们可以将Welcome的进入改为通配符，修改后的映射如下：

<action name="*" >

<result>/tutorial/{1}.jsp</result>

</action>

这是代码是如何工作的呢？

如果找不到映射，本框架将会做如下工作：

l 将“Welcome”映射为星号

l 将“Welcome”代替result中对应的“{1}”

同样的，如果在映射文件中找不到“Login”的映射，将会映射到“/tutorial /Login.jsp”页面。

说明：通配符映射能够使得你能够创建你自己的规约，以便你能够避免冗余的配置。

三． 数据输入表单

大多数应用程序都使用一些数据进入表单。Struts标签使得输入表单更加容易，代码如下：

<%@ taglib prefix="s" uri="/struts-tags" %>

<html>

<head>

<title>Login</title>

</head>

<body>

<s:form action="Logon">

<s:textfield label="User Name" name="username"/>

<s:password label="Password" name="password" />

<s:submit/>

</s:form>

</body>

</html>

执行效果如下：

代码是如何工作的呢？

l JSP标签在页面顶部引入了Struts的标签库

l Struts标签：textfield、password和submit，每一个都写出了正确的Lable和控制类型

四． 需要记住的东西

编写web应用程序的最难的一部分是编写页面。本框架通过提供一系列的Struts标签使得编写页面更加容易。Struts标签能够获得框架提供的动态数据。标签减少了用来创建页面所需做的工作。

附录一

——标签使用指南

一． 通用标签

通用标签被用来在页面执行的时候，来控制执行流。这些标签同样允许数据不是从Action或者值栈中提取，例如本地化、JavaBeans、包括额外的URL或action执行。

l 控制类标签提供了控制流，例如if，else和iterator

l 数据类标签允许数据操作或创建，例如bean、push和i18n

控制类标签有if、elseIf、 else、append、generator、iterator、merge、sort、subset。

数据类标签有a、action、bean、date、debug、i18n、include、param、push、set、text、url、property。

分别介绍如下：

1. 控制类标签

1）if、elseif和else

描述：执行基本的控制流，if能单独使用，也可与else、elseif标签搭配使用。

参数：

名称 | 必选 | 默认值 | 求值 | 类型 | 描述
id | 否 |  | 是 | String | id用来引用元素。对于UI或者form标签，它的意义等同于HTML的id属性
test | 是 |  | 是 | Boolean | 用来决定标签体师是否显示的表达式

使用举例：

<s:if test="%{false}">

<div>Will Not Be Executed</div>

</s:if>

<s:elseif test="%{true}">

<div>Will Be Executed</div>

</s:elseif>

<s:else>

<div>Will Not Be Executed</div>

</s:else>

2）append

该标签的工作是追加迭代器来，

例如，如果有三个迭代器（每个迭代器有三个元素）需要追加，下面展示了这个追加迭代器是如何排列的。

首先是进入第一个迭代器的第一个元素；

第二步是进入第一个迭代器的第二个元素；

第三步是进入第一个迭代器的第三个元素；

第四步是进入第二个迭代器的第一个元素；

第五步是进入第二个迭代器的第二个元素；

第六步是进入第二个迭代器的第三个元素；

第七步是进入第三个迭代器的第一个元素；

第八步是进入第三个迭代器的第二个元素；

第九步是进入第三个迭代器的第三个元素。

参数：

名称 | 必选 | 默认值 | 求值 | 类型 | 描述
id | 否 |  | 是 | String | 如果提供了该id的值，将会具有追加迭代器存储到堆栈上下文中的合成结果

使用举例：

Action类的代码：

public class AppendIteratorTagAction extends ActionSupport [image] {
private List myList1;
private List myList2;
private List myList3;

public String execute() throws Exception [image] {
myList1 = new ArrayList();
myList1.add("1");
myList1.add("2");
myList1.add("3");

myList2 = new ArrayList();
myList2.add("a");
myList2.add("b");
myList2.add("c");

myList3 = new ArrayList();
myList3.add("A");
myList3.add("B");
myList3.add("C");

return "done";
}

public List getMyList1() [image] { return myList1; }
public List getMyList2() [image] { return myList2; }
public List getMyList3() [image] { return myList3; }

jsp页面的代码：

<s:append id="myAppendIterator">

<s:param value="%{myList1}" />

<s:param value="%{myList2}" />

<s:param value="%{myList3}" />

</s:append>

<s:iterator value="%{#myAppendIterator}">

<s:property />

</s:iterator>

3）generator

描述：创建一个基于提供的值的迭代器。

注意：产生的迭代器将常常被推入堆栈顶部，而在标签结束的时候被推出。

参数：

名称 | 必选 | 默认值 | 求值 | 类型 | 描述
converter | 否 |  | 是 | org.apache.struts2.util.IteratorGenerator.Converter | 将从值中分析的字符串转换为一个对象的转换器
count | 否 |  | 是 | Integer | 在迭代器中的最大值
id | 否 |  | 是 | String | 如果提供了id，它将会用来存储产生的迭代器到页面上下文
separator | 是 |  | 是 | String | 分隔符用来将迭代器中的值分开
val | 是 |  | 是 | String | 用来解析成迭代器的源

举例：

例1:

<pre>

例一：

产生一个简单的迭代器

<s:generator val="%{'aaa,bbb,ccc,ddd,eee'}">

<s:iterator>

<s:property /><br/>

</s:iterator>

</s:generator>

</pre>

这里产生了一个迭代器，并且使用iterator标签将它打印出来

例二:

<pre>

产生一个带有count属性的迭代器

<s:generator val="%{'aaa,bbb,ccc,ddd,eee'}" count="3">

<s:iterator>

<s:property /><br/>

</s:iterator>

</s:generator>

</pre>

这里产生了一个迭代器，但是只有其中的三个元素是可用的，这三个分别是aaa、bbb和ccc

例三:

<pre>

产生一个带有id属性的迭代器

<s:generator val="%{'aaa,bbb,ccc,ddd,eee'}" count="4" separator="," id="myAtt" />

<%

Iterator i = (Iterator) pageContext.getAttribute("myAtt");

while(i.hasNext()) {

String s = (String) i.next(); %>

<%=s%> <br/>

<% }

%>

</pre>

产生了一个迭代器，并且将它存入页面上下文的指定的id（myAtt）属性中。

例四:

<pre>

带有converter属性的generator标签

<s:generator val="%{'aaa,bbb,ccc,ddd,eee'}" converter="%{myConverter}">

<s:iterator>

<s:property /><br/>

</s:iterator>

</s:generator>

public class GeneratorTagAction extends ActionSupport {

....

public Converter getMyConverter() {

return new Converter() {

public Object convert(String value) throws Exception {

return "converter-"+value;

}

};

}

...

}

</pre>

产生的这个迭代器，它的每个元素由提供的转换器获得。在这个转换器中，只是为每个元素增加了“converter-”。

this converter, it simply add "converter-" to each entries.

4）iterator

描述：迭代器将会迭代值。一个可迭代的值可以是java.util.Collection，也可以是java.util.Iterator。

参数：

名称 | 必选 | 默认值 | 求值 | 类型 | 描述
id | 否 |  | 是 | String | id用来引用元素。对于UI和表单标签，它与HTML的id标签相当
status | 否 | 否 | 是 | Boolean | 如果该值被指定，一个迭代状态的实例将会在每一个迭代中被推入堆栈中
value | 否 |  | 是 | String | 用来进行迭代的迭代源，否则对象本身将会被放入新的产生的列表中

举例：

下面的例子取回在值栈中的当前对象的getDays()所返回的值，<s:property/>标签打印出当前迭代器的值，代码如下：

<s:iterator value="days">

<p>day is: <s:property/></p>

</s:iterator>

在下面的例子中使用Bean标签并且将它存入ActionContext中。iterator标签将会从ActionContext中取回对象而后调用它的getDays()方法。状态属性常常用来创建IteratorStatus对象，在这个例子中，它的odd()方法用来改变行的颜色。

<s:bean name="org.apache.struts2.example.IteratorExample" id="it">

<s:param name="day" value="'foo'"/>

<s:param name="day" value="'bar'"/>

</s:bean>

<p/>

<table border="0" cellspacing="0" cellpadding="1">

<tr>

<th>Days of the week</th>

</tr>

<p/>

<s:iterator value="#it.days" status="rowstatus">

<tr>

<s:if test="#rowstatus.odd == true">

<td style="background: grey"><s:property/></td>

</s:if>

<s:else>

<td><s:property/></td>

</s:else>

</tr>

</s:iterator>

</table>

下个例子将进一步展示status属性的使用，使用通过OGNL从action类取得的DAO，成员的迭代以及它们的使用（在安全的上下文中），last()方法中指明了当当前的对象是迭代器的最后一个可用的对象，如果不是，我们需要使用逗号来分隔用户，代码如下：

<s:iterator value="groupDao.groups" status="groupStatus">

<tr class="<s:if test="#groupStatus.odd == true ">odd</s:if><s:else>even</s:else>">

<td><s:property value="name" /></td>

<td><s:property value="description" /></td>

<td>

<s:iterator value="users" status="userStatus">

<s:property value="fullName" /><s:if test="!#userStatus.last">,</s:if>

</s:iterator>

</td>

</tr>

</s:iterator>

下一个例子在一个action collection上迭代，并且将每一个迭代的值传给另一个action。这里的诀窍在于使用”[0]”。它获得当前的值并且将值传入edit action。使用”[0]”与使用<s:property/>具有相同的效果（但是，后者，在param标签内部不起作用）。代码如下所示：

<s:action name="entries" id="entries"/>

<s:iterator value="#entries.entries" >

<s:property value="name" />

<s:property />

<s:push value="...">

<s:action name="edit" id="edit" >

<s:param name="entry" value="[0]" />

</s:action>

</push>

</s:iterator>

下例使用iterator标签来模拟一个简单的循环，循环了5次，代码如下：

<s:iterator status="stat" value="{1,2,3,4,5}" >

<!—获得当前的index（从0开始） -->

<s:property value="#stat.index" />

<!— 获得当前堆栈的值 -->

<!—当前的迭代值(0, 1, ... 5) -->

<s:property value="top" />

</s:iterator>

5）merge

描述：它是MergeIterator标签的组件，它的工作是合并迭代器和对合并后的迭代器的后续调用，它将使得每一个合并的迭代器有机会展示它的元素，接着下一个调用将会允许下一个迭代器来展示它的元素。一旦最后一个迭代器已展示完它的所有元素，第一个迭代器又能够开始展示它的元素（除非元素已经用尽）。

从内部来说，任务将委托给MergeIteratorFilter去做。

下面展示了3个列表的合并，其中每个列表有3个元素，步骤如下：

1． 展示第一个列表的第一个元素；

2． 展示第二个列表的第一个元素；

3． 展示第三个列表的第一个元素；

4． 展示第一个列表的第二个元素；

5． 展示第二个列表的第二个元素；

6． 展示第三个列表的第二个元素；

7． 展示第一个列表的第三个元素；

8． 展示第二个列表的第三个元素；

9． 展示第三个列表的第三个元素；

参数：

名称 | 必选 | 默认值 | 求值 | 类型 | 描述
id | 否 |  | 是 | String | 合并后的迭代器的值将会存储在堆栈上下文的id

举例：

Action类代码：

public class MergeIteratorTagAction extends ActionSupport {

private List myList1;

private List myList2;

private List myList3;

public List getMyList1() {

return myList1;

}

public List getMyList2() {

return myList2;

}

public List getMyList3() {

return myList3;

}

public String execute() throws Exception {

myList1 = new ArrayList();

myList1.add("1");

myList1.add("2");

myList1.add("3");

myList2 = new ArrayList();

myList2.add("a");

myList2.add("b");

myList2.add("c");

myList3 = new ArrayList();

myList3.add("A");

myList3.add("B");

myList3.add("C");

return "done";

}

}

jsp页代码：

<s:merge id="myMergedIterator1">

<s:param value="%{myList1}" />

<s:param value="%{myList2}" />

<s:param value="%{myList3}" />

</s:merge>

<s:iterator value="%{#myMergedIterator1}">

<s:property />

</s:iterator>

二． UI标签

三． 主题与模板标签

四． 标签引用

五． Ajax标签

六． 标签语法

标签被设计用来显示动态的数据。为了创建输入域来显示属性“postalCode”，我们需要将“postalCode”传给textfield标签。

下面创建了一个动态的输入域：

<s:textfield name="postalCode"/>

如果在值栈中存在“postalCode”属性，它的值将会被放入该输入域中。当输入被提交到框架之后，它的值将会被放置到“postalCode”属性中。

有时候，我们想要传动态的数据到标签中。例如，我们可能需要用输入域来显示一个label，我们可能想要从应用程序的message资源中。相应地，框架将会解析在标签属性中的表达式。因此在运行时我们能够合并动态的数据到标签的属性中。表达式是像“%{…}”这样的。任何一个在其中嵌入的文本被作为表达式来计算。

使用一个表达式来设置label的例子：

<s:textfield key="postalCode.label" name="postalCode"/>

表达式语言（OGNL）使得我们能够调用方法和计算属性。getText()方法由ActionSupport（大多数Action的基类）提供。我们可以调用任何一个表达式提供的方法，包括getText方法。

非String型的属性

HTTP协议是基于文本的，但是有一些标签时非String类型的，例如bool和int。为了能够直接使用非String型的属性，本框架将所有的非String类型的属性作为一个表达式处理，在这种情况下，你不需要使用任何转义符（但是，如果你使用了转义符，框架也会将其跳过）。

计算boolean型的例子：

<s:select key="state.label" name="state" multiple="true"/>

一旦multiple属性跟一个boolean属性对应起来，框架不会将它作为一个String来解释，这个值将会被作为一个表达式来计算并且自动地将其转换为boolean值。

因为很容易忘记哪个属性是String，哪个属性是非String型的，你仍然可以使用转义符。

计算boolean值（带有转义符的）：

<s:select key="state.label" name="state" multiple="%{true}"/>

带有属性的：

<s:select key="state.label" name="state" multiple="allowMultiple"/>

既带有转义符又带有属性的：

<s:select key="state.label" name="state" multiple="%{allowMultiple}"/>

值是一个对象

更通常的情况是，属性值是自动放入的，因为name属性通常告诉框架哪个属性来调用方法来set值。但是，如果需要直接设置值，建议那个值是Object型而不是String型。

注意：因为值是非String型的，无论传入什么，都是将他作为表达式来处理——而不是一个字面的String。

可能导致错误的例子：

<s:textfield key="state.label" name="state" value="CA"/>

如果textfield被传入 “CA”，本框架将会名字为getCa的属性。通常情况下，这不是我们的本意，我们想要做的是传送一个字符串。在表达式语言中，文字需要被置为单引号之间。

以正确的方式传入一个文字值：

<s:textfield key="state.label" name="state" value="%{'CA'}" />

另一种方法是使用value=”’CA’”，但是在这种情况下，推荐使用表达式符号。

标签属性使用如下三种规则计算：

1． 所有的String属性都被解析成“%{…}”符号；

2． 所有的非String属性没有被解析，而是直接按照表达式来算；

3． 规则2的异常情况是非String型的属性使用转义符号“{%{}”，符号被作为多余的符号被忽略，而只是计算内容。

表示式语言符号

1．在Freemarker、Velocity或者JSTL的表达式语言的JavaBean对象的标准文本

<p>Username: ${user.username}</p>

2．在值栈中的一个username属性

<s:textfield name="username"/>

3． 引用值栈中的属性的另一种方式

<s:url id="es" action="Hello">

<s:param name="request_locale">

es

</s:param>

</s:url>

<s:a href="%{es}">Espanol</s:a>

4． 在Session Context中获得user对象的userName属性

<s:property name="#session.user.username" />

5． 在一个静态map中，像("username","trillian")一样

<s:select label="FooBar" name="foo" list="#{'username':'trillian', 'username':'zaphod'}" />


说明：本文翻译自struts-2

说明：本文翻译自struts-2.0.8-all\struts-2.0.8\docs\docs\coding-actions.html

在使用Struts2标签这一章节中，我们实现了登录表单，在编写Action章节中，我们解释登录表单，并且根据不同的情况返回不同的结果码。

如果你已经编码完成，你可以用如下链接打开登录的Action：

http://localhost:8080/tutorial/Logon.action

并且输入一个合适的用户名和密码。因为你没有给这个Action任何的行为，映射重新显示了默认的Logon.jsp页面。

让我们新增一个Action类来使得登录表单更加有意思。

一． 代码

像例子中所示的，我们能够检查用户名和密码的值。如果其中一者或两者都为空，返回输入页面，否则，返回SUCCESS对应的页面，Logon.java对应的代码如下：

package tutorial;
import com.opensymphony.xwork2.ActionSupport;
public class Logon extends ActionSupport [image] {

public String execute() throws Exception [image] {

if (isInvalid(getUsername())) return INPUT;
if (isInvalid(getPassword())) return INPUT;
return SUCCESS;
}

private boolean isInvalid(String value) [image] {
return (value == null || value.length() == 0);
}

private String username;
public String getUsername() [image] {
return username;
}
public void setUsername(String username) [image] {
this.username = username;
}

private String password;
public String getPassword() [image] {
return password;
}
public void setPassword(String password) [image] {
this.password = password;
}

}

二。代码是如何工作的？

框架自动为我们的username和password属性完成组装功能。我们所需要做的就是核对是否其中有属性为空。

三. 需要记住的东西

在web应用程序中，Action做了重量级的工作。Action与数据库系统和业务规则引擎相互作用，以便我们能够把一些简单的HTML变成复杂的、动态的web体验。

在做了这些工作之后，Action返回了一个结果码来指明框架下一步应该做什么工作。通常，下一步是跳转到成功结果页面，但在某些情况下，我们需要跳转到错误结果页。还在某一些情况下，Action不用担心产生的回应，仅仅决定哪些逻辑结果需要在下一步呈现。


说明：本文翻译自struts自带的doc，路经为：struts-2

说明：本文翻译自struts自带的doc，路经为：struts-2.0.8-all\struts-2.0.8\docs\docs\selecting-results.html

在编写Action这一章节中，我们创建了一个Logon类来测试输入。在本章中，我们将使这个测试的结果起作用。

一． 选择一个“Input”结果

在一个Action执行一个请求后，一个提供用来作为这个请求回应的结果被选择。这个结果可以简单的定向到一个HTML页面、一个JSP页面、一个FreeMaker或一个Velocity模板、或者这个结果可能构造一个PDF或某些更加复杂的输出（例如JasperReports）。在一个action映射中可能有多个可选的结果。为了指明那一个被选择，Action类返回一个与正确结果相对应的名字。

二． 代码

struts.xml的代码如下所示：

<action name="Logon" class="tutorial.Logon">

<result type="redirect-action">Menu</result>

<result name="input">/tutorial/Logon.jsp</result>

</action>

三． 代码是如何工作的？

1. 如果你在表单中输入username和password属性，Logon Action将会返回“success”；

2. 如果你其中一者或两者都没有输入值，Logon Action将返回“input”，框架将会使用Logon.jsp作为回应。

在Hello World章节中，我们的结果使用了默认的类型：Dispatcher。Dispatcher定向到另外的web资源。其它种类的视图能够被用来指明不同的结果类型。

Logon映射为success（默认的返回值）使用了一种不同的返回类型。“redirect-action”使用Action的名字作为参数，接着使客户端定向到一个新的action。

四． 使用一个Stub页

当我们开发web应用程序的时候，我们常常需要进行forward引用——我们需要引用到一个我们还没有开始写的action。例如，在文章的第一部分中，下一步是跳转到“Menu”页面。如果我们成功登录，我们就没有地方去了，因为“Menu”尚不存在。

解决这种问题的方法是创建一个Stub“Menu”页。

Menu.jsp的代码如下：

<html>

<head><title>Missing Feature</title></head>

<body>

<p>

This feature is under construction.

Please try again in the next interation.

</p>

</body>

</html>

这段代码是如何工作的呢？

1. 当登录class返回“Menu”时，框架将会将其与通配符映射对应起来；

2. 框架现在将会返回一个stub的“Menu.jsp”。

说明：如果你没有使用通配符，另一种注入一个丢失页面的方法就是指定一个<action-default-ref>元素。

五． 包括一个缺失页面

如果你页到页的建造一个应用程序，建立一个标准的缺失页面时对应的标准页面是非常必要的。Menu.jsp的代码如下：

<%@ taglib prefix="s" uri="/struts-tags" %>

<s:include value="Missing.jsp" />

这段代码是如何工作的呢？

当需要展现Menu.jsp的时候，它将会包括标准的Missing.jsp的内容。

六． 需要记住的东西

本框架提供多种返回类型。一个Action能够通过名字选择正确的返回结果，而不用知道到底是哪一种返回类型。


说明：该文翻译自struts-2

说明：该文翻译自struts-2.0.8\docs\docs\validating-input.html。

在编写Action这一章节中，我们通过几行java代码来验证username和password。当然，在大型的应用程序中，这将是个很耗时的工作，即使这几行java代码也可能变成维护的重大负担。

令人高兴的是，本框架提供了一个验证框架，这个验证框架能够对输入数据进行后台验证。

一． 代码

验证能够通过XML文档来描述，或者使用注解（annotations）。XML文档的名称以Action的名字为开头，以“-validation”作为文件名后缀。当我们想要验证Logon Action的时候，我们文件名可以命名为Logon-validation.xml。Logon-validation.xml的代码如下：

<!DOCTYPE validators PUBLIC

"-//OpenSymphony Group//XWork Validator 1.0.2//EN"

"http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">

<validators>

<field name="username">

<field-validator type="requiredstring">

<message>Username is required</message>

</field-validator>

</field>

<field name="password">

<field-validator type="requiredstring">

<message>Password is required</message>

</field-validator>

</field>

</validators>

在Logon.java后我们创建一个Logon-validation.xml文件。并将上述粘贴到该文件中。

页面第一次显示的时候，我们不想引发验证。我们需要在被告诉有输入不正确前有机会输入数据。通过验证的一种方式是找到一个ActionSupport基类提供的特定的“input”方法。为了这样做，我们需要编辑Welcome页面和Logon的映射。

Welcome.jsp的修改代码如下：

    <li><a href="<s:url
action="Logon"/>">Sign On</a></li>
<li><a href="<s:url
action="Logon_input"/>">Sign
On</a></li>

struts.xml的修改代码如下：

    <action name="Logon" class="tutorial.Logon">
<action name="Logon_*" method="{1}"
class="tutorial.Logon">

二． 代码是如何工作的？

为了打开Logon表单，Welcome页面涉及了Logon_input。

1. 框架将这个引用于Logon_*映射相匹配起来；

2. “method={1}”属性被“method=input”替换；

3. 框架调用了Logon Action类的input方法；

4. 因为“input”在一个特殊的方法的列表中，所以验证框架将不会调用；

5. 默认的input方法返回一个“input”作为返回结果；

6. 框架展现“Logon.jsp”来作为回应结果，没有带有任何的验证信息。

为了提交Logon表单，Login页面引用了Logon。

1. 框架为目标Action——Logon提供验证；

2. 找到Logon-validation.xml文件，框架为该类创建了一个验证对象，这个验证对象基于XML文件；

3. 验证器对输入的数据产生作用；

4. 如果验证失败，错误信息被添加到内部序列中；

5. 当所有的验证器都已经执行后，如果框架发现有错误信息产生，它寻找“input”结果对应的页面，而不调用Action类；

6. 如果通过验证，调用Action的方法，返回“success”对应的结果。

三． 需要记住的东西

框架提供了一个验证框架。一个输入域可以绑定一个验证器集合。如果验证失败，框架将会返回输入信息页，并显示错误信息。为了通过验证，一个特殊的“input”方法将被调用，来代替默认的“execute”方法。


说明：本文翻译自：struts-2

说明：本文翻译自：struts-2.0.8\docs\docs\validating-input.html

在验证输入这一小节中，我们使用验证框架来验证提交到表单的数据。在“本地化输出”这一节中，我们将验证信息移到消息资源文件中。

当创建web应用程序的时候，我们发现我们在多个页面中常常使用一些相同的消息或字段标签。如果我们的系统由不同语言的用户使用时，我们也想要本地化消息。

一． 本地化验证信息和属性值

让我们增加一个消息资源文件，并且在其中加入验证信息和字段标签。

1．代码

本框架将消息资源与类联系起来。为了为Logon action增加一个消息资源，我们需要将消息资源文件命名为Logon.properties，并且在Logon Action后设置它的classpath。但是，大多数人发现为某一个类使用一个单独的资源文件时间和低效率的工作。很多人更喜欢为一整个类包增加一个消息资源文件。为了做到这样，我们可以简单的为某个包增加一个paacage.properties文件。例如。我们为tutorial包增加一个资源文件tutorial/package.properties。该文件内容如下：

requiredstring = $\{getText(fieldName)} is required.

password = Password

username = User Name

我们还需要改变一下验证器和Logon页面。

Logon-validation.xml修改的地方：

<message>Username is required</message>
<message key="requiredstring"/>

<message>Password is required</message>
<message key="requiredstring"/>

Logon,jsp修改的地方如下：

<s:textfield
label="User Name" name="username"/>
<s:textfield label="%{getText('username')}"
name="username"/>

<s:password
label="Password" name="password" />
<s:password label="%{getText('password')}"
name="password" />

2． 代码是如何工作的？

1）“key”属性告诉验证器为一个消息资源捆绑验证；

2） 在资源捆绑中，表达式：${getText(fieldName)}告诉框架在资源文件中查找字段名。这种方式下我们能够为所有的非空验证使用相同的默认信息；

3）相似地，在文本域中，表达式：%{getText(‘password’)}告诉框架在资源文件中寻找“password”。

二． 本地化其余的信息

其余的页面元素也能够被本地化。例如，我们需要在资源文件中增加“Hello World”和“Missing page”这两个消息。

1． 代码

tutorial/package.properties：

# ...

HelloWorld.message = Struts is up and running ...

Missing.message = This feature is under construction. Please try again in the next interation.

这对HelloWorld是可运行的，因为Hello World已经存在于包中。但是它对于默认的Missing action是不可用的，除非你将我们的基类加入到tutorial包中。

TutorialSupport.java的代码如下：

package tutorial;

import com.opensymphony.xwork2.ActionSupport;

public class TutorialSupport extends ActionSupport {}

并且还需要更新一下struts.xml默认的通配符映射，如下：

<action name="*" class="tutorial.TutorialSupport">

<result>/{1}.jsp</result>

</action>

现在，我们可以更新HelloWorld.jsp和Missing.jsp来查找信息。

Missing.jsp的代码如下：

This feature is under construction. Please try again in the next interation.
<s:text name="Missing.message"/>

在HelloWorld的情况下，我们从Action类设置消息。让我们更新一下class，使用资源文件来代替它。HelloWorld的代码如下：

package tutorial;
public class HelloWorld extends ExampleSupport [image] {
public static final String MESSAGE = "HelloWorld.message";
public String execute() throws Exception [image] {
setMessage(getText(MESSAGE));
return SUCCESS;
}

//
}

2． 代码是如何工作的？

1）对于Missing.jsp，我们使用text标签来查找资源文件中的消息；
2） 在HelloWorld中，我们在Action类中使用getText()方法来获得消息；

3） 在HelloWorld.jsp中，只是显示由Action类设置的消息，所以不要做任何改变。

三． 需要记住的东西

框架支持国际化。为了本地化应用，我们增加了资源文件，并且更新某些元素或标签来应用资源文件中的资源而不是静态的文本。


说明：本文经过笔者对struts2

说明：本文经过笔者对struts2.0.8自带的实例struts2-blank-2.0.8学习而写，对该例子进行了部分修改，希望对广大象要学习struts2的朋友有所帮助。

一． 实例说明

本例是Struts2的简单实例，通过本工程的学习，我们将会对struts2的表单以及表单验证有一个初步的认识、了解struts2的配置以及初探Struts2的本地化输出。

二． 编码准备

1． 包的引入

在MyEclipse或NetBeans中建立web工程，将所需的包放入WebRoot/lib目录中，本实例所需的包有：

commons-logging-1.1.jar

freemarker-2.3.8.jar

ognl-2.6.11.jar

struts2-core-2.0.8.jar

xwork-2.0.3.jar

这些包在struts-2.0.8-all\struts-2.0.8\lib目录下都可以找到，请读者自行下载。

2． web.xml的配置

要使struts能正常工作，需修改web.xml的内容，为其增加struts2的FilterDispatcher，修改后的web.xml的内容如下：

<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_9" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>Struts Blank</display-name>
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
</filter>

<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>

</web-app>

3. 建立源码目录和 jsp 存放目录

在src目录下建立example文件夹，用于存放本实例的java文件等。在WebRoot文件夹下建立子文件夹example，用于存放本工程的jsp文件。

三． 编码

1. Welcome.jsp和 Login.jsp 的编写

首先我们建立Welcome.jsp，该文件包含两个链接，点击“登录”链接后跳转到登录信息输入页面，点击“注册”按钮跳转到注册页，为了学习struts2配置中的通配符使用，我们暂不实现注册功能。Welcome.jsp的代码如下：

<% [image] @ page contentType="text/html; charset=UTF-8" %>
<% [image] @ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<title>欢迎界面</title>
<link href="<s:url value="/css/examplecss"/>" rel="stylesheet" type="text/css"/>
</head>

<body>
<h3>导航</h3>
<ul>
<li><a href="<s:url value="/example/Login.jsp"/>">登录</a></li>
<li><a href="<s:url action="Register"/>">注册</a></li>
</ul>
</body>
</html>

在该页面的顶部，我们需要将struts2的标签库引入，语句为：<%@ taglib prefix="s" uri="/struts-tags" %>

在该页面，主要用到struts2的<s:url>标签，该页面主要用到该标签的两个属性，分别为value和action，其中action属性表示用action来产生url，而value表示使用的目标值。在页面上点击“查看源文件”按钮，可看到生成的语句分别变为：

<link href="/struts2-blank-2.0.8/css/examplecss"
rel="stylesheet"
type="text/css"/>

<a href="/struts2-blank-2.0.8/example/Login.jsp ">

<a href="/struts2-blank-2.0.8/example/Register.action">

由此可知使用该标签时，struts2会自动为我们带上下文路径，对于加了属性action的<s:url>标签，后面会自动带上“.action”作为后缀。

点击“登录”链接后，跳转到Login.jsp页面，该页包含一个登录表单，让用户输入用户名和密码信息，用户点击提交按钮，跳转到指定的Action——Login进行处理。Login.jsp的内容如下：

<% [image] @ page contentType="text/html; charset=UTF-8" %>
<% [image] @ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<title>登录</title>
</head>
<body>
<s:form action="Login">
<s:textfield name="username" label="用户名"/>
<s:password name="password" label="密码"/>
<s:submit/>
</s:form>
</body>
</html>

该页用到Struts2的表单标签<s:form>、<s:textfield>和<s:password>。

<s:form>的action属性表示表单提交后跳转的action的名称，此处为Login，该标签最终将生成HTML的form；

<s:textfield>标签类同于HTML的<input type=”text” …>,其中name表示属性域的名称，label表示其前的提示名；

<s:password>标签类同于HTML的<input type=”password” …>，其name和label类同于<s:textfield>，在此略。

2. 配置文件 struts.xml 和 example.xml

在上述jsp页面，我们需跳转到两个Action地址，需在struts2的配置文件中配置，因当工程变大时，一个庞大的struts2的配置极难维护，建议按包路径分开配置文件，所以本实例除了struts.xml配置文件外，还新增了一个额外的配置文件example.xml。该文件在struts.xml中引用。struts.xml放在src目录下，该文件的内容如下：

<?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>
<constant name="struts.enable.DynamicMethodInvocation" value="false" />
<constant name="struts.devMode" value="false" />
<include file="example.xml"/>
<!-- Add packages here -->
</struts>

可看到该文件通过<include file="example.xml"/>将example.xml也作为struts2的配置文件。

接下来，让我们看看example.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>
<package name="example" namespace="/example" extends="struts-default">
<action name="Login_input" method="{1}" class="example.Login">
<result name="input">/example/Login.jsp</result>
<result type="redirect-action">Menu</result>
</action>
<!-- 为学习struts2配置文件中通配符的使用，我们将未定义的action的引用都定向到example.ExampleSupport这个Action中, -->
<!-- 需定向的Action的名字传到{1}中，eg.若请求Register这个action，当ExampleSupport返回success时，跳转到/example/Register.jsp -->
<action name="*" class="example.ExampleSupport">
<result>/example/{1}.jsp</result>
</action>
</package>
</struts>

3. Login和 ExampleSupport 类以及验证配置类Login-validation.xml的编写

在配置文件example.xml中，定义了两个Action，下面我们用代码来实现这两个Action

首先让我们来看看ExampleSupport这个Action，这个Action不做任何操作，集成自ActionSupport，是本工程的各Action类的基类，该类的代码如下：

package example;
import com.opensymphony.xwork2.ActionSupport;
public class ExampleSupport extends ActionSupport [image] {
}

接着让我们来看看Login这个Action，该类继承自ExampleSupport类，该Action需实现的业务逻辑如下：

1） 当用户名（username）或密码（password）有一者或两者为空时，登录不成功，跳转到登录信息输入页面；

2） 当用户名（username）和密码（password）都不为空时，登录成功，跳转到主菜单页。

对于用户名和密码的验证，我们可以先考虑在Login类中用代码实现的方式，此时该类的代码如下：

package example;

public class Login extends ExampleSupport [image] {
public String execute() throws Exception [image] {
if (isInvalid(getUsername()))
return INPUT;
if (isInvalid(getPassword()))
return INPUT;
return SUCCESS;
}

private boolean isInvalid(String value) [image] {
return (value == null || value.length() == 0);
}

private String username;

public String getUsername() [image] {
return username;
}

public void setUsername(String username) [image] {
this.username = username;
}

private String password;

public String getPassword() [image] {
return password;
}

public void setPassword(String password) [image] {
this.password = password;
}

}

当工程变得愈发复杂时，这一小段一小段验证代码将会变得难以维护，出于此原因，我们可以考虑采用struts2提供的验证机制来实现。在src目录下建立实现验证的xml文件Login-validation.xml，为Login Action中的username和password属性增加非空验证，验证配置如下：

<!DOCTYPE validators PUBLIC
"-//OpenSymphony Group//XWork Validator 1.0.2//EN"
"http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">

<validators>
<field name="username">
<field-validator type="requiredstring">
<message key="requiredstring"/>
</field-validator>
</field>
<field name="password">
<field-validator type="requiredstring">
<message key="requiredstring"/>
</field-validator>
</field>
</validators>

当验证未通过时，将不会进入Login Action中的execute方法，此时可删除掉Login这个Action中的验证内容，该类的execute方法直接跳转到SUCCESS即可，修改后的代码略。

4． 本地化输出——资源文件 package.properties

为了本地化的输出验证错误信息，我们可以将参数信息和错误信息放入资源文件中，资源文件package.properties位于src/example目录下，内容如下：

requiredstring = ${getText(fieldName)}不能为空.

password = 密码

username = 用户名

Missing.message = 该部分尚未构建，请稍候访问...

在src/example目下下建立对应的中文资源文件package_zh_CN.properties，为了避免中文乱码问题，我们编写一个批处理文件code.bat来对package.properties进行编码处理，主要用到native2ascii命令，其内容如下：

del package_zh_CN.properties

copy package.properties package_zh_CN.properties.gbk

native2ascii -encoding GBK package_zh_CN.properties.gbk package_zh_CN.properties

del package_zh_CN.properties.gbk

del *.bak

运行该批处理文件，可在package_zh_CN.properties中可看到编码后的资源文件信息，如下：

requiredstring = ${getText(fieldName)}\u4e0d\u80fd\u4e3a\u7a7a.

password = \u5bc6\u7801

username = \u7528\u6237\u540d

Missing.message = \u8be5\u90e8\u5206\u5c1a\u672a\u6784\u5efa\uff0c\u8bf7\u7a0d\u5019\u8bbf\u95ee...

5． Register.jsp和 Missing.jsp 的编写

在2中的example.xml中，我们配置了通配符映射，在Welcome.jsp中，我们使用

<a href="<s:url action="Register"/>">注册</a>

其中的Register在example中找不到相关映射，于是在用户点击“注册”按钮时，将映射到通配符所映射的Action：example. ExampleSupport.而后跳转到Register.jsp页面，其代码如下：

<% [image] @ page contentType="text/html; charset=UTF-8" %>
<% [image] @ taglib prefix="s" uri="/struts-tags" %>
<s:include value="Missing.jsp"/>

该页面包含Missing.jsp页面，其代码如下：

<% [image] @ page contentType="text/html; charset=UTF-8" %>
<% [image] @ taglib prefix="s" uri="/struts-tags" %>
<html>
<head><title>未构建页面</title></head>

<body>
<p>
<!-- 读取配置文件中的对应信息. -->
<s:text name="Missing.message"/>
</p>
</body>
</html>

四．总结

到此为止，我们的实例已构建完，可通过浏览器访问：http://localhost:8080/struts2-blank-2.0.8/example/Welcome.jsp看到我们的劳动成果。下面总结一下我们在该实例中学到的东西。

1． 常用的一些标签的使用

在本实例中，我们学习了<s:textfield>、<s:password>、<s:form>以及<s:url>的使用。

2． 表单数据的验证

本文描述了一个简单的表单的数据非空验证。

3. 本地化输出错误提示信息

在验证失败时，用资源文件来输出错误提示信息。

4．Strut2配置文件中通配符的使用

对于某些地址，我们可以使用在struts2的配置通配符来使其映射到某个Action中去。


本章要点

— Struts 2的下载和安装

— 纯手工创建一个Web应用

— 纯手工创建一个Struts 2应用

— 实现Struts 2的Action

— 配置Struts 2的Action

— 在Action中访问HttpSession

— 在JSP中输出Action的返回值

— 使用Struts 2的表单标签

— 程序国际化初步

— 数据校验初步

前面已经简要介绍了Struts 2的起源，以及Struts 2的两个前身：Struts 1和WebWork，并详细对比了Struts 2和Struts 1的差异，对比了Struts 2和WebWork的差异，而且指出：Struts 2是WebWork的升级，而不是Struts 1的升级。

虽然Struts 2提供了与Struts 1的兼容，但已经不是Struts 1的升级。对于已有Struts 1开发经验的开发者而言，Struts 1的开发经验对于Struts 2并没有太大的帮助；相反，对于已经有WebWork开发经验的开发者而言，WebWork的开发经验对Struts 2的开发将有很好的借鉴意义。

下面将以一个Struts 2的HelloWorld应用为例，介绍Strust 2 MVC框架如何拦截用户请求，如何调用业务控制器处理用户请求，并介绍Action处理结果和资源之间的映射关系。

本HelloWorld应用是一个简单的应用：用户进入一个登录页面，允许用户输入用户名、密码，如果用户输入的用户名和密码符合要求，则进入一个欢迎页面；如果用户输入错误，则进入一个提示页面。当用户提交表单时，本应用会有基本的数据校验。

2.1 下载和安装Struts 2框架

下面我们从下载、安装Struts 2开始，慢慢开始体验Struts 2 MVC框架的魅力。

笔者写本书的时候，Struts 2已经发布了其产品化GA（General Availability）版，其实最新的产品化GA版是Struts 2.06，故本书的所有应用都是基于该版本的Struts 2。建议读者下载Struts 2.06版，而不是下载最新的Beta版，如果Struts 2有最新的GA版，读者也可以下载更新的GA版，相信不会有太大差异。

下载和安装DWR请按如下步骤进行。

* 登录http://struts.apache.org/download.cgi#Struts206站点，下载Struts 2的最新GA版。在Struts 2.06下有如下几个选项：

— Full Distribution：下载Struts 2的完整版。通常建议下载该选项。

— Example Applications：下载Struts 2的示例应用，这些示例应用对于学习Struts 2有很大的帮助，下载Struts 2的完整版时已经包含了该选项下全部应用。

— Blank Application only：仅下载Struts 2的空示例应用，这个空应用已经包含在Example Applications选项下。

— Essential Dependencies：仅仅下载Struts 2的核心库，下载Struts 2的完整版时将包括该选项下的全部内容。

— Documentation：仅仅下载Struts 2的相关文档，包含Struts 2的使用文档、参考手册和API文档等。下载Struts 2的完整版时将包括该选项下的全部内容。

— Source：下载Struts 2的全部源代码，下载Struts 2的完整版时将包括该选项下的全部内容。

— Alternative Java 4 JARs：下载可选的JDK 1.4的支持JAR。下载Struts 2的完整版时将包括该选项下的全部内容。

通常建议读者下载第一个选项：下载Struts 2的完整版，将下载到的Zip文件解压缩，该文件就是一个典型的Web结构，该文件夹包含如下文件结构：

— apps：该文件夹下包含了基于Struts 2的示例应用，这些示例应用对于学习者是非常有用的资料。

— docs：该文件夹下包含了Struts 2的相关文档，包括Struts 2的快速入门、Struts 2的文档，以及API文档等内容。

— j4：该文件夹下包含了让Struts 2支持JDK 1.4的JAR文件。

— lib：该文件夹下包含了Struts 2框架的核心类库，以及Struts 2的第三方插件类库。

— src：该文件夹下包含了Struts 2框架的全部源代码。

将lib文件夹下的Struts2-core-2.0.6.jar、xwork-2.0.1.jar和ognl-2.6.11.jar等必需类库复制到Web应用的WEB-INF/lib路径下。当然，如果你的Web应用需要使用Struts 2的更多特性，则需要将更多的JAR文件复制到Web应用的WEB-INF/lib路径下。如果需要在DOS或者Shell窗口下手动编译Struts 2相关的程序，则还应该将Struts2-core-2.0.6.jar和xwork-2.0.1.jar添加到系统的CLASSPATH环境变量里。

提示 大部分时候，使用Struts 2的Web应用并不需要利用到Struts 2的全部特性，因此没有必要一次将该lib路径下JAR文件全部复制到Web应用的WEB-INF/lib路径下。

编辑Web应用的web.xml配置文件，配置Struts 2的核心Filter。下面是增加了Struts 2的核心Filter配置的web.xml配置文件的代码：

<?xml version="1.0" encoding="GBK"?>

<!-- web-app是Web应用配置文件的根元素，指定Web应用的Schema信息 -->

<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.

com/xml/ns/j2ee/web-app_2_4.xsd">

<!-- 定义Struts 2的FilterDispatcher的Filter -->

<filter>

<!-- 定义核心Filter的名字 -->

<filter-name>struts2</filter-name>

<!-- 定义核心Filter的实现类 -->

<filter-class>org.apache.Struts2.dispatcher.FilterDispatcher

</ filter-class>

</filter>

<!-- FilterDispatcher用来初始化Struts 2并且处理所有的Web请求 -->

<filter-mapping>

<filter-name>Struts2</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

</web-app>

经过上面3个步骤，我们已经可以在一个Web应用中使用Struts 2的基本功能了，下面将带领读者进入Struts 2 MVC框架的世界。

2.2 从用户请求开始

Struts 2支持大部分视图技术，当然也支持最传统的JSP视图技术，本应用将使用最基本的视图技术：JSP技术。当用户需要登录本系统时，用户需要一个简单的表单提交页面，这个表单提交页面包含了两个表单域：用户名和密码。

下面是一个最简单的表单提交页面，该页面的表单内仅包含两个表单域，甚至没有任何动态内容，实际上，整个页面完全可以是一个静态HTML页面。但考虑到需要在该页面后面增加动态内容，因此依然将该页面以jsp为后缀保存。下面是用户请求登录的JSP页面代码：

<%@ page language="java" contentType="text/html; charset=GBK"%>

<html>

<head>

<title>登录页面</title>

</head>

<body>

<!-- 提交请求参数的表单 -->

<form action="Login.action" method="post">

<table align="center">

<caption><h3>用户登录</h3></caption>

<tr>

<!-- 用户名的表单域 -->

<td>用户名：<input type="text" name="username"/></td>

</tr>

<tr>

<!-- 密码的表单域 -->

<td>密&nbsp;&nbsp;码：<input type="text" name="password"/></td>

</tr>

<tr align="center">

<td colspan="2"><input type="submit" value="登录"/><input

type="reset" value="重填" /></td>

</tr>

</table>

</form>

</body>

</html>

正如前面介绍的，该页面没有包含任何的动态内容，完全是一个静态的HTML页面。但我们注意到该表单的action属性：login.action，这个action属性比较特殊，它不是一个普通的Servlet，也不是一个动态JSP页面。可能读者已经猜到了，当表单提交给login.action时，Struts 2的FilterDispatcher将自动起作用，将用户请求转发到对应的Struts 2 Action。

注意 Struts 2 Action默认拦截所有后缀为.action的请求。因此，如果我们需要将某个表单提交给Struts 2 Action处理，则应该将该表单的action属性设置为*.action的格式。

文本框: 图2.1 用户登录的页面 该页面就是一个基本的HTML页面，在浏览器中浏览该页面，看到如图2.1所示的界面。

整个页面就是一个标准的HTML页面，整个单独的页面还没有任何与用户交互的能力。下面我们开始动手创建一个Struts 2的Web应用。

2.3 创建Struts 2的Web应用

Struts 2的Web应用就是一个普通的Web应用，然后增加Struts 2功能，该应用就可以充分利用Struts 2的MVC框架了。

2.3.1 创建Web应用

笔者一直相信：要想成为一个优秀的程序员，应该从基本功练起，所有的代码都应该用简单的文本编辑器（包括EditPlus、UtraEdit等工具）完成。笔者经常见到一些有两三年开发经验的程序员，一旦离开了熟悉的IDE（集成开发环境，如Eclipse、JBuilder等），完全不能动手写任何代码。而他们往往还振振有词：谁会不用任何工具来开发？

实际上，真正优秀的程序员当然可以使用IDE工具，但即使使用VI（UNIX下无格式编辑器）、记事本也一样可以完成非常优秀的项目。笔者对于IDE工具的态度是：可以使用IDE工具，但绝不可依赖于IDE工具。学习阶段，千万不要使用IDE工具；开发阶段，才去使用IDE工具。

提醒 对于IDE工具，业内有一个说法：IDE工具会加快高手的开发效率，但会使初学者更白痴。

为了让读者更加清楚Struts 2应用的核心，笔者下面将“徒手”建立一个Struts 2应用。

建立一个Web应用请按如下步骤进行。

* 在任意目录新建一个文件夹，笔者将以该文件夹建立一个Web应用。

在第1步所建的文件夹内建一个WEB-INF文件夹。

进入Tomcat，或任何Web容器内，找到任何一个Web应用，将Web应用的WEB-INF下的web.xml文件复制到第2步所建的WEB-INF文件夹下。

修改复制的web.xml文件，将该文件修改成只有一个根元素的XML文件，修改后的web.xml文件代码如下：

<?xml version="1.0" encoding="GBK"?>

<!-- web-app是Web应用配置文件的根元素，指定Web应用的Schema信息 -->

<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.

com/xml/ns/j2ee/web-app_2_4.xsd">

</web-app>

* 在第2步所建的WEB-INF路径下，新建两个文件夹：classes和lib，它们分别用于保存单个*.class文件和JAR文件。

经过上面步骤，已经建立了一个空Web应用。将该Web应用复制到Tomcat的webapps路径下，该Web应用将可以自动部署在Tomcat中。

将2.2节所定义的JSP页面文件复制到第1步所建的文件夹下，该JSP页面将成为该Web应用的一个页面。该Web将有如下文件结构：

Struts2qs

|－WEB-INF

| |－classes

| |－lib

| |－web.xml

|－login.jsp

上面的Struts2qs是Web应用所对应文件夹的名字，可以更改；login.jsp是该Web应用下JSP页面的名字，也可以修改。其他文件夹、配置文件都不可以修改。

启动Tomcat，在浏览器中浏览2.2节定义的JSP页面，将看到如图2.1所示的页面。

2.3.2 增加Struts 2功能

为了给Web应用增加Struts 2功能，只需要将Struts 2安装到Web应用中即可。在Web应用中安装Struts 2框架核心只需要经过如下三个步骤。

* 修改web.xml文件，在web.xml文件中配置Struts 2的核心Filter。

将Struts 2框架的类库复制到Web应用的WEB-INF/lib路径下。

在WEB-INF/classes下增加struts.xml配置文件。

下面是增加了Struts 2功能后Web应用的文件结构：

Struts2qs

|－WEB-INF

| |－classes（struts.xml）

| |－lib（commons-logging.jar，freemarker.jar，ognl.jar，struts2-core.jar，xwork.jar）

| |－web.xml

|－login.jsp

在上面的文件结构中，lib下Struts 2框架的类库可能有版本后缀。例如commons-logging.jar，可能是commons-logging-1.1.jar；struts2-core.jar可能是struts2-core-2.0.6.jar。

修改后的web.xml文件在2.1节已经给出了，故此处不再赘述。

此处需要给读者指出的是，Struts 2的Web应用默认需要Java 5运行环境，需要Web容器支持Servlet API 2.4和JSP API 2.0。如果读者需要使用更低版本的Java运行时环境，则需要使用Struts 2框架的JDK 1.4支持。为了简单起见，笔者建议读者使用Java 5运行时环境，使用Tomcat 5.5或者更高版本。

注意 Struts 2应用默认需要Java 5运行时环境，需要支持Servlet API 2.4和JSP API 2.0的Web容器。

2.4 实现控制器

前面介绍MVC框架时，已经指出：MVC框架的核心就是控制器。当用户通过2.2节的页面提交用户请求时，该请求需要提交给Struts 2的控制器处理。Struts 2的控制器根据处理结果，决定将哪个页面呈现给客户端。

2.4.1 实现控制器类

Struts 2下的控制器不再像Struts 1下的控制器，需要继承一个Action父类，甚至可以无需实现任何接口，Struts 2的控制器就是一个普通的POJO。

实际上，Struts 2的Action就是一个包含execute方法的普通Java类，该类里包含的多个属性用于封装用户的请求参数。下面是处理用户请求的Action类的代码：

//Struts 2的Action类就是一个普通的Java类

public class LoginAction

{

//下面是Action内用于封装用户请求参数的两个属性

private String username;

private String password;

//username属性对应的getter方法

public String getUsername()

{

return username;

}

//username属性对应的setter方法

public void setUsername(String username)

{

this.username = username;

}

//password属性对应的getter方法

public String getPassword()

{

return password;

}

//password属性对应的setter方法

public void setPassword(String password)

{

this.password = password;

}

//处理用户请求的execute方法

public String execute() throws Exception

{

//当用户请求参数的username等于scott，密码请求参数为tiger时，返回success

字符串

//否则返回error字符串

if (getUsername().equals("scott")

&& getPassword().equals("tiger") )

{

return "success";

}

else

{

return "error";

}

}

}

上面的Action类是一个再普通不过的Java类，该类里定义了两个属性：username和password，并为这两个属性提供了对应的setter和getter方法。除此之外，该Action类里还包含了一个无参数的execute方法——这大概也是Action类与POJO唯一的差别。实际上，这个execute方法依然是一个很普通的方法，既没有与Servlet API耦合，也没有与Struts 2 API耦合。

提示 表面上看起来，该Action的两个属性只提供了对应的setter和getter方法，很难理解请求参数在什么时候赋值给该Action的属性，事实上，因为Struts 2的拦截器机制，它们负责解析用户的请求参数，并将请求参数赋值给Action对应的属性。

2.4.2 配置Action

上面定义了Struts 2的Action，但该Action还未配置在Web应用中，还不能处理用户请求。为了让该Action能处理用户请求，还需要将该Action配置在struts.xml文件中。

前面已经介绍过了，struts.xml文件应该放在classes路径下，该文件主要放置Struts 2的Action定义。定义Struts 2 Action时，除了需要指定该Action的实现类外，还需要定义Action处理结果和资源之间的映射关系。下面是该应用的struts.xml文件的代码：

<?xml version="1.0" encoding="GBK"?>

<!-- 指定Struts 2配置文件的DTD信息 -->

<!DOCTYPE struts PUBLIC

"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"

"http://struts.apache.org/dtds/struts-2.0.dtd">

<!-- struts是Struts 2配置文件的根元素 -->

<struts>

<!-- Struts 2的Action必须放在指定的包空间下定义 -->

<package name="strutsqs" extends="struts-default">

<!-- 定义login的Action，该Action的实现类为lee.Action类 -->

<action name="Login" class="lee.LoginAction">

<!-- 定义处理结果和资源之间映射关系。 -->

<result name="error">/error.jsp</result>

<result name="success">/welcome.jsp</result>

</action>

</package>

</struts>

上面映射文件定义了name为login的Action，即：该Action将负责处理向login.action URL请求的客户端请求。该Action将调用自身的execute方法处理用户请求，如果execute方法返回success字符串，请求将被转发到/welcome.jsp页面；如果execute方法返回error字符串，则请求被转发到/error.jsp页面。

2.4.3 增加视图资源完成应用

经过上面步骤，这个最简单的Struts 2应用几乎可以运行了，但还需要为该Web应用增加两个JSP文件，两个JSP文件分别是error.jsp页面和welcome.jsp页面，将这两个JSP页面文件放在Web应用的根路径下（与WEB-INF在同一个文件夹下）。

这两个JSP页面文件是更简单的页面，它们只是包含了简单的提示信息。其中welcome.jsp页面的代码如下：

<%@ page language="java" contentType="text/html; charset=GBK"%>

<html>

<head>

<title>成功页面</title>

</head>

<body>

您已经登录!

</body>

</html>

上面的页面就是一个普通的HTML页面，登录失败后进入的error.jsp页面也与此完全类似。

在如图2.1所示页面的“用户名”输入框中输入scott，在“密码”输入框中输入tiger，页面将进入welcome.jsp页面，将看到如图2.2所示的页面。

文本框: 图2.2 登录成功页面 对于上面的处理流程，可以简化为如下的流程：用户输入两个参数，即username和password，然后向login.action发送请求，该请求被FilterDispatcher转发给LoginAction处理，如果LoginAction处理用户请求返回success字符串，则返回给用户welcome.jsp页面；如果返回error字符串，则返回给用户error.jsp页面。

图2.3显示了上面应用的处理流程。

图2.3 HelloWorld应用的处理流程

2.5 改进控制器

通过前面介绍，读者已经可以完成简单的Struts 2的基本应用了，但还可以进一步改进应用的Action类，例如该Action类可以通过实现Action接口，利用该接口的优势。前面应用的Action类没有与JavaBean交互，没有将业务逻辑操作的结果显示给客户端。

2.5.1 实现Action接口

表面上看起来，实现Struts 2的Action接口没有太大的好处，仅会污染该Action的实现类。事实上，实现Action接口可以帮助开发者更好地实现Action类。下面首先看Action接口的定义：

public interface Action

{

//下面定义了5个字符串常量

public static final String SUCCESS = "success";

public static final String NONE = "none";

public static final String ERROR = "error";

public static final String INPUT = "input";

public static final String LOGIN = "login";

//定义处理用户请求的execute抽象方法

public String execute() throws Exception;

}

在上面的Action代码中，我们发现该Action接口里已经定义了5个标准字符串常量：SUCCESS、NONE、ERROR、INPUT和LOGIN，它们可以简化execute方法的返回值，并可以使用execute方法的返回值标准化。例如对于处理成功，则返回SUCCESS常量，避免直接返回一个success字符串（程序中应该尽量避免直接返回数字常量、字符串常量等）。

因此，借助于上面的Action接口，我们可以将原来的Action类代码修改为如下：

//实现Action接口来实现Struts 2的Action类

public class LoginAction implements Action

{

//下面是Action内用于封装用户请求参数的两个属性

private String username;

private String password;

//username属性对应的getter方法

public String getUsername()

{

return username;

}

//username属性对应的setter方法

public void setUsername(String username)

{

this.username = username;

}

//password属性对应的getter方法

public String getPassword()

{

return password;

}

//password属性对应的setter方法

public void setPassword(String password)

{

this.password = password;

}

//处理用户请求的execute方法

public String execute() throws Exception

{

//当用户请求参数的username等于scott，密码请求参数为tiger时，返回success

字符串

//否则返回error的字符串

if (getUsername().equals("scott")

&& getPassword().equals("tiger") )

{

return SUCCESS;

}

else

{

return ERROR;

}

}

}

对比前面Action和此处的Action实现类，我们发现两个Action类的代码基本相似，除了后面的Action类实现了Action接口。因为实现了Action接口，故Action类的execute方法可以返回Action接口里的字符串常量。

2.5.2 跟踪用户状态

前面的Action处理完用户登录后，仅仅执行了简单的页面转发，并未跟踪用户状态信息——通常，当一个用户登录成功后，需要将用户的用户名添加为Session状态信息。

为了访问HttpSession实例，Struts 2提供了一个ActionContext类，该类提供了一个getSession的方法，但该方法的返回值类型并不是HttpSession，而是Map。这又是怎么回事呢？实际上，这与Struts 2的设计哲学有关，Struts 2为了简化Action类的测试，将Action类与Servlet API完全分离，因此getSession方法的返回值类型是Map，而不是HttpSession。

虽然ActionContext的getSession返回的不是HttpSession对象，但Struts 2的系列拦截器会负责该Session和HttpSession之间的转换。

为了可以跟踪用户信息，我们修改Action类的execute方法，在execute方法中通过ActionContext访问Web应用的Session。修改后的execute方法代码如下：

//处理用户请求的execute方法

public String execute() throws Exception

{

//当用户请求参数的username等于scott，密码请求参数为tiger时，返回success字符串

//否则返回error的字符串

if (getUsername().equals("scott")

&& getPassword().equals("tiger") )

{

//通过ActionContext对象访问Web应用的Session

ActionContext.getContext().getSession().put("user" , getUsername());

return SUCCESS;

}

else

{

return ERROR;

}

}

上面的代码仅提供了Action类的execute方法，该Action类的其他部分与前面的Action类代码完全一样。在上面的Action类通过ActionContext设置了一个Session属性：user。为了检验我们设置的Session属性是否成功，我们修改welcome.jsp页面，在welcome.jsp页面中使用JSP 2.0表达式语法输出Session中的user属性。下面是修改后的welcome.jsp页面代码：

<%@ page language="java" contentType="text/html; charset=GBK"%>

<html>

<head>

<title>成功页面</title>

</head>

<body>

欢迎，${sessionScope.user},您已经登录!

</body>

</html>

文本框: 图2.4 登录成功页面 上面的JSP页面与前面的JSP页面没有太大改变，除了使用了JSP 2.0语法来输出Session中的user属性。关于JSP 2.0表达式的知识，请参看笔者所著的《轻量级J2EE企业应用实战》一书的第2章。

在如图2.1所示页面的“用户名”输入框中输入scott，在“密码”输入框中输入tiger，然后单击“登录”按钮，将看到如图2.4所示的页面。

在上面登录成功的页面中，已经输出登录所用的用户名：scott，可见在Action通过ActionContext设置Session是成功的。

2.5.3 添加处理信息

到目前为止，Action仅仅控制转发用户请求，JSP页面并未获得Action的处理结果。对于大部分Web应用而言，用户需要获得请求Action的处理结果，例如，在线购物系统需要查询某个种类下的商品，则Action调用业务逻辑组件的业务逻辑方法得到该种类下的全部商品，而JSP页面则获取该Action的处理结果，并将全部结果迭代输出。

下面将为应用增加一个Action，该Action负责获取某个系列的全部书籍。为了让该Action可以获取这系列的书籍，我们增加一个业务逻辑组件，它包含一个业务逻辑方法，该方法可以获取某个系列的全部书籍。

下面是系统所用的业务逻辑组件的代码：

public class BookService

{

//以一个常量数组模拟了从持久存储设备（数据库）中取出的数据

private String[] books =

new String[]{

"Spring2.0宝典" ,

"轻量级J2EE企业应用实战",

"基于J2EE的Ajax宝典",

"Struts,Spring,Hibernate整合开发"

};

//业务逻辑方法，该方法返回全部图书

public String[] getLeeBooks()

{

return books;

}

}

上面的业务逻辑组件实际上就是MVC模式中的Model，它负责实现系统业务逻辑方法。理论上，业务逻辑组件实现业务逻辑方法时，必须依赖于底层的持久层组件，但此处的业务逻辑组件则只是返回一个静态的字符串数组——因为这只是一种模拟。

注意 此处的业务逻辑组件只是模拟实现业务逻辑方法，并未真正调用持久层组件来获取数据库信息。

在系统中增加如下Action类，该Action类先判断Session中user属性是否存在，并且等于scott字符串——这要求查看图书之前，用户必须已经登录本系统。如果用户已经登录本系统，则获取系统中全部书籍，否则返回登录页面。

新增的Action类的代码如下：

public class GetBooksAction implements Action

{

//该属性并不用于封装用户请求参数，而用于封装Action需要输出到JSP页面信息

private String[] books;

//books属性的setter方法

public void setBooks(String[] books)

{

this.books = books;

}

//books属性的getter方法

public String[] getBooks()

{

return books;

}

//处理用户请求的execute方法

public String execute() throws Exception

{

//获取Session中的user属性

String user = (String)ActionContext.getContext().getSession().

get("user");

//如果user属性不为空，且该属性值为scott

if (user != null && user.equals("scott"))

{

//创建BookService实例

BookService bs = new BookService();

//将业务逻辑组件的返回值设置成该Action的属性

setBooks(bs.getLeeBooks());

return SUCCESS;

}

else

{

return LOGIN;

}

}

}

通过上面的Action类，我们发现Action类中的成员属性，并不一定用于封装用户的请求参数，也可能是封装了Action需要传入下一个JSP页面中显示的属性。

提示 Action中的成员属性，并一定用于封装用户的请求参数，也可能是封装了Action需要传入下一个页面显示的值。实际上，这些值将被封装在ValueStack对象中。

当我们的控制器需要调用业务逻辑方法时，我们直接创建了一个业务逻辑组件的实例，这并不是一种好的做法，因为控制器不应该关心业务逻辑组件的实例化过程。比较成熟的做法可以利用工厂模式来管理业务逻辑组件；当然，目前最流行的方式是利用依赖注入——这将在后面章节里介绍。

注意 实际项目中不会在控制器中直接创建业务逻辑组件的实例，而是通过工厂模式管理业务逻辑组件实例，或者通过依赖注入将业务逻辑组件实例注入控制器组件。

该Action处理用户请求时，无需获得用户的任何请求参数。将该Action配置在struts.xml文件中，配置该Action的配置片段如下：

<!-- 定义获取系统中图书的Action，对应实现类为lee.GetBooksAction -->

<action name="GetBooks" class="lee.GetBooksAction">

<!-- 如果处理结果返回login，进入login.jsp页面 -->

<result name="login">/login.jsp</result>

<!-- 如果处理结果返回success，进入showBook.jsp页面 -->

<result name="success">/showBook.jsp</result>

</action>

当用户向getBooks.action发送请求时，该请求将被转发给lee.GetBooksAction处理。

2.5.4 输出处理信息

如果用户没有登录，直接向getBooks.action发送请求，该请求将被转发到login.jsp页面。如果用户已经登录，getBooks.action将从系统中加载到系统中的所有图书，并将请求转发给showBook.jsp页面，因此showBook.jsp页面必须负责输出全部图书。

下面笔者将以最原始的方式：JSP脚本来输出全部图书。

注意 在实际应用中，几乎绝对不会使用笔者这种方式来输出Action转发给JSP输出的信息，但笔者为了让读者更清楚Struts 2标签库在底层所完成的动作，故此处使用JSP脚本来输出全部图书信息。

当Action设置了某个属性值后，Struts 2将这些属性值全部封装在一个叫做struts.valueStack的请求属性里。

提示 读者可能感到奇怪：笔者是如何知道Struts 2将这些属性值封装在struts.valueStack请求属性里的?这一方面与编程经验有关，另一方面可以通过查看Struts 2的各种文档，最重要的一点是可以在showBook.jsp页面中通过getAttributeNames方法分析请求中的全部属性。

为了在JSP页面中输出需要输出的图书信息，我们可以通过如下代码来获取包含全部输出信息的ValueStack对象。

//获取封装输出信息的ValueStack对象

request.getAttribute("struts.valueStack");

上面代码返回一个ValueStack对象，该对象封装了全部的输出信息。该对象是 Struts 2使用的一个ValueStack对象，可以通过OGNL表达式非常方便地访问该对象封装的信息。

从数据结构上来看，ValueStack有点类似于Map结构，但它比Map结构更加强大（因为它可以根据表达式来查询值）。Action所有的属性都被封装到了ValueStack对象中，Action中的属性名可以理解为ValueStack中value的名字。

大致理解了ValueStack对象的结构后，我们可以通过如下代码来获取Action中设置的全部图书信息。

//调用ValueStack的fineValue方法查看某个表达式的值

vs.findValue("books");

理解了上面关键的两步，整个JSP页面的代码就比较容易了解了。下面是showBook.jsp页面的代码：

<%@ page language="java" contentType="text/html; charset=GBK">

<% @page import="java.util.*,com.opensymphony.xwork2.util.*"%>

<html>

<head>

<title>作者李刚的图书</title>

</head>

<body>

<table border="1" width="360">

<caption>作者李刚的图书</caption>

<%

//获取封装输出信息的ValueStack对象

ValueStack vs = (ValueStack)request.getAttribute("struts.valueStack");

//调用ValueStack的fineValue方法获取Action中的books属性值

String[] books = (String[])vs.findValue("books");

//迭代输出全部图书信息

for (String book : books)

{

%>

<tr>

<td>书名：</td>

<td><%=book%></td>

</tr>

<%}%>

</table>

</body>

</html>

不可否认，上面JSP页面的代码是丑陋的，而且难以维护，因为里面镶嵌了大量的Java脚本。但它对于读者理解Struts 2如何处理封装在Action的ValueStack却很有帮助。

在浏览器中向getBooks.action发送请求，将看到如图2.5所示的页面。

通过上面页面，我们看到JSP页面已经输出了Struts 2控制器的返回信息。上面整个过程，已经完全包括了Struts 2框架的3个部分：视图、控制器和模型。

图2.5 在JSP页面中输出Action的返回信息

2.6 改进视图组件

通过前面的几节的介绍，我们已经明白了Struts 2 MVC框架的基本数据流，已经完成了Struts 2应用中模型、控制器、视图3个组件的开发。但应用中的视图组件：JSP页面非常丑陋，特别是输出Action返回信息的JSP页面，使用了大量的Java脚本来控制输出，下面将会使用Struts 2的标签来改善整个应用视图组件。

2.6.1 改善输出页面

为了控制输出Struts 2的ValueStack中封装的值，Struts 2提供了大量的标签。其中比较常用的标签有：

— if：该标签支持标签体，如果if标签里判断的表达式返回真，则输出标签体内容。

— else：该标签不能独立使用，它需要与if标签结合使用，如果if标签内判断的表达式返回假，则输出该标签里的标签体。

— iterator：主要用于迭代输出某个集合属性的各个集合元素。

— property：该标签用于输出指定属性值。

关于Struts 2标签库更深入的使用，第10章还会深入介绍，故此处不再详细讲解。通过使用上面的几个标签，替换showBook.jsp页面中的Java脚本，修改后的showBook.jsp页面代码如下：

<%@ page language="java" contentType="text/html; charset=GBK" %>

<!-- 导入Struts 2的标签库 -->

<%@taglib prefix="s" uri="/struts-tags"%>

<html>

<head>

<title>作者李刚的图书</title>

</head>

<body>

<table border="1" width="360">

<caption>作者李刚的图书</caption>

<!-- 迭代输出ValueStack中的books对象，其中status是迭代的序号 -->

<s:iterator value="books" status="index">

<!-- 判断序号是否为奇数 -->

<s:if test="#index.odd == true">

<tr style="background-color:#cccccc">

</s:if>

<!-- 判断迭代元素的序号是否不为偶数 -->

<s:else>

<tr>

</s:else>

<td>书名：</td>

<td><s:property/></td>

</tr>

</s:iterator>

</table>

</body>

</html>

上面的JSP页面使用了Struts 2的标签库，因此必须在JSP页面的首部添加taglib指令，该taglib指令用于导入标签库。

提示 如果需要使用某个标签库中的标签，则必须在页面的开始导入该标签库。

页面中使用Struts 2的iterator标签迭代输出ValueStack中的books数组，并为每个数组元素定义了一个序号：index。通过判断序号是否为奇数，如果行序号为奇数，则输出一个有背景色的表格行；否则输出一个无背景色的表格行。

在浏览器中再次向getBooks.action发送请求（发送请求之前，必须先登录本系统），将看到如图2.6所示的界面。

图2.6 使用Struts 2标签改善后的输出界面

上面页面的输出效果与图2.5并没有太大的不同，只是使用不同颜色来分隔了记录行。这也得益于Struts 2标签库的简洁。

关键在于2.5.4节中的JSP页面代码与本节页面代码的差异：前面JSP页面使用了大量的Java脚本，让整个页面的代码看起来非常凌乱，降低了可阅读性、可维护性。但本页面中仅使用Struts 2脚本控制输出，完全消除了页面中的Java脚本，降低了该页面的后期维护成本。

2.6.2 使用UI标签简化表单页面

前面已经提到过，Struts 2的一个重要组件就是标签库。Struts 2标签库中不仅提供了前面所示的基本控制、数据输出等功能，还提供了非常丰富的UI组件，除了提供系列的主题相关标签外，还提供了一系列的表单相关的标签。

Struts 2为常用表单域都提供了对应的标签，下面是常用的表单域标签。

— form：对应一个表单元素。

— checkbox：对应一个复选框元素。

— password：对应一个密码输入框。

— radio：对应一个单选框元素。

— reset：对应一个重设按钮。

— select：对应一个下拉列表框。

— submit：对应一个提交按钮。

— textarea：对应一个多行文本域。

— textfield：对应一个单行文本框。

关于这些界面相关的标签，同样将在第10章详细介绍。下面将使用Struts 2的表单相关标签简化用户登录的login.jsp页面，修改的login.jsp页面的代码如下：

<%@ page language="java" contentType="text/html; charset=GBK"%>

<%@taglib prefix="s" uri="/struts-tags"%>

<html>

<head>

<title>登录页面</title>

</head>

<body>

<!-- 使用form标签生成表单元素 -->

<s:form action="Login">

<!-- 生成一个用户名文本输入框 -->

<s:textfield name="username" label="用户名"/>

<!-- 生成一个密码文本输入框 -->

<s:textfield name="password" label="密 码"/>

<!-- 生成一个提交按钮 -->

<s:submit value="登录"/>

</s:form>

</body>

</html>

将该页面与前面的表单页面进行对比，我们发现该页面的代码简洁多了。因为使用了Struts 2的表单标签，定义表单页面也更加迅速。在浏览器中浏览该页面，看到如图2.7所示的界面。

图2.7 使用Struts 2表单标签后的表单页

当然，Struts 2的标签还有许多功能，此处先不详述，本书的第10章将会详细介绍Struts 2标签的用法。

2.7 完成程序国际化

因为一个企业应用经常需要面对多区域的用户，因此，程序国际化是一个企业应用必须实现的功能。Struts 2提供了很好的程序国际化支持。

2.7.1 定义国际化资源文件

Struts 2的程序国际化支持建立在Java程序国际化的基础之上，关于Java程序的国际化笔者将在第9章简要介绍。此处不会详细介绍，但我们要明白一个概念：程序国际化的设计思想是非常简单的，其主要思想是：程序界面中需要输出国际化信息的地方，我们不要在页面中直接输出信息，而是输出一个key值，该key值在不同语言环境下对应不同的字符串。当程序需要显示时，程序将根据不同的语言环境，加载该key对应该语言环境下的字符串——这样就可以完成程序的国际化。

图2.8显示了程序国际化的示意图。

图2.8 程序国际化示意图

从图2.8可以看出，如果需要程序支持更多的语言环境，只需要增加更多语言资源文件即可。

为了给本应用增加程序国际化支持（支持英文和中文），则应该提供两份语言资源文件。下面是本应用所使用的中文语言环境下资源文件的代码。

loginPage=登录页面

errorPage=错误页面

succPage=成功页面

failTip=对不起，您不能登录！

succTip=欢迎，${0},您已经登录！

viewLink=查看作者李刚已出版的图书

bookPageTitle=作者李刚已出版的图书

bookName=书名：

user=用户名

pass=密 码

login=登录

因为该资源文件中包含了非西欧字符，因此必须使用native2ascii命令来处理该文件。将上面文件保存在WEB-INF/classes路径下，文件名为“messageResouce.properties”。保存该文件后，必须使用native2ascii命令来处理该文件，处理该文件的命令格式为：

native2ascii messageResouce.properties messageResouce_zh_CN.properties

上面命令将包含非西欧字符的资源文件处理成标准的ASCII格式，处理完成后生成了一份新文件：messageResouce _zh_CN.properties文件。这个文件的文件名符合资源文件的命名格式，资源文件的文件名命名格式为：

basename_语言代码_国家代码.properties

当请求来自简体中文的语言环境时，系统将自动使用这种资源文件中的内容输出。

注意 对于包含非西欧字符的资源文件，一定要使用native2assii命令来处理该文件，否则将看到一堆乱码。

除此之外，还应该提供如下英文语言环境的资源文件。

loginPage=Login Page

errorPage=Error Page

succPage=Welcome Page

failTip=Sorry,You can't log in!

succTip=welcome,{0},you has logged in!

viewLink=View LiGang\'s Books

bookPageTitle=LiGang\'s Books

bookName=BookName:

user=User Name

pass=User Pass

login=Login

将上面资源文件保存在WEB-INF/classes路径下，文件名为“messageResouce_en_ US.properties”。当请求来自美国时，系统自动使用这份资源文件的内容输出。

2.7.2 加载资源文件

Struts 2支持在JSP页面中临时加载资源文件，也支持通过全局属性来加载资源文件。通过全局属性加载资源文件更简单，本应用使用全局属性加载Struts 2国际化资源文件。

加载资源文件可以通过struts.properties文件来定义，本应用的struts.properties文件仅有如下一行代码：

//定义Struts 2的资源文件的baseName是messageResource

struts.custom.i18n.resources=messageResource

在struts.properties文件中增加上面的代码定义后，表明该应用使用的资源文件的baseName为“messageResouce”——这与我们前面保存资源文件的baseName是一致的。

Struts 2默认加载WEB-INF/classes下的资源文件，在上一节中，我们就是将资源文件保存在该路径下的。如果将该资源文件保存在WEB-INF/classes的子目录下，例如保存在WEB-INF/classes/lee路径下，则需要修改struts.properties中的定义如下：

//定义Struts 2的资源文件的baseName是messageResource，且文件放在WEB-INF/

classes/lee路径下

struts.custom.i18n.resources=lee.messageResource

2.7.3 输出国际化信息

为了让程序可以显示国际化信息，则需要在JSP页面中输出key，而不是直接输出字符串常量。

Struts 2提供了如下两种方式来输出国际化信息：

— <s:text name="messageKey"/>：使用s:text标签来输出国际化信息。

— <s:property value="%{getText("messageKey")}"/>：使用表达式方式输出国际化信息。

因此，我们再次修改表现层的JSP页面，使用国际化标签输出国际化信息。修改后的showBook.jsp页面代码如下：

<%@ page language="java" contentType="text/html; charset=GBK"%>

<%@taglib prefix="s" uri="/struts-tags"%>

<html>

<head>

<!-- 使用s:text输出国际化信息 -->

<title><s:text name="bookPageTitle"/></title>

</head>

<body>

<table border="1" width="360">

<!-- 使用s:text输出国际化信息 -->

<caption><s:text name="bookPageTitle"/></caption>

<s:iterator value="books" status="index">

<s:if test="#index.odd == true">

<tr style="background-color:#cccccc">

</s:if>

<s:else>

<tr>

</s:else>

<td><s:text name="bookName"/></td>

<td><s:property/></td>

</tr>

</s:iterator>

</table>

</body>

</html>

我们发现，上面的JSP页面不再包含任何直接字符串，而是全部通过<s:text name="..."/>来输出国际化提示。

再次在浏览器浏览该页面，将看到与图2.7相同的界面。

重新设置浏览者所在的语言/区域选项，设置语言/区域选项请先进入“控制面板”，在控制面板中单击“区域和语言选项”，进入如图2.9所示的对话框。

如果我们选择“英语（美国）”选项，然后单击“确定”按钮，将设置本地的语言环境为美国英语。

再次向服务器请求login.jsp页面，将看到如图2.10所示的页面。

图2.9 设置语言/区域选项 图2.10 程序国际化的效果

如果我们使用FireFox浏览器来浏览该页面时，发现依然显示中文界面——这是因为FireFox的语言环境并不受Windows系统的控制。为了让FireFox也使用美国英语环境，单击FireFox浏览器菜单栏中的“工具”菜单，选择“选项”菜单项，将出现“选项”对话框，单击“高级”按钮，将看到如图2.11所示的界面。

在如图2.11所示的对话框中单击“选择”按钮，将出现“语言和字符编码”对话框，在该对话框下面的下拉列表框中选择“英语/美国 [en-us]”选项，如图2.12所示，然后单击下拉框右边的“添加”按钮，将添加了“英语/美国”的语言环境。并在该对话框上面的列表框中选择“英语/美国 [en-us]”，然后单击“上移”按钮，将该选项移至最上面，让整个页面优先使用英语/美国的环境。

图2.11 设置FireFox的语言环境 图2.12 设置FireFox的语言环境为英语/美国

再次使用FireFox浏览器浏览login.jsp页面，将看到该页面变成了英文界面。

2.8 增加数据校验

在上面应用中，即使浏览者输入任何用户名、密码，系统也会处理用户请求。在我们整个HelloWorld应用中，这种空用户名、空密码的情况不会引起太大的问题。但如果数据需要保存到数据库，或者需要根据用户输入的用户名、密码查询数据，这些空输入可能引起异常。

为了避免用户的输入引起底层异常，通常我们会在进行业务逻辑操作之前，先执行基本的数据校验。

2.8.1 继承ActionSupport

ActionSupport类是一个工具类，它已经实现了Action接口。除此之外，它还实现了Validateable接口，提供了数据校验功能。通过继承该ActionSupport类，可以简化Struts 2的Action开发。

在Validatable接口中定义了一个validate()方法，重写该方法，如果校验表单输入域出现错误，则将错误添加到ActionSupport类的fieldErrors域中，然后通过OGNL表达式负责输出。

为了让Struts 2增加输入数据校验的功能，改写程序中的LoginAction，增加重写validate方法。修改后的LoginAction类代码如下：

//Struts 2的Action类就是一个普通的Java类

public class LoginAction

{

//下面是Action内用于封装用户请求参数的两个属性

private String username;

private String password;

//username属性对应的getter方法

public String getUsername()

{

return username;

}

//username属性对应的setter方法

public void setUsername(String username)

{

this.username = username;

}

//password属性对应的getter方法

public String getPassword()

{

return password;

}

//password属性对应的setter方法

public void setPassword(String password)

{

this.password = password;

}

//处理用户请求的execute方法

public String execute() throws Exception

{

//当用户请求参数的username等于scott，密码请求参数为tiger时，返回success

字符串

//否则返回error的字符串

if (getUsername().equals("scott")

&& getPassword().equals("tiger") )

{

return "success";

}

else

{

return "error";

}

}

//完成输入校验需要重写的validate方法

public void validate()

{

//如果用户名为空，或者用户名为空字符串

if (getUsername() == null || getUsername().trim().equals(""))

{

//添加表单校验错误

addFieldError("username", "user.required");

}

//当密码为空，或者密码为空字符串时，添加表单校验错误

if (getPassword() == null || getPassword().trim().equals(""))

{

addFieldError("password", "pass.required");

}

}

}

上面的Action类重写了validate方法，该方法会在执行系统的execute方法之前执行，如果执行该方法之后，Action类的fieldErrors中已经包含了数据校验错误，请求将被转发到input逻辑视图处。

为了在校验失败后，系统能将视图转入input处，必须在配置该Action时配置input属性。下面是修改后login Action的配置片段：

<!-- 定义login的Action -->

<action name="Login" class="lee.LoginAction">

<!-- 定义input的逻辑视图名，对应login.jsp页面 -->

<result name="input">/login.jsp</result>

<!-- 定义error的逻辑视图名，对应error.jsp页面 -->

<result name=" success ">/error.jsp</result>

<!-- 定义welcome的逻辑视图名，对应welcome.jsp页面 -->

<result name="success">/welcome.jsp</result>

</action>

对比上面的Action配置与前面的Action配置，我们发现该Action配置片段中增加了input逻辑视图的配置，该逻辑视图映射到login.jsp页面。

前面已经提到：当用户提交请求时，请求得到execute方法处理之前，先会被validate方法处理，如果该方法处理结束后，Action的fieldErrors里的校验错误不为空，请求将被转发给input逻辑视图。如果我们不输入用户名、密码而直接提交表单，将看到如图2.13所示的界面。

图2.13 输入校验的界面

看到这里也许读者觉得非常神奇：我们仅仅在Action添加了数据校验错误，并未在输入页面输出这些校验错误信息，但图2.13所示的页面，却可以看到页面已经输出了这些校验信息——这是因为Struts 2的标签，上面的JSP页面中表单使用的并不是HTML表单，而是用了<s:form .../>标签，Struts 2的<s:form ... />标签已经具备了输出校验错误的能力。

提示 Struts 2的<s:form .../>默认已经提供了输出校验错误的能力。

但上面的程序还存在一个问题：校验信息的国际化。查看上面的Action类代码发现：重写validate方法时，如果发生校验失败的问题，校验错误的提示信息是以硬编码方式写死了——这就失去了国际化的能力。

实际上，ActionSupport类已经提供了国际化信息的能力，它提供了一个getText（String key）方法，该方法用于从资源文件中获取国际化信息。为了让校验信息支持国际化，再次改写Action里的validate方法，改写后的validate方法代码如下：

//执行数据校验的validate方法

public void validate()

{

//如果用户名为空，或者为空字符串

if (getUsername() == null || getUsername().trim().equals(""))

{

//添加校验错误提示，使用getText方法来使提示信息国际化

addFieldError("username", getText("user.required"));

}

if (getPassword() == null || getPassword().trim().equals(""))

{

addFieldError("password", getText("pass.required"));

}

}

在上面的validate方法中，添加校验错误提示时，并不是直接给出了错误提示的字符串，而是调用了getText方法来获取错误提示。因为在Action中，使用getText方法来获取了两个国际化提示：user.required和pass.required，因此应该在国际化资源文件中添加这两条提示信息。

提示 ActionSupport增加了让提示信息国际化的能力，ActionSupport提供的getText方法可以根据资源文件加载获得国际化提示信息。

此时，如果没有任何输出，直接提交登录表单，将看到如图2.14所示的界面。

图2.14 国际化数据校验的错误提示

2.8.2 使用Struts 2的校验框架

上面的输入校验是通过重写ActionSupport类的validate方法实现的，这种方法虽然不错，但需要大量重写的validate方法——毕竟，重复书写相同的代码不是一件吸引人的事情。

类似于Struts 1，Struts 2也允许通过定义配置文件来完成数据校验。Struts 2的校验框架实际上是基于XWork的validator框架。

下面还是使用原来的Action类（即不重写validate方法），却增加一个校验配置文件，校验配置文件通过使用Struts 2已有的校验器，完成对表单域的校验。Struts 2提供了大量的数据校验器，包括表单域校验器和非表单域校验器两种。

本应用主要使用了requiredstring校验器，该校验器是一个必填校验器——指定某个表单域必须输入。

下面是校验规则的定义文件：

<?xml version="1.0" encoding="GBK"?>

<!-- 指定校验规则文件的DTD信息 -->

<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork Validator

1.0.2//EN"

"http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">
<!-- 校验规则定义文件的根元素 -->

<validators>

<!-- 校验第一个表单域：username -->

<field name="username">

<!-- 该表单域必须填写 -->

<field-validator type="requiredstring">

<!-- 如果校验失败，显示user.required对应的信息 -->

<message key="user.required"/>

</field-validator>

</field>

<!-- 校验第二个表单域：password -->

<field name="password">

<field-validator type="requiredstring">

<!-- 如果校验失败，显示pass.required对应的信息 -->

<message key="pass.required"/>

</field-validator>

</field>

</validators>

定义完该校验规则文件后，该文件的命名应该遵守如下规则：

ActionName-validation.xml：其中ActionName就是需要校验的Action的类名。

因此上面的校验规则文件应该命名为“LoginAction-validation.xml”，且该文件应该与Action类的class文件位于同一个路径下。因此，将上面的校验规则文件放在WEB-INF/classes/lee路径下即可。

当然，在struts.xml文件的Action定义中，一样需要定义input的逻辑视图名，将input逻辑视图映射到login.jsp页面。

如果不输入用户名、密码而提交表单，将再次看到如图2.14所示的界面。在这种校验方式下，无需书写校验代码，只需要通过配置文件指定校验规则即可，因此提供了更好的可维护性。

2.9 本章小结

本章以一个HelloWorld应用为例，简要介绍了Struts 2 MVC框架的基本流程，从Action类基本流程控制讲起，详细介绍了如何开发一个Struts 2应用。本章的后面部分在基本Struts 2应用基础上，介绍了一些Struts 2的深入应用，包括在Action中访问HttpSession状态，将Action处理结果传回JSP页面显示，本应用也综合应用了Struts 2的标签库、数据校验、程序国际化等常用功能。通过阅读本章的内容，读者应该对Struts 2框架有一个大致的掌握。


本章要点

— Struts 1框架的基本知识

— 使用Struts 1框架开发Web应用

— WebWork框架的基本知识

— 使用WebWork框架开发Web应用

— 在Eclipse中整合Tomcat

— 使用Eclipse开发Web应用

— 为Web应用增加Struts 2支持

— Struts 2框架的MVC组件

— Struts 2框架的流程

— 通过web.xml文件加载Struts 2框架

— 通过struts.properties文件配置Struts 2属性

— struts.xml文件的结构

— 前面一章已经大致介绍了Struts 2应用的基本情况，通过前一章的学习，读者应该对Struts 2框架有了一个大致的掌握。但对于各知识点的细节，还需要进一步学习，本章将详细介绍Struts 2框架的基础部分。

因为Struts 2是在WebWork和Struts 1的基础上发展起来的，因此掌握一定的WebWork和Struts 1框架的知识，将对于掌握Struts 2框架大有裨益。特别是WebWork方面的开发经验，绝大部分都可以适用于Struts 2应用的开发。本章将会简要介绍Struts 1和WebWork框架的使用。

本章还将介绍在Eclipse IDE工具中开发Struts 2应用，并且详细介绍Struts 2配置文件的各个细节。

3.1 Struts 1的MVC实现

Struts 1是世界上最“古老”的MVC框架，它出现于2001年。Struts 1以ActionServlet作为核心控制器，由ActionServlet负责拦截用户的所有请求。Struts 1框架有3个重要组成部分：Action、ActionForm和ActionForward对象。

3.1.1 下载和安装Struts 1框架

本节的示例程序基于Struts 1的1.2.9版，请读者也下载该版本的Struts 1框架，下载和安装Struts 1框架请按如下步骤进行。

* 登录struts.apache.org站点，下载Struts的合适版本。解压缩下载到的压缩包，解压缩后，发现有如下文件结构：

— contrib：该路径下存放了Struts 1表达式语言支持的类库和标签库文件。

— lib：该路径下存放了Struts 1框架的核心类库。

— webapps：该路径下存放了Struts 1框架的示例应用及文档应用。

— 另外还一些关于Struts的说明和LICENSE等文档

将lib下的JAR文件全部复制到WEB-INF/lib下，并将TLD文件全部复制到WEB-INF下，再将validator-rules.xml文件也复制到WEB-INF下。如果需要用到Struts-EL（表达式）语言，还应将contrib下的JAR文件复制到WEB-INF/lib下。

为了在编译程序时可以导入Struts 1类库，还应将struts.jar文件增加到环境变量中。

3.1.2 实现ActionForm

Struts 1使用单独的ActionForm封装请求参数，当用户提交一个请求时，Struts 1将使用单独的ActionForm来封装所有的请求参数。从结构上看，ActionForm就是一个POJO：它需要为用户的每个请求参数都提供一个对应的属性，并为该属性设置相应的setter和getter方法——但ActionForm的设计不是POJO，它必须继承Struts 1的ActionForm基类。

下面是一个简单的登录页面，包含两个表单域，分别代表用户名和密码。该登录页面的代码如下：

<%@ page language="java"
contentType="text/html;charset=gb2312"
errorPage=""%>

<!-- 导入Struts的三个标签库-->

<%@include file="taglibs.jsp"%>

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=gb2312">

<!-- 使用国际化资源文件的key输出标题-->

<title><bean:message key="login"/></title>

</head>

<body>

<!-- 输出登录页面标题 -->

<h3><bean:message key="loginTitle"/></h3>

<!-- 输出Action处理完后的错误信息。 -->

<font color="red">${requestScope.err}</font>

<!-- 通过messagePresent判断输入校验后的出错提示是否存在 -->

<logic:messagesPresent>

<!-- 输出出错提示 -->

<bean:message key="errors.header"/>

<ul>

<html:messages id="error">

<li><bean:write name="error"/></li>

</html:messages>

</ul><hr />

</logic:messagesPresent>

<!-- 下面是登录表单 -->

<html:form action="login.do">

<!-- 使用国际化信息来输出标签 -->

<bean:message key="username"/><html:text property="username"/><br>

<bean:message key="pass"/><html:password property="pass"/><br>

<html:submit><bean:message key="login"/></html:submit><br>

</html:form>

</body>

</html>

文本框: 图3.1 系统登录的页面 在浏览器中浏览该页面时，看到该页面包含了一个简单的登录表单。该页面的界面如图3.1所示。

该页面中有两个表单域，这两个表单域封装了需要向服务器发送的请求参数，对于Strurts应用而言，请求参数是通过ActionForm来封装的，ActionForm非常类似一个POJO（定义几个属性，为每个属性提供setter和getter方法），但该ActionFrom需要继承ActionForm，或者是它的子类。因为本应用使用了Struts的校验框架，因此本应用中的ActionForm继承了ValidatorForm类。下面是本应用中ActionForm类的代码：

public class LoginForm extends ValidatorForm

{

//下面两个属性用于封装两个表单域的请求参数

private String username;

private String pass;

//以下是属性字段的系列get、set方法

public String getUsername()

{

return username;

}

public void setUsername(String username)

{

this.username = username;

}

public String getPass()

{

return pass;

}

public void setPass(String pass)

{

this.pass = pass;

}

}

上面ActionForm类的代码非常简单，除了该类需要继承ValidatorForm之外。也正是因为该ActionForm继承了ValidatorForm，从而导致了该类的污染，降低了该类的代码复用。

对于Struts 1而言，ActionForm类是一个既烦琐又没有技术含量的类，写起来难免让程序员觉得意兴索然。虽然有很多IDE（集成开发环境）可以自动生成该ActionForm类，但Struts 1框架也感觉到大量重复书写该类是一个负担，后来提供了动态ActionForm，让人可以避免书写ActionForm类。

注意 Struts 1中的ActionForm类是一个非常简单的类，它实质上是一个普通的JavaBean，但必须继承ActionForm类。除此之外，ActionForm还可以使用动态FormBean，从而允许通过配置文件来定义ActionForm。

3.1.3 实现Action

Action就是用于处理用户请求的业务控制器，当用户请求发送到ActionServlet后，ActionServlet拦截到用户请求，将请求转发到系统的业务控制器处理。ActionServlet在转发用户请求时，会将请求参数封装成ActionForm实例，并将该 ActionForm实例转发给Action实例。

Action实例从ActionForm中取出用户请求参数，然后调用业务逻辑组件处理用户请求，并根据处理结果，调用不同的视图页面来呈现处理结果。

下面是系统的Action处理类代码。

public class LoginAction extends Action

{

//必须重写该execute方法，该方法用于处理用户请求

public ActionForward execute(ActionMapping mapping, ActionForm form,

HttpServletRequest request, HttpServletResponse response)throws

Exception

{

//获取封装用户请求参数的ActionForm实例

LoginForm loginForm = (LoginForm)form;

//从ActionForm中取出用户请求参数

String username = loginForm.getUsername();

String pass = loginForm.getPass();

//处理用户请求

if (username != null && username.equals("scott"))

{

return mapping.findForward("welcome");

}

else

{

return mapping.findForward("error");

}

}

}

上面的Action代码非常简单，甚至没有调用任何业务逻辑组件，只是直接判断用户请求参数的用户名和密码是否为scott和tiger，如果用户名和密码正确，则返回welcome的ActionForward，否则返回error的ActionForward。

注意 ActionForward就是一个逻辑视图，通过在配置文件中定义ActionFoward的映射，完成逻辑视图名和实际视图资源之间的映射。

Struts 1的Action类与Struts 2的Action类有一定的类似性，都通过调用execute方法来处理用户请求。但最大的区别在于Struts 1 Action的execute方法与Servlet API耦合，但Struts 2 Action类的execute方法无需与Servlet API耦合。

3.1.4 配置Struts 1的Action

实现了Struts 1的Action后，还需要在struts-config.xml文件中配置Action，配置Action需要指定Action的实现类，以及Action处理请求的URL。

配置Action时，还应该配置该Action对应的ActionForm，每个ActionForm使用一个<form-bean.../>元素定义。定义Action和ActonForm之间的关联关系时，在定义Action的<action../>元素中通过name属性指定与此Action关联的ActionForm。

因为本应用还使用了Struts 1的数据校验框架，因此配置<action.../>元素时，还应该增加validate属性，并将该属性值设置成true；还需要增加input属性，该属性指向输入校验失败后转入的视图资源。

下面是本应用的struts-config.xml文件代码。

<?xml version="1.0" encoding="GBK"?>

<!-- 指定Struts 1配置文件的DTD信息 -->

<!DOCTYPE struts-config PUBLIC

"-//Apache Software Foundation//DTD Struts Configuration 1.2//EN"

"http://struts.apache.org/dtds/struts-config_1_2.dtd">

<!-- 指定Struts 1配置文件的根元素 -->

<struts-config>

<!-- 所有的ActionForm都应该在form-beans元素中定义 -->

<form-beans>

<!-- 每个form-bean定义一个ActionForm -->

<form-bean name="loginForm" type="lee.LoginForm"/>

</form-beans>

<!-- 所有的Action都放在action-mappings元素中定义 -->

<action-mappings>

<!-- 定义Action，其处理类为lee.LoginAction，对应的ActionForm为

loginForm -->

<action path="/login" type="lee.LoginAction" name="loginForm"

validate="true" scope="request" input="/login.jsp">

<!-- 当返回welcome的ActionForward时，转入welcome.jsp页 -->

<forward name="welcome" path="/welcome.jsp" />

<!-- 当返回error的ActionForward时，转入error.jsp页 -->

<forward name="error" path="/error.jsp"/>

</action>

</action-mappings>

<!-- 加载国际化资源文件 -->

<message-resources parameter="MyResource"/>

<!-- 加载Struts 1校验框架的插件 -->

<plug-in className="org.apache.struts.validator.ValidatorPlugIn">

<!-- 指定数据校验的两个校验规则文件 -->

<set-property property="pathnames" value="/WEB-INF/validator-rules.xml,

/WEB-INF/validation.xml" />

<set-property property="stopOnFirstError" value="true"/>

</plug-in>

</struts-config>

正如前面看到的，页面中使用大量国际化标签来输出国际化消息，因此本应用必须加载相应的国际化资源文件，本应用的国际化资源文件的baseName是MyResource。指定应用的国际化资源文件通过<message-resources .../>元素指定。

本应用使用了Struts 1的校验框架来完成输入校验，因此在本应用中加载了两个校验规则文件，其中validator-rules.xml文件是一个系统规则文件，通常无需修改；validation.xml文件是开发者定义的规则文件，该文件定义了需要校验的表单应该满足的规则。

下面是校验规则文件的代码。

<?xml version="1.0" encoding="GBK"?>

<!-- 校验规则定义文件 -->

<!DOCTYPE form-validation PUBLIC "-//Apache Software Foundation

//DTD Commons Validator Rules Configuration 1.1.3//EN"

"http://jakarta.apache.org/commons/dtds/validator_1_1_3.dtd">

<!-- 校验规则文件的根元素 -->

<form-validation>

<formset>

<!-- 定义需要校验的ActionForm，定义该ActionForm为loginForm -->

<form name="loginForm">

<!-- 定义username表单域必须满足必填、模式匹配两个规则 -->

<field property="username" depends="required,mask">

<arg key="username" position="0"/>

<!-- 指定模式匹配在正则表达式 -->

<var>

<var-name>mask</var-name>

<var-value>^\w+$</var-value>

</var>

</field>

<!-- 定义pass表单域必须满足必填、最小长度两个规则 -->

<field property="pass" depends="required,minlength">

<arg key="pass" position="0"/>

<!-- 定义最小长度的规则 -->

<arg name="minlength" key="${var:minlength}" resource="false"

position="1"/>

<var>

<var-name>minlength</var-name>

<var-value>4</var-value>

</var>

</field>

</form>

</formset>

</form-validation>

上面的规则文件定义了表单域的两个表单必须满足的规则：username是必填的，而且必须匹配^\w+$的正则表达式；pass是必填的，并且至少有4位。

定义了上面的校验规则后，如果用户的输入不能通过该输入校验，则系统自动转入配置<action .../>元素时指定的input属性所对应的视图资源。

输入校验的提示信息都保存在国际化资源文件中，因此使用输入校验时，通常都建议使用国际化资源文件来保存校验提示信息。

提示 使用输入校验时，通常建议使用国际化资源文件来保存校验失败的提示信息。

3.1.5 完成应用流程

经过上面步骤，可以非常清楚地看出上面示例应用的流程，用户进入系统的login.jsp页面，用户输入请求信息，发送请求。如果用户的请求参数不能通过输入校验，则系统将请求转发到login.jsp页面，否则请求将被转发到业务逻辑控制器Action处。

Action处理用户请求参数，如果处理结果为success的ActionForward，则进入welcome.jsp页面；如果处理结果为error的ActionForward，则进入error.jsp页面。

图3.2显示了该应用的顺序图。

图3.2 系统顺序图

如果用户输入的用户名正确、密码正确，用户登录成功，可以在welcome.jsp页面见到国际化的欢迎信息，并且通过Struts 1的标签输出登录的用户名。下面是welcome.jsp页面的代码。

<%@ page contentType="text/html;charset=gb2312" errorPage="error.jsp"%>

<!-- 导入Struts的三个标签库-->

<%@include file="taglibs.jsp"%>

<html>

<head>

<!-- 通过Struts 1标签输出国际化的页面标题 -->

<title><bean:message key="loginSuccess"/></title>

</head>

<body>

<h3><bean:message key="loginSuccess"/></h3>

<hr>

<!-- 通过Struts 1标签输出国际化的欢迎信息，并把登录用的用户名作为参数传入 -->

<bean:message key="welcome" arg0="${loginForm.username}"/>

</body>

</html>

如果用户登录成功，将看到如图3.3所示的页面。

图3.3 登录成功页面

上面的Struts 1应用已经包含了Struts 1框架的核心部分，如ActionForm、Action实现，数据校验完成，应用的国际化支持等。

通过上面的示例应用，我们看到Struts 1框架的最大缺点是：Struts 1应用的类与Servlet API耦合严重，而且大都需要继承Struts 1框架提供的基类，或实现Struts 1框架提供的接口，造成了代码污染。

3.2 WebWork的MVC实现

WebWork由OpenSymphony组织负责开发、维护，WebWork 2前身是Rickard Oberg开发的WebWork。现在我们所说的WebWork，通常指的是WebWork 2，它是由Xwork 1和WebWork 2两个项目组成的。

WebWork紧紧以ServletDispatcher为核心，使用ServletDispatcher来处理所有的用户请求，它是整个WebWork框架的核心控制器。WebWork支持多视图表示，视图部分可以使用JSP、Velocity、FreeMarker、JasperReports、XML等。WebWork当前的最新版本是2.2.5，本节所使用的范例是基于该版本的WebWork完成的。由于WebWork已经全面整合到Struts 2框架中，故WebWork不会再有新版本发布，即不会有WebWork 2.3.X发布。

3.2.1 WebWork的下载和安装

WebWork当前的最新稳定版本是2.2.5，笔者建议读者也下载该版本。WebWork的下载和安装按如下步骤进行。

* 登录http://www.opensymphony.com/webwork/站点，下载WebWork的最新稳定版，解压缩后发现如下的文件结构：

— dist：该路径下存放了WebWork的两个编译版本。

— docs：该路径下存放了WebWork的各种文档，包括WebWork的Wiki文档、API文档等。

— lib：该路径下存放了WebWork编译和运行所需的第三方类库，其中default路径下的所有JAR文件是WebWork应用必需的类库文件。

— src：该路径下存放了WebWork的源文件，如果使用WebWork过程中遇到无法解决的问题，可以参考该源文件解决。

— webapps：该路径下存放了WebWork的示例应用，这些示例应用是学习WebWork极好的资料。

— webwork-2.2.5.jar：该文件是WebWork框架的核心类库文件。

— webwork-src-2.2.5.jar：该压缩文件里包含了WebWork框架所有源代码。

— 此外还有一些WebWork框架的编译脚本文件及各种说明文件。

WebWork通常都作为Web框架使用，在Web应用中使用该框架。为Web应用增加WebWork框架的方法，与在Web应用中增加其他框架的方法完全一样。下面介绍如何安装WebWork框架。

将webwork-2.2.5.jar文件复制到Web应用的WEB-INF/lib路径下，再将lib\default路径下的*.jar文件全部复制到WEB-INF/lib下。

注意 使用WebWork框架必须有webwork-2.2.5.jar和lib\default路径下的全部JAR文件。至于lib路径下的其他JAR文件，则是可选的——只有当应用中需要使用这些第三方框架时，才需要这些类库文件。

为了可以编译WebWork应用程序，建议将webwork-2.2.5.jar增加到CLASSPATH的环境变量中。当然也可以使用Ant工具，则无需增加环境变量。

3.2.2 实现Action

用户需要实现的WebWork核心组件依然是业务控制器，业务控制器就是一个Action，该Action也包含一个execute方法，但该方法不再包含任何Servlet API，也不包含任何WebWork API。这个Action与Struts 1的Action不同，它非常类似于一个普通的POJO。

本应用的Action代码如下：

public class LoginAction extends ActionSupport

{

//下面两个属性用于封装用户请求参数

private String user;

private String pass;

//如下请求参数用于封装Action处理请求后的反馈

private String tip;

//user属性对应的setter和getter方法

public String getUser()

{

return user;

}

public void setUser(String user)

{

this.user = user;

}

//pass属性对应的setter和getter方法

public String getPass()

{

return pass;

}

public void setPass(String pass)

{

this.pass = pass;

}

//tip属性对应的setter和getter方法

public String getTip()

{

return tip;

}

//tip属性对应的setter和getter方法

public void setTip(String tip)

{

this.tip = tip;

}

//处理用户请求的execute方法

public String execute() throws Exception

{

if (getUser().equals("scott")

&& getPass().equals("tiger"))

{

setTip(getText("loginSuccess"));

return SUCCESS;

}

else

{

setTip(getText("loginFail"));

return ERROR;

}

}

}

上面Action类的代码与Struts 1的Action类存在如下几个区别：

— WebWork的Action类无需与Servlet API耦合，从而可以更方便地进行单元测试。

— WebWork的Action类可无需ActionForm的支持，它本身包含了两个属性用于封装用户的请求参数。

— WebWork的Action类里包含的属性不仅可以封装用户请求参数，还可以封装Action的处理结果。

— WebWork的Action的execute方法返回的逻辑视图是一个标准字符串，而不是一个ActionForward对象。

本应用中的Action继承了WebWork的ActionSupport基类，继承该基类导致了Action的代码污染，WebWork不强制要求Action类继承ActionSupport基类，但继承该基类可以更便捷地实现Action类，例如程序国际化。

上面的Action实现类中包含了两个国际化信息，这两个国际化信息对应的key分别为loginSuccess和loginFail。为了在Action类中访问这两个国际化信息，需要在Action类中使用getText方法，该方法可以输出对应key的国际化信息。该getText方法正是来自于ActionSupport基类的方法。

注意 WebWork的Action类不强制要求使用ActionForm类封装用户请求参数。但可使用模型驱动模式，在模型驱动模式下，Action不再负责封装请求参数，也不负责封装Action的处理结果。而是将请求参数和处理结果放在模型中封装——模型就是一个类似于ActionForm的对象，但该对象是一个POJO，无需继承任何WebWork基类。相对于直接使用Action封装请求参数的模式，模型驱动模式的编程更加复杂，因为需要额外增加一个模型类；但模型驱动模式更易理解：Action专门负责处理用户请求，而模型则专门负责封装数据。

因为上面的Action封装了user和pass两个请求参数，故对应的JSP页面也应该包含这样两个表单域。下面是对应JSP页面的代码。

<%@ page contentType="text/html;charset=GBK"%>

<!-- 导入WebWork的标签库文件 -->

<%@ taglib prefix="ww" uri="/webwork"%>

<html>

<head>

<!-- 使用国际化信息标签输出国际化标题 -->

<title><ww:text name="loginTitle"/></title>

</head>

<body>

<ww:text name="loginTitle"/>

<hr>

<!-- 使用WebWork标签生成登录表单 -->

<ww:form method="post" action="login">

<!-- 生成登录用的用户名文本框 -->

<ww:textfield name="user" label="%{getText('user')}"/>

<!-- 生成登录用的密码文本框 -->

<ww:textfield name="pass" label="%{getText('pass')}"/>

<!-- 生成登录按钮 -->

<ww:submit value="%{getText('submit')}"/>

</ww:form>

</body>

</html>

读者可能已经注意到，上面的表单定义中，大量使用了WebWork的标签库，借助于WebWork标签库，可以更简单的生成页面的表单元素。因此，上面页面中的表单定义中，甚至无需为表单域指定对应的Label信息，但可生成如下表单定义。图3.4显示了在浏览器中浏览该页面的效果。

当使用<ww:textfield name="user" label="%{getText ('user')}"/>来生成一个文本框时，该标签负责生成一个名为user的文本输入框，该输入框对应的Label通过%{getText('user')表达式生成输出，该表达式负责输出一个国际化信息，该国际化信息对应的key为user。

文本框: 图3.4 登录页面的效果 3.2.3 配置Action

实现上面的Action后，需要在应用中配置该Action。WebWork使用一个xwork.xml文件来配置Action。

在配置WebWork的Action之前，需要先配置Web应用加载WebWork框架。让Web应用加载WebWork框架就是配置WebWork的核心控制器，WebWork的核心控制器是ServletDispatcher。

因为Web应用默认只加载web.xml文件，因此为了加载WebWork框架，必须在web.xml文件中配置WebWork的核心控制器：ServletDispatcher。修改后的web.xml文件如下：

<?xml version="1.0" encoding="GBK"?>

<!-- 指定Web应用配置文件的Schema信息 -->

<web-app xmlns="http://java.sun.com/xml/ns/j2ee"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.

com/xml/ns/j2ee/web-app_2_4.xsd"

version="2.4">

<!-- 配置WebWork的核心Servlet -->

<servlet>

<!-- 指定WebWork核心Servlet的名字 -->

<servlet-name>webwork</servlet-name>

<!-- 指定WebWork核心Servlet的实现类 -->

<servlet-class>com.opensymphony.webwork.dispatcher.ServletDispatcher

</servlet-class>

<load-on-startup>2</load-on-startup>

</servlet>

<!-- 配置WebWork核心Servlet的映射 -->

<servlet-mapping>

<!-- 配置WebWork核心Servlet映射的URL -->

<servlet-name>webwork</servlet-name>

<url-pattern>*.action</url-pattern>

</servlet-mapping>

</web-app>

指定了上面的web.xml配置文件后，Web应用会自动加载WebWork框架，并加载Web应用的WEB-INF/classes路径下的xwork.xml文件。xwork.xml文件就是WebWork的配置文件，该文件主要配置应用的Action，以及Action对应的URL。

<?xml version="1.0" encoding="GBK"?>

<!-- 下面指定了WebWork配置文件的DTD信息 -->

<!DOCTYPE xwork PUBLIC

"-//OpenSymphony Group//XWork 1.1//EN"

"http://www.opensymphony.com/xwork/xwork-1.1.dtd">

<!-- xwork是WebWork配置文件的根元素 -->

<xwork>

<!-- 导入WebWork默认配置文件 -->

<include file="webwork-default.xml" />

<!-- WebWork默认将Action配置放在package下管理 -->

<package name="default" extends="webwork-default">

<!-- 指定login的Action，该Action的实现类为lee.LoginAction -->

<action name="Login" class="lee.LoginAction">

<!-- 指定input逻辑视图对应的物理视图资源 -->

<result name="input">/login.jsp</result>

<!-- 指定error逻辑视图对应的物理视图资源 -->

<result name="error">/error.jsp</result>

<!-- 指定success逻辑视图对应的物理视图资源 -->

<result name="success">/success.jsp</result>

</action>

</package>

</xwork>

在上面的xwork.xml配置文件中，所有的Action放在package元素下管理，因为WebWork以包来管理所有的Action，所以全部的action元素都作为package元素的子元素存在。

注意 WebWork以package来管理WebWork的Action，故所有的action元素都是package元素的子元素。

配置action元素时，必须指定两个主要属性：name属性和class属性。其中name属性指定了该Action负责处理的URL，而class属性指定了该Action对应的实现类。例如上面的Action配置，该Action的name为login，表示该Action负责处理向login.action的URL发送的请求，处理请求的Java类为lee.LoginAction。

实际上，WebWork可以包含两个配置文件，还有一个webwork.properties配置文件，这个文件是一个标准的属性文件，由一系列的WebWork属性的key和value组成。下面是webwork.properties文件的代码。

#指定该WebWork应用处于开发模式下，如果出错，将输出更多出错调试信息

webwork.devMode = true

#指定每次请求都重载WebWork的配置文件

webwork.configuration.xml.reload=true

#指定WebWork应用的国际化资源文件的baseName为mess

webwork.custom.i18n.resources=mess

因为本应用处于开发阶段，故设置了WebWork应用的开发模式为true，并且每次请求都会重载WebWork的配置文件。上面配置文件的最后一行指定了WebWork的国际化资源文件的baseName为mess。

上面配置WebWork应用时，指定了Action处理后逻辑视图名和物理视图之间的对应关系："input"字符串对应/login.jsp页面，"error"字符串对应/error.jsp页面，"success"字符串对应/success.jsp页面。对比上面的Action类代码，发现该类的execute方法分别返回了SUCCESS和ERROR两个字符串常量——它们的定义包含在ActionSupport类中，其值分别为"success"和"error"字符串。

这就是本系统流程：当用户输入的用户名为scott，且密码为tiger时，Action返回一个success的逻辑视图，该逻辑视图对应为/success.jsp页面；否则将进入/error.jsp页面。那么input逻辑视图的作用呢？input逻辑视图是输入校验失败后默认转入的视图页面。

提示 与Struts 1类似，WebWork采用input逻辑视图来作为输入校验失败后转入的视图。

3.2.4 完成数据校验

WebWork可以支持手动完成输入校验，但这不是本应用将采用的方式，因为手动完成输入校验需要重写Action类的validate()方法，需要大量书写重复代码，相当烦琐。本应用利用了XWork的校验框架来完成数据校验，完成数据校验只需要在Action class文件所在的路径下存放对应的校验文件即可。

下面是本应用的数据校验文件代码。

<?xml version="1.0" encoding="gb2312"?>

<!-- WebWork校验文件的DTD信息 -->

<!DOCTYPE validators PUBLIC "-//OpenSymphony Group//XWork

Validator 1.0.2//EN"

"http://www.opensymphony.com/xwork/xwork-validator-1.0.2.dtd">

<!-- validators是校验文件的根元素 -->

<validators>

<!-- 使用表单校验器校验user表单域 -->

<field name="user">

<!-- 指定user表单域必须满足requiredstring（必填）规则 -->

<field-validator type="requiredstring">

<!-- 违反输入校验后的提示信息 -->

<message key="user.required"/>

</field-validator>

</field>

<!-- 使用表单校验器校验pass表单域 -->

<field name="pass">

<!-- 指定pass表单域必须满足requiredstring（必填）规则 -->

<field-validator type="requiredstring">

<message key="pass.required"/>

</field-validator>

<!-- 指定pass表单域必须满足stringlength（长度范围）规则 -->

<field-validator type="stringlength">

<!-- 指定最小长度 -->

<param name="minLength">3</param>

<!-- 指定最大长度 -->

<param name="maxLength">6</param>

<message key="pass.length"/>

</field-validator>

</field>

</validators>

上面的校验文件使用了两个表单校验器：requiredstring和stringlength，分别对应用的两个表单域进行校验，分别校验用户名必须满足必填规则，密码必须满足必填且字符串长度必须在3和6之间的规则。

如果用户输入不满足该规则，则看到如图3.5所示的界面。

图3.5 输入校验的效果

通过上面的示范代码，可以看出，结合XWork的校验框架，可以大大简化WebWork应用的数据校验。

对比上面的Struts 1框架和WebWork框架的使用，不难发现WebWork与Struts 2的设计更类似。

3.2.5 完成应用流程

正如我们见到的，如果我们输入了合适的用户名和密码，Action处理完用户请求后，将会封装一个名为tip的属性在结果里，并转入success的逻辑视图，该逻辑视图对应了/success.jsp页面。该页面的代码非常简单，它只是简单地使用WebWork标签来输出tip的提示信息。页面代码如下：

<%@ page contentType="text/html;charset=GBK"%>

<!-- 导入WebWork的标签库 -->

<%@ taglib prefix="ww" uri="/webwork"%>

<html>

<head>

<!-- 输出tip提示信息 -->

<title><ww:property value="tip"/></title>

</head>

<body>

<h3><ww:property value="tip"/></h3>

<!-- 使用WebWork的国际化标签来输出国际化提示 -->

<ww:text name="welcomeMsg">

<!-- 传入国际化提示信息的参数 -->

<ww:param value="user"/>

</ww:text>

</body>

</html>

在上面的页面中，我们使用了<ww.text .../>标签来输出国际化消息，该标签输出的国际化消息如下：

welcomeMsg=欢迎您，{0}，您已经成功登录本系统！

看到上面的国际化消息需要传入一个参数，我们通过为<ww:text .../>标签指定一个<ww:param .../>子标签来传入参数：第一个<ww:param .../>子元素为第一个参数指定值，第二个<ww:param .../>子元素为第二个参数指定值……依此类推。

上面代码使用了<ww:param value="user"/>来传入参数，该参数的值就是Action中user属性值。当用户输入的用户名和密码正确时，将看到如图3.6所示的界面。

图3.6 登录成功的界面

3.3 在Eclipse中开发Struts 2

前面已经详细介绍了如何开发一个Struts 2应用，但没有使用任何IDE工具，那种开发方式虽然能让读者更清楚开发的细节，但对于开发效率却不如使用IDE工具的开发效率高。本节将以Eclipse为例，介绍如何在IDE工具中开发Struts 2应用。

3.3.1 创建Web应用

笔者所使用的Eclipse已经安装了MyEclipse插件，因此可以很方便地建立一个Web应用，并将其部署在Web服务器中。

下面以Tomcat 5520为例，介绍如何在Eclipse中整合Tomcat服务器，新建一个Web应用，并将该应用部署到Tomcat服务器中。

现在开始在Eclipse中整合Tomcat服务器，实际上，如果我们安装了MyEclipse插件，则可以在Eclipse中整合大部分的Java EE服务器（包括Web服务器）。但如果我们没安装任何插件，Eclipse则很难与任何Java EE服务器整合。

提示 Eclipse必须安装了相应插件后才具有丰富的功能。为了开发Java EE应用，通常都推荐安装MyEclipse插件，该插件是一个商业产品，必须支付相应费用才可使用。

为了在Eclipse中整合Tomcat服务器，请按如下步骤进行。

* 单击Eclipse窗口里的“Window”菜单，在出现的“Window”的系列菜单项中选择“Preferences”菜单项，将弹出如图3.7所示的界面。

该Preferences设置窗口是Eclipse大部分参数的设置窗口，该窗口还可设置Eclipse插件的相关属性。MyEclipse插件的相关属性就是在该窗口里设置的，而整合Java EE服务器只是MyEclipse插件的相关属性。

单击图3.7所示窗口左边导航树中的“MyEclipse”节点，然后单击“MyEclipse”节点下的“Application Servers”子节点，将看到如图3.8所示的界面。

图3.7 Eclipse的Preferences设置窗口

图3.8 在Eclipse中整合应用服务器

拖动图3.8所示窗口左边的垂直滚动条，我们可以发现大部分应用服务器的名字，例如Jboss、Jetty、Resin等，当然也可以发现我们需要使用的Tomcat节点，然后单击Tomcat 5节点，出现如图3.9所示的界面。

图3.9 整合Tomcat5

在图3.9右上方看到两个单选框：Enable和Disable，为了整合Tomcat，显然应该选中Enable单选框。选中该单选框后，还应该在下面的三个文本输入框内输入相应的地址，例如，Tomcat Home Directory里应该输入Tomcat的安装路径，笔者的Tomcat安装在D盘的tomcat5520路径下，故在该文本框内输入了D:\tomcat5520的内容。通常，一旦我们输入了正确的Tomcat安装路径，下面的两个文本框会自动输入，无需用户干预。

单击图3.9所示窗口右下方的“OK”按钮，返回Eclipse主界面，Eclipse和Tomcat的整合成功。

下面介绍如何在Eclipse中建立一个Web应用，在Eclipse中建立一个Web应用也可借助于MyEclipse的帮助。借助于MyEclipse建立Web应用按如下步骤进行。

* 单击Eclipse主界面中的“File”菜单，在出现的菜单中单击“New”下的“Other...”菜单项，将弹出一个如图3.10所示的对话框。

在如图3.10所示的对话框中，选中“Web Project”节点，表明将要创建一个Web应用，然后单击“Next”按钮，看到如图3.11所示的对话框。

图3.10 新建Web应用的对话框 图3.11 新建Web应用

在如图3.11所示对话框的Project Name文本框中输入应用的名字，本应用使用Struts2Demo作为应用的名字，输入完成后可看到如图3.11所示的对话框。单击“Finish”按钮，新建Web应用成功。

3.3.2 增加Struts 2支持

为了让Web应用具有Struts 2支持功能，必须将Struts 2框架的核心类库增加到Web应用中。将Struts 2框架下lib路径下的struts2-core-2.0.6.jar、xwork-2.0.1.jar等Struts 2框架的核心类库复制到Web应用的lib路径下，也就是“%workspace%Struts2Demo\WebRoot\ WEB-INF\lib”路径下。

返回Eclipse的主界面，在Eclipse主界面的左上角资源导航中看到了struts2Demo节点，选中该节点，然后按F5键，将看到Eclipse主界面左上角资源导航中出现如图3.12所示的界面。

看到如图3.12所示的界面，表明该Web应用已经加入了Struts 2的核心类库，但还需要修改web.xml文件，让该文件负责加载Struts 2框架。

文本框: 图3.12 添加了Struts 2类库 在如图3.12所示的导航树中，单击“WEB-INF”节点前的加号，展开该节点，看到该节点下包含的web.xml文件子节点。

单击web.xml文件节点，编辑该文件，编辑该文件没有丝毫特殊之处，同样是在web.xml文件中定义Struts 2的核心Filter，并定义该Filter所拦截的URL 模式。至此，该Web应用完全具备了Struts 2框架的支持。

3.3.3 部署Struts 2应用

本应用的功能非常类似于第2章所介绍的Struts 2应用，因此本节所使用的JSP页面与第2章所使用的JSP页面也非常类似，故此处不再给出这些JSP页面的代码。

在Eclipse中添加JSP页面也是很简单的，此处不再赘述。

建立了这些JSP页面后，单击Eclipse主界面上部署Web应用的工具按钮，部署Web应用和启动服务器的两个按钮如图3.13所示。

图3.13 在Eclipse中部署Web应用和启动服务器的按钮

在Eclipse中部署Web应用请按如下步骤进行。

* 单击部署Web应用的按钮，弹出如图3.14所示的对话框。

图3.14 部署Web应用的对话框

在图3.14所示对话框的上面看到选择项目的下拉列表，选中需要部署的Web应用，例如本示例就是Struts2Demo。单击右边的“Add”按钮，该按钮用于添加想要部署到的Web服务器，将看到如图3.15所示的对话框。

图3.15 部署到应用服务器

在图3.15所示对话框上面的Server下拉列表中选中“Tomcat 5”选项，其他选项设置参照图3.15，然后单击“Finish”按钮，返回图3.14所示对话框，单击“OK”按钮，Web应用部署成功。

Web应用部署成功后，单击如图3.13所示工具条中的“启动服务器”按钮旁的小三角，出现下拉菜单，选择“Tomcat 5”下的“Start”菜单项，如图3.16所示，启动Tomcat服务器。

* 在浏览器中访问刚才的Struts2Demo应用，将可看到本应用登录页面。以Tomcat的端口为8888为例，应该在浏览器中访问如下地址：http://localhost:8888/Struts2Demo/ login.jsp，将看到如图3.17所示的界面。

图3.16 启动服务器 图3.17 部署成功后的登录界面

在图3.17中看到该登录页面的文本框没有任何提示信息，按钮上也没有文本，这是因为本应用缺少国际化资源文件。增加该应用所需的国际化资源文件，该资源文件与前面应用的资源文件相同，此处不再给出。

为了让Struts 2应用加载该国际化资源文件，还应该使用struts.properties文件，该文件内指定如下一行代码：

#指定Struts 2的国际化资源文件的baseName为messageResource

struts.custom.i18n.resources=messageResource

至此，Struts 2应用已经获得了国际化支持。如果在浏览器中再次浏览该页面，将看到如图3.18所示的界面。

3.3.4 增加应用的Action

在Eclipse工具中新建一个Java类，该Java类的类名为“LoginAction”，其代码与第2章所使用的LoginAction类代码相同，将该类文件保存在Struts2Demo应用的src路径下的lee目录下。

图3.18 增加了国际化资源文件后的登录页面

新建一个Java文件的方法非常简单，此处不再赘述。本应用的Action类的代码与第2章的Action类完全一样，此处也不再给出。

增加了Struts 2的Action类后，还需要增加对应的配置文件，也是通过单击Eclipse的“File”菜单，然后单击“New”下的“File”菜单项来操作的。

提示 因为Eclipse暂时还没有专门的Struts 2插件，因此只能用最原始的方式来建立一个Struts 2的配置文件。等到专门的Struts 2插件被开发出来后，应该可以有更简单的方法来建立Struts 2的配置文件。

建立该文件后，为该文件增加Struts 2的Action定义，定义该Action与前面介绍的基本一致，需要定义Action的name属性、class属性等。增加Action定义后的struts.xml文件代码如下：

<?xml version="1.0" encoding="GBK"?>

<!-- 指定Struts 2配置文件的DTD信息 -->

<!DOCTYPE struts PUBLIC

"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"

"http://struts.apache.org/dtds/struts-2.0.dtd">

<!-- 指定Struts 2配置文件的根元素 -->

<struts>

<!-- 所有的Action定义都应该放在package下 -->

<package name="lee" extends="struts-default">

<!-- 定义Action -->

<action name="Login" class="lee.LoginAction">

<!-- 定义三个逻辑视图和物理资源之间的映射 -->

<result name="input">/login.jsp</result>

<result name="error">/error.jsp</result>

<result name="success">/welcome.jsp</result>

</action>

</package>

</struts>

至此，整个Struts 2应用完全建立成功，如果读者还需要增加数据校验，则可增加对应的校验文件，并将该文件放在与对应Action源文件的同一路径下即可。

3.4 Struts 2的基本流程

经过前面介绍，我们已经基本了解了Struts 2框架的MVC实现。大致上，Struts 2框架由3个部分组成：核心控制器FilterDispatcher、业务控制器和用户实现的业务逻辑组件。在这3个部分里，Struts 2框架提供了核心控制器FilterDispatcher，而用户需要实现业务控制器和业务逻辑组件。

3.4.1 核心控制器：FilterDispatcher

FilterDispatcher是Struts 2框架的核心控制器，该控制器作为一个Filter运行在Web应用中，它负责拦截所有的用户请求，当用户请求到达时，该Filter会过滤用户请求。如果用户请求以action结尾，该请求将被转入Struts 2框架处理。

Struts 2框架获得了*.action请求后，将根据*.action请求的前面部分决定调用哪个业务逻辑组件，例如，对于login.action请求，Struts 2调用名为login的Action来处理该请求。

Struts 2应用中的Action都被定义在struts.xml文件中，在该文件中定义Action时，定义了该Action的name属性和class属性，其中name属性决定了该Action处理哪个用户请求，而class属性决定了该Action的实现类。

Struts 2用于处理用户请求的Action实例，并不是用户实现的业务控制器，而是Action代理——因为用户实现的业务控制器并没有与Servlet API耦合，显然无法处理用户请求。而Struts 2框架提供了系列拦截器，该系列拦截器负责将HttpServletRequest请求中的请求参数解析出来，传入到Action中，并回调Action的execute方法来处理用户请求。

显然，上面的处理过程是典型的AOP（面向切面编程）处理方式。图3.19显示了这种处理模型。

图3.19 Struts 2的拦截器和Action

从图3.19中可以看出，用户实现的Action类仅仅是Struts 2的Action代理的代理目标。用户实现的业务控制器（Action）则包含了对用户请求的处理。用户的请求数据包含在HttpServletRequest对象里，而用户的Action类无需访问HttpServletRequest对象。拦截器负责将HttpServletRequest里的请求数据解析出来，并传给业务逻辑组件Action实例。

3.4.2 业务控制器

正如从图3.19所看到的，业务控制器组件就是用户实现Action类的实例，Action类里通常包含了一个execute方法，该方法返回一个字符串——该字符串就是一个逻辑视图名，当业务控制器处理完用户请求后，根据处理结果不同，execute方法返回不同字符串 ——每个字符串对应一个视图名。

程序员开发出系统所需要的业务控制器后，还需要配置Struts 2的Action，即需要配置Action的如下三个部分定义：

— Action所处理的URL。

— Action组件所对应的实现类。

— Action里包含的逻辑视图和物理资源之间的对应关系。

每个Action都要处理一个用户请求，而用户请求总是包含了指定URL。当Filter Dispatcher拦截到用户请求后，根据请求的URL和Action处理URL之间的对应关系来处理转发。

3.4.3 Struts 2的模型组件

实际上，模型组件已经超出了MVC框架的覆盖范围。对于Struts 2框架而言，通常没有为模型组件的实现提供太多的帮助。

文本框: 图3.20 控制器调用模型组件 Java EE应用里的模型组件，通常指系统的业务逻辑组件。而隐藏在系统的业务逻辑组件下面的，可能还包含了DAO、领域对象等组件。

通常，MVC框架里的业务控制器会调用模型组件的方法来处理用户请求。也就是说，业务逻辑控制器不会对用户请求进行任何实际处理，用户请求最终由模型组件负责处理。业务控制器只是中间负责调度的调度器，这也是称Action为控制器的原因。

图3.20显示了这种处理流程。

提示 在图3.20中看到Action调用业务逻辑组件的方法。当控制器需要获得业务逻辑组件实例时，通常并不会直接获取业务逻辑组件实例，而是通过工厂模式来获得业务逻辑组件的实例；或者利用其他IoC容器（如Spring容器）来管理业务逻辑组件的实例。

3.4.4 Struts 2的视图组件

Struts 2已经改变了Struts 1只能使用JSP作为视图技术的现状，Struts 2允许使用其他的模板技术，如FreeMarker、Velocity作为视图技术。

当Struts 2的控制器返回逻辑视图名时，逻辑视图并未与任何的视图技术关联，仅仅是返回一个字符串，该字符串作为逻辑视图名。

当我们在struts.xml文件中配置Action时，不仅需要指定Action的name属性和class属性，还要为Action元素指定系列result子元素，每个result子元素定义一个逻辑视图和物理视图之间的映射。前面所介绍的应用都使用了JSP技术作为视图，故配置result子元素时没有指定type属性，默认使用JSP作为视图资源。

如果需要在Struts 2中使用其他视图技术，则可以在配置result子元素时，指定相应的type属性即可。例如，如果需要使用FreeMarker，则为result指定值为freemarker的type属性；如果想使用Velocity模板技术作为视图资源，则为result指定值为velocity的type属性……

3.4.5 Struts 2的运行流程

经过上面介绍，我们发现Struts 2框架的运行流程非常类似于WebWork框架的流程。

提示 在Struts 2的官方站点，我们可以找到如下说法：Essentially，Struts 2.0 is the technical equivalent of WebWork 2.3。Aside from the package and property renaming，it isn't much different than，say，migrating from WebWork 2.1 to 2.2——意思是说：Struts 2.0技术等同于WebWork 2.3框架，除了包和属性被改名外。从WebWork 2.2迁移到Struts 2不会比从WebWork 2.1迁移到WebWork 2.2更复杂。

这里我们可以看到，Struts 2其实就是WebWork 2.2的升级版，这也就不难理解：为什么WebWork和Struts 2如此相似！

因此，Struts 2的运行流程与WebWork的运行流程完全相同，读者可以参看图1.8来了解Struts 2的运行流程。

3.5 Struts 2的基本配置

前面大致了解了Struts 2框架的基本内容，但这些基本内容都必须建立在Struts 2的配置文件基础之上，这些配置文件的配置信息也是Struts 2应用的核心部分。

3.5.1 配置web.xml文件

任何MVC框架都需要与Web应用整合，这就不得不借助于web.xml文件，只有配置在web.xml文件中Servlet才会被应用加载。

通常，所有的MVC框架都需要Web应用加载一个核心控制器，对于Struts 2框架而言，需要加载FilterDispatcher，只要Web应用负责加载FilterDispatcher，FilterDispatcher将会加载应用的Struts 2框架。

因为Struts 2将核心控制器设计成Filter，而不是一个普通Servlet。故为了让Web应用加载FilterDispacher，只需要在web.xml文件中配置FilterDispatcher即可。

配置FilterDispatcher的代码片段如下：

<!-- 配置Struts 2框架的核心Filter -->

<filter>

<!-- 配置Struts 2核心Filter的名字 -->

<filter-name>struts</filter-name>

<!-- 配置Struts 2核心Filter的实现类 -->

<filter-class>org.apache.struts2.dispatcher.FilterDispatcher

</filter-class>

<init-param>

<!-- 配置Struts 2框架默认加载的Action包结构 -->

<param-name>actionPackages</param-name>

<param-value>org.apache.struts2.showcase.person</param-value>

</init-param>

<!-- 配置Struts 2框架的配置提供者类 -->

<init-param>

<param-name>configProviders </param-name>

<param-value>lee.MyConfigurationProvider</param-value>

</init-param>

</filter>

正如上面看到的，当配置Struts 2的FilterDispatcher类时，可以指定一系列的初始化参数，为该Filter配置初始化参数时，其中有3个初始化参数有特殊意义：

— config：该参数的值是一个以英文逗号（,）隔开的字符串，每个字符串都是一个XML配置文件的位置。Struts 2框架将自动加载该属性指定的系列配置文件。

— actionPackages：该参数的值也是一个以英文逗号（,）隔开的字符串，每个字符串都是一个包空间，Struts 2框架将扫描这些包空间下的Action类。

— configProviders：如果用户需要实现自己的ConfigurationProvider类，用户可以提供一个或多个实现了ConfigurationProvider接口的类，然后将这些类的类名设置成该属性的值，多个类名之间以英文逗号（,）隔开。

除此之外，还可在此处配置Struts 2常量，每个<init-param>元素配置一个Struts 2常量，其中<param-name>子元素指定了常量name，而<param-value>子元素指定了常量value。

提示 关于Struts 2常量的讲解，请参阅本书的 4.1.2 节。

在web.xml文件中配置了该Filter，还需要配置该Filter拦截的URL。通常，我们让该Filter拦截所有的用户请求，因此使用通配符来配置该Filter拦截的URL。

下面是配置该Filter拦截URL的配置片段：

<!-- 配置Filter拦截的URL -->

<filter-mapping>

<!-- 配置Struts 2的核心FilterDispatcher拦截所有用户请求 -->

<filter-name>struts</filter-name>

<url-pattern>/*</url-pattern>

</filter-mapping>

配置了Struts 2的核心FilterDispatcher后，基本完成了Struts 2在web.xml文件中的配置。

如果Web应用使用了Servlet 2.3以前的规范，因为Web应用不会自动加载Struts 2框架的标签文件，因此必须在web.xml文件中配置加载Struts 2标签库。

配置加载Struts 2标签库的配置片段如下：

<!-- 手动配置Struts 2的标签库 -->

<taglib>

<!-- 配置Struts 2标签库的URI -->

<taglib-uri>/s</taglib-uri>

<!-- 指定Struts 2标签库定义文件的位置 -->

<taglib-location>/WEB-INF/struts-tags.tld</taglib-location>

</taglib>

在上面配置片段中，指定了Struts 2标签库配置文件物理位置：/WEB-INF/struts- tags.tld，因此我们必须手动复制Struts 2的标签库定义文件，将该文件放置在Web应用的WEB-INF路径下。

如果Web应用使用Servlet 2.4以上的规范，则无需在web.xml文件中配置标签库定义，因为Servlet 2.4规范会自动加载标签库定义文件。

提示 Struts 2的标签库定义文件包含在struts2-core-2.0.6.jar文件里，在struts2-core-2.0.6.jar文件的META-INF路径下，包含了一个struts-tag.tld文件，这个文件就是Struts 2的标签库定义文件，Servlet 2.4规范会自动加载该标签库文件。

对于Servlet 2.4以上的规范，Web应用自动加载该标签库定义文件。加载struts-tag.tld标签库定义文件时，该文件的开始部分包含如下代码片段：

<taglib>

<!-- 定义标签库的版本 -->

<tlib-version>2.2.3</tlib-version>

<!-- 定义标签库所需的JSP版 -->

<jsp-version>1.2</jsp-version>

<short-name>s</short-name>

<!-- 定义Struts 2标签库的URI -->

<uri>/struts-tags</uri>

...

</taglib>

因为该文件中已经定义了该标签库的URI：struts-tags，这就避免了在web.xml文件中重新定义Struts 2标签库文件的URI。

3.5.2 struts.xml配置文件

Struts框架的核心配置文件就是struts.xml配置文件，该文件主要负责管理Struts 2框架的业务控制器Action。

在默认情况下，Struts 2框架将自动加载放在WEB-INF/classes路径下的struts.xml文件。在大部分应用里，随着应用规模的增加，系统中Action数量也大量增加，导致struts.xml配置文件变得非常臃肿。

为了避免struts.xml文件过于庞大、臃肿，提高struts.xml文件的可读性，我们可以将一个struts.xml配置文件分解成多个配置文件，然后在struts.xml文件中包含其他配置文件。

下面的struts.xml文件中就通过include手动导入了一个配置文件：struts-part1.xml文件，通过这种方式，就可以将Struts 2的Action按模块配置在多个配置文件中。

<?xml version="1.0" encoding="UTF-8" ?>

<!-- 指定Struts 2配置文件的DTD信息 -->

<!DOCTYPE struts PUBLIC

"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"

"http://struts.apache.org/dtds/struts-2.0.dtd">

<!-- 下面是Struts 2配置文件的根元素 -->

<struts>

<!-- 通过include元素导入其他配置文件 -->

<include file="struts-part1.xml" />

...

</struts>

通过这种方式，Struts 2提供了一种模块化的方式来管理struts.xml配置文件。

除此之外，Struts 2还提供了一种插件式的方式来管理配置文件。用WinRAR等解压缩软件打开struts2-core-2.0.6.jar文件，看到如图3.21所示的文件结构，在光标选中的一行，看到有一个struts-default.xml文件。

图3.21 struts2-core-2.0.6.jar压缩文件的文件结构

查看struts-default.xml文件，看到该文件代码片段如下：

<?xml version="1.0" encoding="UTF-8" ?>

<!-- 指定Struts 2配置文件的DTD信息 -->

<!DOCTYPE struts PUBLIC

"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"

"http://struts.apache.org/dtds/struts-2.0.dtd">

<!-- Struts 2配置文件的根元素 -->

<struts>

<!-- 下面定义了Struts 2框架的一些基础Bean -->

<bean class="com.opensymphony.xwork2.ObjectFactory" name="xwork" />

<bean type="com.opensymphony.xwork2.ObjectFactory" name="struts"

class="org.apache.struts2.impl.StrutsObjectFactory" />

....

<!-- 下面是一些静态注入Bean定义 -->

<bean class="com.opensymphony.xwork2.util.OgnlValueStack" static="true" />

<bean class="org.apache.struts2.dispatcher.Dispatcher" static="true" />

...

<!-- 下面定义Struts 2的默认包空间 -->

<package name="struts-default">

<!-- 定义Struts 2内建支持的结果类型 -->

<result-types>

<!-- 定义Action链Result类型 -->

<result-type name="chain" class="com.opensymphony.xwork2.

ActionChainResult"/>

<!-- 定义Dispatcher的Result类型，并设置default="true"，

指定该结果Result是默认的Result类型 -->

<result-type name="dispatcher"

class="org.apache.struts2.dispatcher.Servlet

DispatcherResult" default="true"/>

<!-- 定义FreeMarker的Result类型 -->

<result-type name="freemarker"

class="org.apache.struts2.views.freemarker.

FreemarkerResult"/>

...

</result-types>

<!-- 定义Struts 2内建的拦截器 -->

<interceptors>

<interceptor name="alias" class="com.opensymphony.xwork2.

interceptor.AliasInterceptor"/>

<interceptor name="autowiring"

class="com.opensymphony.xwork2.spring.interceptor.

ActionAutowiringInterceptor"/>

...

<!-- 定义基本拦截器栈 -->

<interceptor-stack name="basicStack">

<interceptor-ref name="exception"/>

<interceptor-ref name="servlet-config"/>

<interceptor-ref name="prepare"/>

<interceptor-ref name="checkbox"/>

<interceptor-ref name="params"/>

<interceptor-ref name="conversionError"/>

</interceptor-stack>

<!-- 还有系列拦截器栈 -->

...

</interceptors>

<!-- 定义默认的拦截器栈引用 -->

<default-interceptor-ref name="defaultStack"/>

</package>

</struts>

上面的代码并未全部列出struts-default.xml文件，只是列出了每个元素的代表。上面配置文件中定义了一个名字为struts-default的包空间，该包空间里定义了Struts 2内建的Result类型，还定义了Struts 2内建的系列拦截器，以及由不同拦截器组成的拦截器栈，文件的最后还定义了默认的拦截器引用。

这个struts-default.xml文件是Struts 2框架的默认配置文件，Struts 2框架每次都会自动加载该文件。查看我们前面使用的struts.xml文件，看到我们自己定义的package元素有如下代码片段：

<!-- 指定Struts 2配置文件的根元素 -->

<struts>

<!-- 配置名为lee的包空间，继承struts-default包空间 -->

<package name="lee" extends="struts-default">

...

</package>

</struts>

在上面配置文件中，名为lee的包空间，继承了名为struts-default的包空间，struts-default包空间定义在struts-default.xml文件中。可见，Struts 2框架默认会加载struts-default.xml文件。

不仅如此，Struts 2框架提供了一种类似Eclipse的扩展方式，它允许以一种“可插拔”的方式来安装插件，例如后面将要介绍的Spring插件、JSF插件等，它们都提供了一个类似struts2-Xxx-plugin.jar的文件——这个文件就是插件安装文件，只要将该文件放在Web应用的WEB-INF/lib路径下，Struts 2框架将自动加载该框架。

使用WinRAR工具打开struts2-spring-plugin2.06.jar文件，找到一个struts-plugin.xml文件，打开该文件，该文件的代码如下：

<?xml version="1.0" encoding="UTF-8" ?>

<!-- 指定Struts 2的DTD信息 -->

<!DOCTYPE struts PUBLIC

"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"

"http://struts.apache.org/dtds/struts-2.0.dtd">

<struts>

<!-- 定义一个名字为spring的ObjectFactory -->

<bean type="com.opensymphony.xwork2.ObjectFactory" name="spring"

class="org.apache.struts2.spring.StrutsSpringObjectFactory" />

<!-- 指定名字为sping的ObjectFactory是Struts 2的ObjectFactory -->

<constant name="struts.objectFactory" value="spring" />

<!-- 定义名为spring-default的包空间 -->

<package name="spring-default">

<!-- 定义整合Spring框架所必需的拦截器列表 -->

<interceptors>

<interceptor name="autowiring"

class="com.opensymphony.xwork2.spring.interceptor.

ActionAutowiringInterceptor"/>

<interceptor name="sessionAutowiring"

class="org.apache.struts2.spring.interceptor.

SessionContextAutowiringInterceptor"/>

</interceptors>

</package>

</struts>

在上面配置文件中，配置了Struts 2与Spring框架整合必需的常量、拦截器等——这也是Struts 2框架使用Spring插件必需的配置。只要将struts2-spring-plugin2.06.jar文件放在WEB-INF/lib路径下，Struts 2框架将自动加载该文件。

提示 如果用户开发属于自己的Struts 2插件，只要将对应的struts-plugin. xml文件放在JAR文件中，Struts 2框架将自动加载该文件。通过这种方式，Struts 2框架允许使用可插拔的方式管理Struts 2的插件。

3.5.3 struts.properties配置文件

Struts 2框架有两个核心配置文件，其中struts.xml文件主要负责管理应用中的Action映射，以及该Action包含的Result定义等。除此之外，Struts 2框架还包含一个struts.properties文件，该文件定义了Struts 2框架的大量属性，开发者可以通过改变这些属性来满足应用的需求。

struts.properties文件是一个标准的Properties文件，该文件包含了系列的key-value对象，每个key就是一个Struts 2属性，该key对应的value就是一个Struts 2属性值。

struts.properties文件通常放在Web应用的WEB-INF/classes路径下。实际上，只要将该文件放在Web应用的CLASSPATH路径下，Struts 2框架就可以加载该文件。

现在的问题是，struts.properties文件的哪些key是有效的？即struts.properties文件里包含哪些属性是有效的Struts 2属性。下面列出了可以在struts.properties中定义的Struts 2属性。

— struts.configuration：该属性指定加载Struts 2配置文件的配置文件管理器。该属性的默认值是org.apache.Struts2.config.DefaultConfiguration，这是Struts 2默认的配置文件管理器。如果需要实现自己的配置管理器，开发者则可以实现一个实现Configuration接口的类，该类可以自己加载Struts 2配置文件。

— struts.locale：指定Web应用的默认Locale。

— struts.i18n.encoding：指定Web应用的默认编码集。该属性对于处理中文请求参数非常有用，对于获取中文请求参数值，应该将该属性值设置为GBK或者GB2312。

提示 当设置该参数为GBK时，相当于调用HttpServletRequest的setCharacterEncoding方法。

— struts.objectFactory：指定Struts 2默认的ObjectFactory Bean，该属性默认值是spring。

— struts.objectFactory.spring.autoWire：指定Spring框架的自动装配模式，该属性的默认值是name，即默认根据Bean的name属性自动装配。

— struts.objectFactory.spring.useClassCache：该属性指定整合Spring框架时，是否缓存Bean实例，该属性只允许使用true和false两个属性值，它的默认值是true。通常不建议修改该属性值。

— struts.objectTypeDeterminer：该属性指定Struts 2的类型检测机制，通常支持tiger和notiger两个属性值。

— struts.multipart.parser：该属性指定处理multipart/form-data的MIME类型（文件上传）请求的框架，该属性支持cos、pell和jakarta等属性值，即分别对应使用cos的文件上传框架、pell上传及common-fileupload文件上传框架。该属性的默认值为jakarta。

注意 如果需要使用cos或者pell的文件上传方式，则应该将对应的JAR文件复制到Web应用中。例如，使用cos上传方式，则需要自己下载cos框架的JAR文件，并将该文件放在WEB-INF/lib路径下。

— struts.multipart.saveDir：该属性指定上传文件的临时保存路径，该属性的默认值是javax.servlet.context.tempdir。

— struts.multipart.maxSize：该属性指定Struts 2文件上传中整个请求内容允许的最大字节数。

— struts.custom.properties：该属性指定Struts 2应用加载用户自定义的属性文件，该自定义属性文件指定的属性不会覆盖struts.properties文件中指定的属性。如果需要加载多个自定义属性文件，多个自定义属性文件的文件名以英文逗号（,）隔开。

— struts.mapper.class：指定将HTTP请求映射到指定Action的映射器，Struts 2提供了默认的映射器：org.apache.struts2.dispatcher.mapper.DefaultActionMapper。默认映射器根据请求的前缀与Action的name属性完成映射。

— struts.action.extension：该属性指定需要Struts 2处理的请求后缀，该属性的默认值是action，即所有匹配*.action的请求都由Struts 2处理。如果用户需要指定多个请求后缀，则多个后缀之间以英文逗号（,）隔开。

— struts.serve.static：该属性设置是否通过JAR文件提供静态内容服务，该属性只支持true和false属性值，该属性的默认属性值是true。

— struts.serve.static.browserCache：该属性设置浏览器是否缓存静态内容。当应用处于开发阶段时，我们希望每次请求都获得服务器的最新响应，则可设置该属性为false。

— struts.enable.DynamicMethodInvocation：该属性设置Struts 2是否支持动态方法调用，该属性的默认值是true。如果需要关闭动态方法调用，则可设置该属性为false。

— struts.enable.SlashesInActionNames：该属性设置Struts 2是否允许在Action名中使用斜线，该属性的默认值是false。如果开发者希望允许在Action名中使用斜线，则可设置该属性为true。

— struts.tag.altSyntax：该属性指定是否允许在Struts 2标签中使用表达式语法，因为通常都需要在标签中使用表达式语法，故此属性应该设置为true，该属性的默认值是true。

— struts.devMode：该属性设置Struts 2应用是否使用开发模式。如果设置该属性为true，则可以在应用出错时显示更多、更友好的出错提示。该属性只接受true和flase两个值，该属性的默认值是false。通常，应用在开发阶段，将该属性设置为true，当进入产品发布阶段后，则该属性设置为false。

— struts.i18n.reload：该属性设置是否每次HTTP请求到达时，系统都重新加载资源文件。该属性默认值是false。在开发阶段将该属性设置为true会更有利于开发，但在产品发布阶段应将该属性设置为false。

提示 开发阶段将该属性设置了true，将可以在每次请求时都重新加载国际化资源文件，从而可以让开发者看到实时开发效果；产品发布阶段应该将该属性设置为false，是为了提供响应性能，每次请求都需要重新加载资源文件会大大降低应用的性能。

— struts.ui.theme：该属性指定视图标签默认的视图主题，该属性的默认值是xhtml。

— struts.ui.templateDir：该属性指定视图主题所需要模板文件的位置，该属性的默认值是template，即默认加载template路径下的模板文件。

— struts.ui.templateSuffix：该属性指定模板文件的后缀，该属性的默认属性值是ftl。该属性还允许使用ftl、vm或jsp，分别对应FreeMarker、Velocity和JSP模板。

— struts.configuration.xml.reload：该属性设置当struts.xml文件改变后，系统是否自动重新加载该文件。该属性的默认值是false。

— struts.velocity.configfile：该属性指定Velocity框架所需的velocity.properties文件的位置。该属性的默认值为velocity.properties。

— struts.velocity.contexts：该属性指定Velocity框架的Context位置，如果该框架有多个Context，则多个Context之间以英文逗号（,）隔开。

— struts.velocity.toolboxlocation：该属性指定Velocity框架的toolbox的位置。

— struts.url.http.port：该属性指定Web应用所在的监听端口。该属性通常没有太大的用户，只是当Struts 2需要生成URL时（例如Url标签），该属性才提供Web应用的默认端口。

— struts.url.https.port：该属性类似于struts.url.http.port属性的作用，区别是该属性指定的是Web应用的加密服务端口。

— struts.url.includeParams：该属性指定Struts 2生成URL时是否包含请求参数。该属性接受none、get和all三个属性值，分别对应于不包含、仅包含GET类型请求参数和包含全部请求参数。

— struts.custom.i18n.resources：该属性指定Struts 2应用所需要的国际化资源文件，如果有多份国际化资源文件，则多个资源文件的文件名以英文逗号（,）隔开。

— struts.dispatcher.parametersWorkaround：对于某些Java EE服务器，不支持HttpServlet Request调用getParameterMap()方法，此时可以设置该属性值为true来解决该问题。该属性的默认值是false。对于WebLogic、Orion和OC4J服务器，通常应该设置该属性为true。

— struts.freemarker.manager.classname：该属性指定Struts 2使用的FreeMarker管理器。该属性的默认值是org.apache.struts2.views.freemarker.FreemarkerManager，这是Struts 2内建的FreeMarker管理器。

— struts.freemarker.wrapper.altMap：该属性只支持true和false两个属性值，默认值是true。通常无需修改该属性值。

— struts.xslt.nocache：该属性指定XSLT Result是否使用样式表缓存。当应用处于开发阶段时，该属性通常被设置为true；当应用处于产品使用阶段时，该属性通常被设置为false。

— struts.configuration.files：该属性指定Struts 2框架默认加载的配置文件，如果需要指定默认加载多个配置文件，则多个配置文件的文件名之间以英文逗号（,）隔开。该属性的默认值为struts-default.xml,struts-plugin.xml,struts.xml，看到该属性值，读者应该明白为什么Struts 2框架默认加载struts.xml文件了。

在有些时候，开发者不喜欢使用额外的struts.properties文件，Struts 2允许在struts.xml文件中管理Struts 2属性，在struts.xml文件中通过配置constant元素，一样可以配置这些属性。

struts.xml配置片段如下：

<?xml version="1.0" encoding="UTF-8" ?>

<!-- 指定Struts 2的DTD信息 -->

<!DOCTYPE struts PUBLIC

"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"

"http://struts.apache.org/dtds/struts-2.0.dtd">

<struts>

<!-- 通过constant元素配置Struts 2的属性 -->

<constant name=" struts.custom.i18n.resources " value="mess" />

...

</struts>

上面代码片段配置了一个常用属性：struts.custom.i18n.resources，该属性指定了应用所需的国际化资源文件的baseName为mess。

提示 Struts 2提供了两种方式来管理Struts 2的属性：既可以通过struts.properties文件来配置Struts 2属性，也可通过在struts.xml文件中配置constant元素来配置Struts 2属性。

3.5.4 struts.xml文件结构

struts.xml文件是整个Struts 2框架的核心，下面提供了一个最完整的struts.xml文件，这个文件没有任何实际意义，仅仅是一个struts.xml文件示范。

<?xml version="1.0" encoding="GBK"?>

<!-- 下面指定Struts 2配置文件的DTD信息 -->

<!DOCTYPE struts PUBLIC

"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"

"http://struts.apache.org/dtds/struts-2.0.dtd">

<!-- struts是Struts 2配置文件的根元素 -->

<struts>

<!-- 下面元素可以出现0次，也可以无限多次 -->

<constant name="" value="" />

<!-- 下面元素可以出现0次，也可以无限多次 -->

<bean type="" name="" class="" scope="" static="" optional="" />

<!-- 下面元素可以出现0次，也可以无限多次 -->

<include file="" />

<!-- package元素是Struts配置文件的核心，该元素可以出现0次，或者无限多次 -->

<package name="必填的包名" extends="" namespace="" abstract=""

externalReferenceResolver>

<!-- 该元素可以出现，也可以不出现，最多出现一次 -->

<result-types>

<!-- 该元素必须出现，可以出现无限多次-->

<result-type name="" class="" default="true|false">

<!-- 下面元素可以出现0次，也可以无限多次 -->

<param name="参数名">参数值</param>*

</result-type>

</result-types>

<!-- 该元素可以出现，也可以不出现，最多出现一次 -->

<interceptors>

<!-- 该元素的interceptor元素和interceptor-stack至少出现其中之一，

也可以二者都出现 -->

<!-- 下面元素可以出现0次，也可以无限多次 -->

<interceptor name="" class="">

<!-- 下面元素可以出现0次，也可以无限多次 -->

<param name="参数名">参数值</param>*

</interceptor>

<!-- 下面元素可以出现0次，也可以无限多次 -->

<interceptor-stack name="">

<!-- 该元素必须出现，可以出现无限多次-->

<interceptor-ref name="">

<!-- 下面元素可以出现0次，也可以无限多次 -->

<param name="参数名">参数值</param>*

</interceptor-ref>

</interceptor-stack>

</interceptors>

<!-- 下面元素可以出现0次，也可以无限多次 -->

<default-interceptor-ref name="">

<!-- 下面元素可以出现0次，也可以无限多次 -->

<param name="参数名">参数值</param>

</default-interceptor-ref>

<!-- 下面元素可以出现0次，也可以无限多次 -->

<default-action-ref name="">

<!-- 下面元素可以出现0次，也可以无限多次 -->

<param name="参数名">参数值</param>*

</default-action-ref>?

<!-- 下面元素可以出现0次，也可以无限多次 -->

<global-results>

<!-- 该元素必须出现，可以出现无限多次-->

<result name="" type="">

<!-- 该字符串内容可以出现0次或多次 -->

映射资源

<!-- 下面元素可以出现0次，也可以无限多次 -->

<param name="参数名">参数值</param>*

</result>

</global-results>

<!-- 下面元素可以出现0次，也可以无限多次 -->

<global-exception-mappings>

<!-- 该元素必须出现，可以出现无限多次-->

<exception-mapping name="" exception="" result="">

异常处理资源

<!-- 下面元素可以出现0次，也可以无限多次 -->

<param name="参数名">参数值</param>*

</exception-mapping>

</global-exception-mappings>

<action name="" class="" method="" converter="">

<!-- 下面元素可以出现0次，也可以无限多次 -->

<param name="参数名">参数值</param>*

<!-- 下面元素可以出现0次，也可以无限多次 -->

<result name="" type="">

映射资源

<!-- 下面元素可以出现0次，也可以无限多次 -->

<param name="参数名">参数值</param>*

</result>

<!-- 下面元素可以出现0次，也可以无限多次 -->

<interceptor-ref name="">

<!-- 下面元素可以出现0次，也可以无限多次 -->

<param name="参数名">参数值</param>*

</interceptor-ref>

<!-- 下面元素可以出现0次，也可以无限多次 -->

<exception-mapping name="" exception="" result="">

异常处理资源

<!-- 下面元素可以出现0次，也可以无限多次 -->

<param name="参数名">参数值</param>*

</exception-mapping>

</action>

</package>*

<struts>

上面的struts.xml配置文件是一个非常全面的配置文件，包含了Struts 2的全部配置元素。上面配置文件主要用于帮助对XML DTD不太熟悉的读者。

实际上，如果读者需要熟悉XML文件的DTD（元素类型定义）语法，通过查看Struts 2配置文件的DTD信息，同样可以了解Struts 2配置文件的结构。下面是Struts 2配置文件的DTD文件代码。

<!--

Struts configuration DTD.

Use the following DOCTYPE

<!DOCTYPE struts PUBLIC

"-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"

"http://struts.apache.org/dtds/struts-2.0.dtd">

-->

<!ELEMENT struts (package|include|bean|constant)*>

<!ELEMENT package (result-types?, interceptors?, default-interceptor-ref?, default-action-ref?, global-results?, global-exception-mappings?, action*)>

<!ATTLIST package

name CDATA #REQUIRED

extends CDATA #IMPLIED

namespace CDATA #IMPLIED

abstract CDATA #IMPLIED

externalReferenceResolver NMTOKEN #IMPLIED

>

<!ELEMENT result-types (result-type+)>

<!ELEMENT result-type (param*)>

<!ATTLIST result-type

name CDATA #REQUIRED

class CDATA #REQUIRED

default (true|false) "false"

>

<!ELEMENT interceptors (interceptor|interceptor-stack)+>

<!ELEMENT interceptor (param*)>

<!ATTLIST interceptor

name CDATA #REQUIRED

class CDATA #REQUIRED

>

<!ELEMENT interceptor-stack (interceptor-ref+)>

<!ATTLIST interceptor-stack

name CDATA #REQUIRED

>

<!ELEMENT interceptor-ref (param*)>

<!ATTLIST interceptor-ref

name CDATA #REQUIRED

>

<!ELEMENT default-interceptor-ref (param*)>

<!ATTLIST default-interceptor-ref

name CDATA #REQUIRED

>

<!ELEMENT default-action-ref (param*)>

<!ATTLIST default-action-ref

name CDATA #REQUIRED

>

<!ELEMENT global-results (result+)>

<!ELEMENT global-exception-mappings (exception-mapping+)>

<!ELEMENT action (param|result|interceptor-ref|exception-mapping)*>

<!ATTLIST action

name CDATA #REQUIRED

class CDATA #IMPLIED

method CDATA #IMPLIED

converter CDATA #IMPLIED

>

<!ELEMENT param (#PCDATA)>

<!ATTLIST param

name CDATA #REQUIRED

>

<!ELEMENT result (#PCDATA|param)*>

<!ATTLIST result

name CDATA #IMPLIED

type CDATA #IMPLIED

>

<!ELEMENT exception-mapping (#PCDATA|param)*>

<!ATTLIST exception-mapping

name CDATA #IMPLIED

exception CDATA #REQUIRED

result CDATA #REQUIRED

>

<!ELEMENT include (#PCDATA)>

<!ATTLIST include

file CDATA #REQUIRED

>

<!ELEMENT bean (#PCDATA)>

<!ATTLIST bean

type CDATA #IMPLIED

name CDATA #IMPLIED

class CDATA #REQUIRED

scope CDATA #IMPLIED

static CDATA #IMPLIED

optional CDATA #IMPLIED

>

<!ELEMENT constant (#PCDATA)>

<!ATTLIST constant

name CDATA #REQUIRED

value CDATA #REQUIRED

>

通过上面给出的struts.xml配置文件范例，以及Struts 2配置文件的DTD信息，我们已经可以全面掌握Struts 2配置文件的文件结构了，但struts.xml文件各配置元素的意义、各属性的相关含义依然不能深入了解。关于Struts 2配置文件各元素及各属性将在下一章深入介绍。

3.6 本章小结

因为Struts 2是以Struts 1和WebWork为基础的，故本章大致介绍了Struts 1和WebWork框架的MVC实现，并在此基础上侧重于介绍Struts 1和WebWork与Struts 2的延续性，以及Struts 2框架与Struts 1和WebWork的类似性。通过阅读本章，读者应该明白：Struts 2是通过WebWork发展起来的，而不是通过Struts 1发展起来的。

本章还介绍了如何通过Eclipse IDE工具来开发Struts 2应用，Struts 2的配置文件，以及通过web.xml文件加载Struts 2框架。详细介绍了struts.properties文件，以及struts.xml配置文件的结构。关于深入配置struts.xml文件的元素，则放在下一章介绍


Note: 不久前写了一篇文章《使用Restful2ActionMapper让Struts2支持REST风格的URL映射》，但后来发现有些不对，Struts2的Restful2ActionMapper并不按我想的那样运行

Note: 不久前写了一篇文章《使用Restful2ActionMapper让Struts2支持REST风格的URL映射》，但后来发现有些不对，Struts2的Restful2ActionMapper并不按我想的那样运行。因为在我的实验项目中，我是改写了这个Restful2ActionMapper的。Struts2自己带的Restful2ActionMapper稍嫌复杂，而且我对它的有些地方的处理不甚满意，所以自己写了一个，没有使用Struts2自己的Restful2ActionMapper进行调试。特此向大家道歉，并修正该文档。另外，我对我改写的这个Restful2ActionMapper的代码进行了一些删减调整，将代码附于文后。

一、概述

REST是由 Roy Fielding 在他的论文《Architectural Styles and the Design of Network-based Software Architectures》中提出的一个术语。关于REST，请参考：http://www.redsaga.com/opendoc/REST_cn.pdf
在REST的定义中，一个Web应用总是使用固定的URI向外部世界呈现（或者说暴露）一个资源，并使用不同的HTTP请求方法来处理对资源的CRUD（创建、读取、更新和删除）操作。除了我们所熟知的GET和POST这两种HTTP请求方法，HTTP还有HEAD、PUT、DELETE等请求方法。我们在Web应用中处理来自客户端的请求时，通常只考虑GET和POST，并使用某种URL映射将URL映射到对资源的某种操作。而REST架构风格则要求使用HTTP的GET、POST、PUT、DELETE来分别表示资源的读取、创建、更新、删除，而URI保持不变。举例来说，/article/2007/8/a001这个URI表示一篇文章，表示形式为：/article/{year}/{month}/{id}，对这个资源的CRUD操作如下（下面的表示形式中，我省去了http://host/context/namespace这样的前缀）：
读取：GET /article/2007/8/a001
创建：POST /article/2007/8/a001
更新：PUT /article/2007/8/a001
删除：DELETE /article/2007/8/a001
如果我们用传统的struts或webwork的开发方法，我们可能会定义一个ArticleAction，定义好CRUD的method，并使用不同的URI映射来表示这几种操作。比如，我们可能会使用这样的URI来读取article：/getArticle.action?year=2007&month=8&id=a001，并使用这样的URI来删除article：/deleteArticle.action?year=2007&month=8&id=a001，或者，把这几种操作用相同形式的URI来表示：/article.action?method=get&year=2007&month=8&id=a001。显然，REST风格的URI表示更友好。
Struts2和Webwork2都带了一个RestfulActionMapper来支持REST风格的URI映射，但是它的功能太弱了，表现形式也很呆板。Struts2（我使用的是Struts 2.0.9）中还有一个Restful2ActionMapper，可以更好地支持REST风格。
从struts2的官方文档中可以找到关于Restful2ActionMapper的说明: Improved restful action mapper that adds several ReST-style improvements to action mapping, but supports fully-customized URL's via XML.
我查看了Restful2ActionMapper的源码，对它有些地方的处理有异议，所以改写了这个类。以下的配置中，请使用文后附上的Restful2ActionMapper代替Struts2原来的类。

二、配置和使用

现在，我们配置struts2使它使用Restful2ActionMapper。在Web项目中，修改struts.properties文件（它最终会发布到你的web应用的WEB-INF/classes目录中）：
struts.mapper.class=org.apache.struts2.dispatcher.mapper.Restful2ActionMapper
struts.enable.SlashesInActionNames=true
当然，你也可以在struts.xml里进行配置，请参考struts2的相关文档。
（这里，请将struts.mapper.class这行修改为使用我修改后的Restful2ActionMapper）

    这里有个小建议，许多人在WEB-INF/web.xml里对struts2的配置是让struts2处理所有扩展名为action的url，也就是设置url-pattern为*.action。 我的建议是，不要使用扩展名来作为url-pattern，使用基于路径的匹配形式会更好，我一般是使用“/app/*”作为url-pattern。至于扩展名，我一般是在struts.properties文件中指定：
struts.action.extension=html,xml,json
    或者，不要扩展名：
struts.action.extension=
    不过，这些都是题外话。

现在，以上面讲到的article为例，我们定义ArticleAction。按照Restful2ActionMapper的规则，URL与method的对应关系如下：
GET /article => public String index(); 资源索引；
GET /article/2007/8/a001 => public String view(); 对应于读取操作；
POST /article/2007/8/a001 => public String create(); 创建资源；
PUT /article/2007/8/a001 => public String update(); 更新资源；
DELETE /article/2007/8/a001 => public String remove(); 删除资源；
GET /article/2007/8/a001!edit => public String edit(); 请求编辑资源，和REST的四种操作没有对应关系；
GET /article/!editNew => public String editNew(); 请求编辑新资源，和REST的四种操作没有对应关系。
后两种方式似乎和REST没什么关系，但为传统的Web应用开发提供了方便。比如edit()，服务器返回一个表单页面。但是，如果我们让应用服务器只返回xml或json，那么这个edit()是可以不要的，有读取操作就够了。(也许把view方法改为read更贴切点)。
按照这些规则，我们在ArticleAction中定义view()、create()、update()、remove()等method，并在这个action中定义year、month、id的getter/setter方法：

java 代码

  1. package app;
  3. public class ArticleAction {
  4. private String year, month, id;
  5. ...
  6. getter/setter methods for year,month,id
  7. ...
  8. public String view() { ... }
  9. public String index() { ... }
  10. public String create() { ... }
  11. public String update() { ... }
  12. public String remove() { ... }
  13. }

然后我们需要配置这个action，使它能与形如/article/{year}/{month}/{id}的URL对应起来。我们在相应的struts2的action配置文件中加入如下几行：

xml 代码

  1. <action name="article/*/*/*" className="app.ArticleAction">
  2. <param name="year">{1}</param>
  3. <param name="month">{2}</param>
  4. <param name="id">{3}</param>
  5. <result name="..." type="..."/>
  6. </action>

OK！现在已经可以使用这个action了。当然，这还需要浏览器客户端的支持。当你的客户端以GET来请求/article/2007/8/a001时，struts2就会调用ArticleAction的view方法，而PUT请求则会对应到update方法，DELETE请求会对应到remove方法...
但是，如果你的客户端只支持GET和POST怎么办？Restful2ActionMapper的文档中提到：To simulate the HTTP methods PUT and DELETE, since they aren't supported by HTML, the HTTP parameter "__http_method" will be used.对于只支持GET和POST的传统网页，我们可以增加一个"__http_method"参数来模拟PUT和DELETE，比如：POST /article/2007/8/a001&__http_method=DELETE。随着Javascript和Ajax框架的发展，我们已经可以使用PUT和DELETE等方法。Ajax使用XmlHttpRequest进行操作时，在发送请求之前，可以通过设置RequestType的方式来完成对请求方法的设定。

三、不足之处

Restful2ActionMapper对REST风格的支持是不完全的。在REST风格中，我们可以使用同一个URI来获取同一个资源的多种表现形式。在发送HTTP请求时，只要我们在请求头中指定一个Accept参数，那么服务器就可以通过判断该参数来决定返回什么类型的数据。如果Accept为text/xml，服务器会返回xml格式的数据，如果Accept为text/json，则会返回json格式的数据，但URI是固定的。而Restful2ActionMapper只是作了URI的映射，并没有考虑返回数据的格式问题。要让struts2支持完全的REST风格，我们不得不对它进行改造，或者，等待它的改进。
另外，Restful2ActionMapper所定义的URL映射规则也有一个小小的“陷阱”。比如，GET /user/1表示读取id为1的user，但按照Restful2ActionMapper的定义，/user/new会对应到action的editNew方法，如果这个"new"就是某个用户的id呢？为了避开这个陷阱，我宁愿使用/user/!editNew这种丑陋的形式。事实上，随着客户端技术的发展，我们完全可以不使用editNew方法而构造输入页面，然后向服务器发送POST来创建资源。同样，edit方法也不是必要的。
（注：我修改后的Restful2ActionMapper去除了/user/new这种形式的映射）

四、其它

有个struts2的插件，叫jsonplugin，可以让struts2很方便地支持json输出。而Adobe Spry Framework、YUI-ext、DOJO等都能很好地支持json，并能很好地支持HTTP的各种请求方法。我推荐struts2的用户使用jsonplugin和Adobe Spry Framework或YUI-ext(或其它UI Framework)。Struts2只输出json格式的结果（最好还能输出xml），而UI和数据装配交给Adobe Spry/YUI-ext等去做。这样的组合会让你更好更方便地使用REST风格。

五、修改后的Restful2ActionMapper

这里我附上修改后的Restful2ActionMapper，大家可以在此基础上进行扩充。比如，我前面提到Restful2ActionMapper不能根据Accept请求头来返回不同格式的数据，其实也是可以进行改进的。我看到已经有人在读过我这篇文章后提出一种方案，类似于这样的：
/user/a001/xml => 返回xml格式的result
/user/a001/json => 返回json格式的result
/user/a001/...
这是一种办法，另外，根据url的扩展名来做，也是一种办法。但是这都不是好方案！我前面已经提过，按照REST风格，一个Web应用总是使用固定的URI向外部世界呈现（或者说暴露）一个资源，而前面这两种方案只是使URL友好点而已，并不真正符合REST风格。当然，这样也不错了，也是不错的方案，其实ROR中也有类似的做法。
但我们还有更好的方案，我提个思路，然后大家自行对Restful2ActionMapper进行改进：
在Action中可以设置一个consumeMime属性，并写好对应的getter/setter方法。在Restful2ActionMapper返回mapping之前，提取request的Accept头信息，然后将该信息放到mapping.params之中。action的各个method最后只返回consumeMime，这样就可以在action的配置文件中按consumeMime来配置result了。
下面，附上修改后的Restful2ActionMapper代码：

java 代码

  1. import com.opensymphony.xwork2.config.ConfigurationManager;
  2. import javax.servlet.http.HttpServletRequest;
  3. import org.apache.commons.logging.Log;
  4. import org.apache.commons.logging.LogFactory;
  5. import org.apache.struts2.dispatcher.mapper.ActionMapping;
  7. public class Restful2ActionMapper extends DefaultActionMapper {
  9. protected static final Log LOG = LogFactory
  10. .getLog(Restful2ActionMapper.class);
  11. public static final String HTTP_METHOD_PARAM = "__http_method";
  12. private static final byte HTTP_METHOD_GET = 1;
  13. private static final byte HTTP_METHOD_POST = 2;
  14. private static final byte HTTP_METHOD_PUT = 3;
  15. private static final byte HTTP_METHOD_DELETE = 4;
  17. public Restful2ActionMapper() {
  18. setSlashesInActionNames("true");
  19. }
  21. /*
  22. * (non-Javadoc)
  23. *
  24. * @see org.apache.struts2.dispatcher.mapper.ActionMapper#getMapping(javax.servlet.http.HttpServletRequest)
  25. */
  26. public ActionMapping getMapping(HttpServletRequest request,
  27. ConfigurationManager configManager) {
  29. if (!isSlashesInActionNames()) {
  30. throw new IllegalStateException(
  31. "This action mapper requires the setting 'slashesInActionNames' to be set to 'true'");
  32. }
  33. ActionMapping mapping = super.getMapping(request, configManager);
  35. if (mapping == null)
  36. return null;
  38. String actionName = mapping.getName();
  39. if ((actionName == null) || (actionName.length() == 0))
  40. return mapping;
  42. // If a method hasn't been explicitly named, try to guess using
  43. // ReST-style patterns
  44. if (mapping.getMethod() != null)
  45. return mapping;
  47. int lastSlashPos = actionName.lastIndexOf('/');
  48. if (lastSlashPos == -1)
  49. return mapping;
  50. String requestMethod = request.getMethod();
  51. String httpMethodParam = request.getParameter(HTTP_METHOD_PARAM);
  52. byte requestMethodCode = 0;
  53. if ("PUT".equalsIgnoreCase(requestMethod))
  54. requestMethodCode = HTTP_METHOD_PUT;
  55. else if ("DELETE".equalsIgnoreCase(requestMethod))
  56. requestMethodCode = HTTP_METHOD_DELETE;
  57. else if ("PUT".equalsIgnoreCase(httpMethodParam))
  58. requestMethodCode = HTTP_METHOD_PUT;
  59. else if ("DELETE".equalsIgnoreCase(httpMethodParam))
  60. requestMethodCode = HTTP_METHOD_DELETE;
  61. else if ("GET".equalsIgnoreCase(requestMethod))
  62. requestMethodCode = HTTP_METHOD_GET;
  63. else if ("POST".equalsIgnoreCase(requestMethod))
  64. requestMethodCode = HTTP_METHOD_POST;
  66. if (requestMethodCode == HTTP_METHOD_GET) {
  67. if (lastSlashPos == actionName.length() - 1)
  68. mapping.setMethod("index");
  69. else
  70. mapping.setMethod("view");
  71. } else if (requestMethodCode == HTTP_METHOD_POST)
  72. mapping.setMethod("create");
  73. else if (requestMethodCode == HTTP_METHOD_PUT)
  74. mapping.setMethod("update");
  75. else if (requestMethodCode == HTTP_METHOD_DELETE)
  76. mapping.setMethod("remove");
  78. return mapping;
  79. }
  80. }