转自:http://blog.csdn.net/huahaoing/article/details/6268444
Struts作为MVC 2的Web框架,自推出以来不断受到开发者的追捧,得到用广泛的应用。作为最成功的Web框架,Struts自然拥有众多的优点:
· MVC 2模型的使用
· 功能齐全的标志库(Tag Library)
· 开放源代码
但是,所谓“金无赤金,人无完人”,Struts自身也有不少的缺点:
· 需要编写的代码过多,容易引起“类爆炸”
· 单元测试困难
这些缺点随着Web的发展越来越明显。这就促生了Struts 2.0,它的诞生能很好的解决上述问题。 好啦,废话就不多说了,现在就让我们感受一下的Struts 2.0的魅力吧。
- 搭建开发和运行环境
- 到Apache下载Struts 2.0包
- 打开Eclipse 3.2新建Web工程
点击菜单File/New/Project,出现如图1所示对话框
图1 新建工程对话框
选择Web/Dynamic Web Project,点击“Next”,出现图2对话框
图2 新建动态Web工程对话框
在“Project Name”中键入Struts2_HelloWorld,点击“New”,出现以下对话框
图3 新建服务器运行时对话框
选择“Apache/Apache Tomat v5.5”,点击“Next”,出现以下对话框
图4新建服务器运行时对话框
点击“Finish”,关闭对话框。
-
- 将Struts 2.0 lib下的jar文件加到工程的构建路径(build path)
图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子窗口
-
- 打开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>
-
- 新建struts.xml文件
右键点击,Struts2_HelloWorld/Java Resources: src,出现如图7所示菜单
图7 新建Other菜单
点击“Other”,出现新建对话框,如图8所示
图8 新建对话框
点击“Next”,出现新建文件对话框,如图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>
-
- 新建index.html文件
右键点击Struts2_HelloWorld/WebContent,出现如图10所示的菜单
图10 新建Other菜单
点击“Other”,出现新建对话框,如图11所示
图11 新建对话框
选择Web/HTML,点击“Next”出现如图12所示的对话框
图12 新建HTML页面对话框
在“File Name”中键入index.html,点击“Next”,出现如图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>
-
- 将应用程序打包到tomcat上
右键点击Struts_HelloWorld,出现如图14所示的菜单
图14 工程菜单
点击“Export/WAR file”,出现如图15所示的对话框
图15 输出对话框
选择“Web/WAR file”,点击“Next”,出现如图16所示的对话框
图16 输出路径对话框
输入war文件的路径(如%tomcat%/webapps/Struts2_HelloWorld.war),点击“Finish”关闭对话框。
-
- 启动tomcat,运行应用程序
打开你的Internet Explorer,键入http://localhost:8080/Struts2_HelloWorld/,窗口输出如图17所示
图17 Hello World窗口
- 第一个Struts 2.0应用程序——Hello World
- 新建类包(package)
右键点击Struts2_HelloWorld/Java Resources: src,出现如图18所示菜单
图18 新建菜单"
点击“New/Package”,出现如图19所示对话框
图19新建Java类包对话框
在“Name”键入tutorial,点击“Finish”关闭对话框。
-
- 新建HelloWorld.java文件
右键点击Struts2_HelloWorld/Java Resources: src/tutorial,出现如图20所示菜单
图20 新建菜单
点击“New/Class”,出现如图21所示对话框
图21 新建Java类对话框
在“Name”中键入HelloWorld,在“Superclass”中键入com.opensymphony.xwork2.ActionSupport,点击“Finish”关闭对话框。将HelloWorld.java的内容修改为:
package tutorial;
import com.opensymphony.xwork2.ActionSupport;
public class HelloWorld extends ActionSupport {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String execute() {
name = "Hello, " + name + "!";
return SUCCESS;
}
}
-
- 在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>
-
- 新建SayHello.jsp
参考“新建index.html文件”步骤,弹出如图22所示对话框
图22 新建对话框
点击“Next”, 进入下一步,如图23所示
图23 新建JSP对话框
在“File name”键入SayHello.jsp,点击“Next”进入下一步,如图24所示
图24 模板选择对话框
点击“Finish”关闭对话框,并将SayHello.jsp的内容修改为:
<%@ 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>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>
-
- 新建HelloWorld.jsp(请参考上一步),HelloWorld.jsp的内容为:
<%@ 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>Hello</title>
</head>
<body>
<h3><s:property value="name" /></h3>
</body>
</html>
-
- 重新打包发布应用程序
先停止tomcat, 再将tomcat里webapps下的Struts2_HelloWorld.war和Struts2_HelloWorld文件夹删除,参照“将应用程序打包到tomcat上”重新发布应用程序。
-
- 启动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 新建菜单
点击“Next/Other”
图28 新建对话框
选择“Java/JUnit/JUnit Test Case”,点击“Next”
图29 新建JUnit 测试用例对话框
选择“New JUnit 4 test”,在“Name”中键入HelloWorldTest,在“Class under test”键入tutorial.HelloWorld,点击“Next”
图30 选择方法对话框
选中HelloWorld/execute方法,点击Finish。如果生成的HelloWorldTest.java文件的图标(Icon)出现红色交叉标志,请进行以下步骤添加JUnit 4的jar包。
右键点击Struts2_HelloWorld,出现如图所示菜单。 图31 新建菜单
点击“Build Path/Add Libararis”,弹出图32对话框
图32 添加库对话框
选中“JUnit”,点击“Next”
图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 {
@Test
public void testExecute() {
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 运行为菜单
点击“Run As/JUnit Test”,出现JUnit子窗口如图35所示
图35 JUnit子窗口
图35的绿色矩形表示,所有单元测试通过。
- 总结
上面的例子简单地演示了,Web 应用程序的基本操作,也即是,页面输入->Action处理->再输出到另外页面。Struts 2.0的简单易用、方便测试相信也会给大家留下不错的印象吧。我相信,Struts 2.0作为一个全新的Web架构,将会再次掀起Web开发的热潮。 不过,Struts 2.0还在测试中,正式版的发布还需些时日,所以文档方面可能有所欠缺。请大家继续留意我的博客,我会尽我所能为大家写更多关于Struts 2.0的文章。
在上一篇文章《为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的代码的顶部加入以下代码可以做到这点。 |
- 非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) --> |
参数:
名称 | 必需 | 默认 | 类型 | 描述 |
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。
- UI标志
UI标志又可以分为表单UI和非表单UI两部分。表单UI部分基本与Struts 1.x相同,都是对HTML表单元素的包装。不过,Struts 2.0加了几个我们经常在项目中用到的控件如:datepicker、doubleselect、timepicker、optiontransferselect等。因为这些标志很多都经常用到,而且参数也很多,要在一篇文章详细说明并非易事。
需要深入了解这些标志的朋友,可以到以下查看以下网址: |
在此,我虽然不能够详细讲述这些标志,但是可以与大家分享一个来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 {
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() {
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() {
return leftSideCartoonCharacters;
}
public void setLeftSideCartoonCharacters(List leftSideCartoonCharacters) {
this.leftSideCartoonCharacters = leftSideCartoonCharacters;
}
public List getRightSideCartoonCharacters() {
return rightSideCartoonCharacters;
}
public void setRightSideCartoonCharacters(List rightSideCartoonCharacters) {
this.rightSideCartoonCharacters = rightSideCartoonCharacters;
}
public String getFavouriteVehicalType() {
return favouriteVehicalType;
}
public void setFavouriteVehicalType(String favouriteVehicalType) {
this.favouriteVehicalType = favouriteVehicalType;
}
public String getFavouriteVehicalSpecific() {
return favouriteVehicalSpecific;
}
public void setFavouriteVehicalSpecific(String favouriteVehicalSpecific) {
this.favouriteVehicalSpecific = favouriteVehicalSpecific;
}
public List getVehicalTypeList() {
return vehicalTypeList;
}
public List getVehicalSpecificList() {
OgnlValueStack stack = ServletActionContext.getValueStack(ServletActionContext.getRequest());
Object vehicalType = stack.findValue("top");
if (vehicalType != null && vehicalType instanceof VehicalType) {
List l = (List) vehicalSpecificMap.get(vehicalType);
return l;
}
return Collections.EMPTY_LIST;
}
public List getFavouriteLanguages() {
return favouriteLanguages;
}
public String execute() throws Exception {
return SUCCESS;
}
/**//* Getters and Setters */
public String doSubmit() {
return SUCCESS;
}
// === inner class
public static class Language {
String description;
String key;
public Language(String key, String description) {
this.key = key;
this.description = description;
}
public String getKey() {
return key;
}
public String getDescription() {
return description;
}
}
public static class VehicalType {
String key;
String description;
public VehicalType(String key, String description) {
this.key = key;
this.description = description;
}
public String getKey() { return this.key; }
public String getDescription() { return this.description; }
public boolean equals(Object obj) {
if (! (obj instanceof VehicalType)) {
return false;
}
else {
return key.equals(((VehicalType)obj).getKey());
}
}
public int hashCode() {
return key.hashCode();
}
}
public static class VehicalSpecific {
String key;
String description;
public VehicalSpecific(String key, String description) {
this.key = key;
this.description = description;
}
public String getKey() { return this.key; }
public String getDescription() { return this.description; }
public boolean equals(Object obj) {
if (! (obj instanceof VehicalSpecific)) {
return false;
}
else {
return key.equals(((VehicalSpecific)obj).getKey());
}
}
public int hashCode() {
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 οnclick="alert('aaaa');" />
<s:reset οnclick="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.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 {
private String message;
public String getMessage() {
return message;
}
@Override
public String execute() {
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输出页面
参考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
默认情况下,当请求HelloWorld.action发生时,Struts运行时(Runtime)根据struts.xml里的Action映射集(Mapping),实例化tutoiral.HelloWorld类,并调用其execute方法。当然,我们可以通过以下两种方法改变这种默认调用。这个功能(Feature)有点类似Struts 1.x中的LookupDispathAction。
- 在classes/sturts.xml中新建Action,并指明其调用的方法;
- 访问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调用方法页面
通过上面的两个例子,细心的朋友应该可能会发现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中添加以下几个包: |
前面,我花了不少的时间讨论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 {
private String name;
private String password;
private String message;
public String getName() {
return name;
}
publicvoid setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
publicvoid setPassword(String password) {
this.password = password;
}
public String getMessage() {
return message;
}
@Override
public String execute() {
if("max".equals(name) &&"Secret".equals(password)) {
message ="Welcome, "+ name;
}else{
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成功页面
在浏览器地址栏中键入http://localhost:8080/Struts2_Action/Login.jsp,分别在User name中输入“Scott”和“password”,点击“Submit”按钮,出现如图6所示页面。
图6 Login失败页面
Struts 2.0更厉害的是支持更高级的POJO访问,如user.getPassword()。我们可以用另一写法实现例4。首先,将name和password从Login类中分离出来,到新建类User中。这样对我们开发多层系统尤其有用。它可以使系统结构更清晰。
package tutorial;
import com.opensymphony.xwork2.ActionSupport;
publicclass LoginX extends ActionSupport {
private User user;
private String message;
publicvoid setUser(User user) {
this.user = user;
}
public User getUser() {
return user;
}
public String getMessage() {
return message;
}
@Override
public String execute() {
if("max".equals(user.getName()) &&"secret".equals(user.getPassword())) {
message ="Welcome, "+ user.getName();
}else{
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方式。
- 非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)的对象。
- 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 {
private String message;
public String getMessage() {
return message;
}
@Override
public String execute() {
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 {
private String message;
private Map att;
private HttpServletRequest request;
private HttpServletResponse response;
public String getMessage() {
return message;
}
publicvoid setSession(Map att) {
this.att = att;
}
publicvoid setServletRequest(HttpServletRequest request) {
this.request = request;
}
publicvoid setServletResponse(HttpServletResponse response) {
this.response = response;
}
@Override
public String execute() {
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")等同。 |
国际化是商业系统中不可或缺的一部分,所以无论您学习的是什么Web框架,它都是必须掌握的技能。
其实,Struts 1.x在此部分已经做得相当不错了。它极大地简化了我们程序员在做国际化时所需的工作,例如,如果您要输出一条国际化的信息,只需在代码包中加入FILE-NAME_xx_XX.properties(其中FILE-NAME为默认资源文件的文件名),然后在struts-config.xml中指明其路径,再在页面用<bean:message>标志输出即可。
不过,所谓“没有最好,只有更好”。Struts 2.0并没有在这部分止步,而是在原有的简单易用的基础上,将其做得更灵活、更强大。
国际化Hello World
下面让我们看一个例子——HelloWorld。这个例子演示如何根据用户浏览器的设置输出相应的HelloWorld。
- 在Eclipse创建工程配置开发和运行环境(如果对这个步骤有问题,可以参考我早前的文章《为Struts 2.0做好准备》)。
- 在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)的配置 |
- 在src文件夹中加入globalMessages_en_US.properties文件,内容如下:
HelloWorld=Hello World!
- 在src文件夹中加入globalMessages_zh_CN.properties文件,内容如下:
HelloWorld=你好,世界!
在此想和大家分享一个不错的编写properties文件的Eclipse插件(plugin),有了它我们在编辑一些简体中文、繁体中文等Unicode文本时,就不必再使用native2ascii编码了。您可以通过Eclipse中的软件升级(Software Update)安装此插件,步骤如下: 1、展开Eclipse的Help菜单,将鼠标移到Software Update子项,在出现的子菜单中点击Find and Install; |
- 在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>
- 发布运行应用程序,在浏览器地址栏中输入http://localhost:8080/Struts2_i18n/HelloWorld.jsp ,出现图1所示页面。
图1 中文输出 - 将浏览器的默认语言改为“英语(美国)”,刷新页面,出现图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中有下面几种方法:
- 使用全局的资源文件,方法如上例所示。这适用于遍布于整个应用程序的国际化字符串,它们在不同的包(package)中被引用,如一些比较共用的出错提示;
- 使用包范围内的资源文件。做法是在包的根目录下新建名的package.properties和package_xx_XX.properties文件。这就适用于在包中不同类访问的资源;
- 使用Action范围的资源文件。做法为Action的包下新建文件名(除文件扩展名外)与Action类名同样的资源文件。它只能在该Action中访问。如此一来,我们就可以在不同的Action里使用相同的properties名表示不同的值。例如,在ActonOne中title为“动作一”,而同样用title在ActionTwo表示“动作二”,节省一些命名工夫;
- 使用<s:i18n>标志访问特定路径的properties文件。使用方法请参考我早前的文章《常用的Struts 2.0的标志(Tag)介绍》。在您使用这一方法时,请注意<s:i18n>标志的范围。在<s:i18n name="xxxxx">到</s:i18n>之间,所有的国际化字符串都会在名为xxxxx资源文件查找,如果找不到,Struts 2.0就会输出默认值(国际化字符串的名字)。
上面我列举了四种配置和访问资源的方法,它们的范围分别是从大到小,而Struts 2.0在查找国际化字符串所遵循的是特定的顺序,如图3所示:
图3 资源文件查找顺序图
假设我们在某个ChildAction中调用了getText("user.title"),Struts 2.0的将会执行以下的操作:
- 查找ChildAction_xx_XX.properties文件或ChildAction.properties;
- 查找ChildAction实现的接口,查找与接口同名的资源文件MyInterface.properties;
- 查找ChildAction的父类ParentAction的properties文件,文件名为ParentAction.properties;
- 判断当前ChildAction是否实现接口ModelDriven。如果是,调用getModel()获得对象,查找与其同名的资源文件;
- 查找当前包下的package.properties文件;
- 查找当前包的父包,直到最顶层包;
- 在值栈(Value Stack)中,查找名为user的属性,转到user类型同名的资源文件,查找键为title的资源;
- 查找在struts.properties配置的默认的资源文件,参考例1;
- 输出user.title。
参数化国际化字符串
许多情况下,我们都需要在动行时(runtime)为国际化字符插入一些参数,例如在输入验证提示信息的时候。在Struts 2.0中,我们通过以下两种方法做到这点:
- 在资源文件的国际化字符串中使用OGNL,格式为${表达式},例如:
validation.require=${getText(fileName)} is required
- 使用java.text.MessageFormat中的字符串格式,格式为{ 参数序号(从0开始), 格式类形(number | date | time | choice), 格式样式},例如:
validation.between=Date must between {0, date, short} and {1, date, short}
在显示这些国际化字符时,同样有两种方法设置参数的值:
- 使用标志的value0、value1...valueN的属性,如:
<s:text name="validation.required" value0="User Name"/>
- 使用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 {
public Map<String, Locale> getLocales() {
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"
οnchange="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来访问页面呢? |
转换器(Converter)——Struts 2.0中的魔术师
在我已往的Struts 1.x项目经验中,有个问题不时的出现——在创建FormBean时,对于某个属性到底应该用String还是其它类型?
开发Web应用程序与开发传统桌面应用程序不同,Web应用程序实际上是分布个不同的主机(当然也可以同一个主机,不过比较少见)上的两个进程之间互交。这种互交建立在HTTP之上,它们互相传递是都是字符串。换句话说, 服务器可以的接收到的来自用户的数据只能是字符串或字符数组,而在服务器上的对象中,这些数据往往有多种不同的类型,如日期(Date),整数(int),浮点数(float)或自定义类型(UDT)等,如图1所示。因此,我们需要服务器端将字符串转换为适合的类型。
图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 {
private String msg;
private Locale loc = Locale.US;
public String getMsg() {
return msg;
}
public Locale getLoc() {
return loc;
}
public void setLoc(Locale loc) {
this .loc = loc;
}
@Override
public String execute() {
// 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" /> < 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 {
@Override
public Object convertValue(Map context, Object value, Class toType) {
if (toType == Locale. class ) {
String locale = ((String[]) value)[ 0 ];
return new Locale(locale.substring( 0 , 2 ), locale.substring( 3 ));
} else if (toType == String. class ) {
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英文输出
在Locale输入框中输入“zh_CN”,按“Submit”提交,出现如图3所示页面:
图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)”的方法。下面简单地介绍一下函数的参数:
- context——用于获取当前的ActionContext
- value——需要转换的值
- toType——需要转换成的目标类型
实现转换器,我们需要通过配置告诉Struts 2.0。我们可以通过以下两种方法做到这点:
- 配置全局的类型转换器,也即是上例的做法——在源代码文件夹下,新建一个名为“xwork-conversion.properties”的配置文件,并在文件中加入“待转换的类型的全名(包括包路径和类名)=转换器类的全名”对;
- 应用于某个特定类的类型转换器,做法为在该类的包中添加一个格式为“类名-conversion.properties”的配置文件,并在文件中加入“待转换的属性的名字=转换器类的全名”对。上面的例子也可以这样配置——在源代码文件夹的tutorial包下新建名为“HelloWorld-conversion.properties”文件,并在其中加入“loc=tutorial.LocaleConverter”。
在继承DefaultTypeConverter时,如果是要将value转换成其它非字符串类型时,要记住value是String[]类型,而不是String类型。它是通过request.getParameterValues(String arg)来获得的,所以不要试图将其强行转换为String类型。 |
已有的转换器
对于一此经常用到的转换器,如日期、整数或浮点数等类型,Struts 2.0已经为您实现了。下面列出已经实现的转换器。
- 预定义类型,例如int、boolean、double等;
- 日期类型, 使用当前区域(Locale)的短格式转换,即DateFormat.getInstance(DateFormat.SHORT);
- 集合(Collection)类型, 将request.getParameterValues(String arg)返回的字符串数据与java.util.Collection转换;
- 集合(Set)类型, 与List的转换相似,去掉相同的值;
- 数组(Array)类型, 将字符串数组的每一个元素转换成特定的类型,并组成一个数组。
对于已有的转换器,大家不必再去重新发明轮子。Struts在遇到这些类型时,会自动去调用相应的转换器。
批量封装对象(Bean)
不知道大家是否遇过这种情况,在一个页面里同时提交几个对象。例如,在发布产品的页面,同时发布几个产品。我在之前一个项目就遇到过这种需求,当时用的是Struts 1.x。那是一个痛苦的经历,我在Google搜了很久都没有理想的结果。幸运的是,在Struts 2.0中这种痛苦将一去不复返。下面我就演示一下如何实现这个需求。
首先,在源代码文件夹下的tutorial包中新建Product.java文件,内容如下:
package tutorial;
import java.util.Date;
publicclass Product {
private String name;
privatedouble price;
private Date dateOfProduction;
public Date getDateOfProduction() {
return dateOfProduction;
}
publicvoid setDateOfProduction(Date dateOfProduction) {
this.dateOfProduction = dateOfProduction;
}
public String getName() {
return name;
}
publicvoid setName(String name) {
this.name = name;
}
publicdouble getPrice() {
return price;
}
publicvoid setPrice(double price) {
this.price = price;
}
}
然后,在同上的包下添加ProductConfirm.java类,代码如下:
package tutorial;
import java.util.List;
import com.opensymphony.xwork2.ActionSupport;
publicclass ProductConfirm extends ActionSupport {
public List<Product> products;
public List<Product> getProducts() {
return products;
}
publicvoid setProducts(List<Product> products) {
this.products = products;
}
@Override
public String execute() {
for(Product p : products) {
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所示,填写表单,按“Submit”提交,出现图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
上面的代码并不复杂,但有几点需要说明:
- ProductConfirm文件中的for(Product p : productes)的写法是J2SE 5.0中的新特性,作用遍历products列表;
- List<Product>也是J2SE 5.0的才有的泛型(Generic);
- ProductConfirm-conversion.properties中“Element_products=tutorial.Product”是告诉Struts 2.0列表products的元素的类型为Product,而不是定义转换器;
- 在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 没有提示的错返回页面
当然,我们还可以在页面上加上错误提示信息,通过在AddProducts.jsp的“<body>”后,加入下面代码可以实现:
<div style="color:red">
<s:fielderror />
</div>
刷新浏览器,重新提交请求,出现如图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.0中实现表单数据校验(Validation)
All Input Is Evil! |
在写前几篇文章的时候,有些朋友建议我的写一篇关于表单数据校验的文章。 正如文章的开头所引用的《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 {
private String msg;
private Locale loc = Locale.US;
public String getMsg() {
return msg;
}
public Locale getLoc() {
return loc;
}
public void setLoc(Locale loc) {
this .loc = loc;
}
@Override
public void validate() {
System.out.println( " Calling validate() " );
if ( ! (loc.equals(Locale.US) || loc.equals(Locale.CHINA))) {
addFieldError( " loc " , getText( " validation.loc " ));
}
}
public void validateExecute() {
System.out.println( " Calling validateExecute() by reflection " );
}
@Override
public String execute() {
System.out.println( " Calling execute() " );
// 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" /> < 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 {
@Override
public Object convertValue(Map context, Object value, Class toType) {
if (toType == Locale. class ) {
System.out.println( " Converting String to Locale " );
String locale = ((String[]) value)[ 0 ];
return new Locale(locale.substring( 0 , 2 ), locale.substring( 3 ));
} else if (toType == String. class ) {
System.out.println( " Converting Locale to String " );
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的数据校验工作方式,它需要经过下面几个步骤:
- 通过转换器将请求参数转换成相应的Bean属性;
- 判断转换过程是否出现异常。如果有,则将其保存到ActionContext中,conversionError拦截器再封装为fieldError;如果没有,进行下一步;
- 通过反射(Reflection)来调用validateXxx()方法(其中,Xxx表示Action的方法名);
- 调用validate()方法;
- 如果经过上述步骤没有出现fieldError,则调用Action方法;如果有,则会跳过Action方法,通过国际化将fieldError输出到页面。
不喜欢看文字的朋友,可以参考下面的图1。
图1 校验顺序图
看到这里可能大家会疑问:“这么多地方可以校验表单数据,到底我应该在那里做呢?”有选择是好事,但抉择的过程往往是痛苦的,往往让人不知所措。如果大家参照以下几点建议,相信会比较容易地做出正确的抉择。
- 如果需要转换的数据,通常做法在转换的时候做格式的校验,在Action中的校验方法中校验取值。假如用户填错了格式,我们可以通过在资源文件配置invalid.fieldvalue.xxx(xxx为属性名)来提示用户正确的格式,不同的阶段出错显示不同的信息。具体做法请参考上面的例子;
- 至于用validate()还是validateXxx(),我推荐使用validate()。原因是validateXxx()使用了反射,相对来说性能稍差,而validate()则是通过接口com.opensymphony.xwork2.Validateable调用。当然如果你的表单数据取值是取决于特定Action方法,则应该使用validateXxx()。
在运行上面的例子时,在Locale中输入zh并提交时出现图2所示页面。
图2 错误格式
在Locale中输入de_DE时,出现如图3所示页面。
图3 取值错误
使用Struts 2.0的校验框架
上一节的内容都是关于如何编程实现校验,这部分工作大都是单调的重复。更多情况下,我们使用Struts 2.0的校验框架,通过配置实现一些常见的校验。
我学习编程有个习惯——喜欢先看输出结果,再看代码实现。这样学的好处是先看结果可以刺激学习的激情,也可以在看代码前自已思考一下如何实现,然后带着问题去看代码,那就清晰多了。因此下面我们先来做演示。
首先,在tutorial包下新建ValidationAction.java,代码如下:
package tutorial;
import com.opensymphony.xwork2.ActionSupport;
public class ValidationAction extends ActionSupport {
private String reqiuredString;
public String getReqiuredString() {
return reqiuredString;
}
public void setReqiuredString(String reqiuredString) {
this .reqiuredString = reqiuredString;
}
@Override
public String execute() {
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 错误提示
在Required String中随便填点东西,转到Output.jsp页面,如图6所示。
图6 Output.jsp
通过上面的例子,大家可以看到使用该校验框架十分简单方便。不过,上例还有两点不足:
- 还没有国际化错误消息;
- 没有实现客户端的校验。
当然,要完善以上不足,对于Struts 2.0来说,只是小菜一碟。
- 在Xxx-validation.xml文件中的<message>元素中加入key属性;
- 在Input.jsp中的<s:form>标志中加入validate="true"属性,就可以在用Javascript在客户端校验数据。
下面是具体的实现,首先在国际化资源文件中加入错误消息,然后按照上面说明实现。因为要使用Javascript在客户端显示出错信息,所以在加载Input.jsp页面时,Struts 2.0需要获得国际化的字符串,故我们需要使用Action来访问Input.jsp页面,具体实现请参考《在Struts 2.0中国际化(i18n)您的应用程序》的最后部分。最后发布运行应用程序,直接在页面中点击“Submit”,表单没有被提交并出现错误提示,如图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)您的应用程序》提到的“资源文件查找顺序”相似,有特定的配置文件查找顺序。不同的是校验框架按照自上而下的顺序在类层次查找配置文件。假设以下条件成立:
- 接口 Animal;
- 接口 Quadraped 扩展了 Animal;
- 类 AnimalImpl 实现了 Animal;
- 类 QuadrapedImpl 扩展了 AnimalImpl 实现了 Quadraped;
- 类 Dog 扩展了 QuadrapedImpl;
如果Dog要被校验,框架方法会查找下面的配置文件(其中别名是Action在struts.xml中定义的别名):
- Animal-validation.xml
- Animal-别名-validation.xml
- AnimalImpl-validation.xml
- AnimalImpl-别名-validation.xml
- Quadraped-validation.xml
- Quadraped-别名-validation.xml
- QuadrapedImpl-validation.xml
- QuadrapedImpl-别名-validation.xml
- Dog-validation.xml
- 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 >
关于每个校验器的具体用法,请参考以下链接: |
总结
使用校验框架既可以方便地实现表单数据校验,又能够将校验与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所示。
图1 拦截器调用序列图
已有的拦截器
Struts 2已经为您提供丰富多样的,功能齐全的拦截器实现。大家可以到struts2-all-2.0.1.jar或struts2-core-2.0.1.jar包的struts-default.xml查看关于默认的拦截器与拦截器链的配置。
在本文使用是Struts 2的最新发布版本2.0.1。需要下载的朋友请点击以下链接: |
以下部分就是从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 {
@Override
public String execute() {
try {
// 模拟耗时的操作
Thread.sleep( 500 );
} catch (Exception e) {
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自定义拦截器是相当容易的一件事。
大家在开始着手创建自定义拦截器前,切记以下原则: |
要求拦截器是无状态的原因是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 {
@Override
public String intercept(ActionInvocation ai) throws Exception {
Map session = ai.getInvocationContext().getSession();
String role = (String) session.get( " ROLE " );
if ( null != role) {
Object o = ai.getAction();
if (o instanceof RoleAware) {
RoleAware action = (RoleAware) o;
action.setRole(role);
}
return ai.invoke();
} else {
return Action.LOGIN;
}
}
}
以上代码相当简单,我们通过检查session是否存在键为“ROLE”的字符串,判断用户是否登陆。如果用户已经登陆,将角色放到Action中,调用Action;否则,拦截直接返回Action.LOGIN字段。为了方便将角色放入Action,我定义了接口tutorial.RoleAware,代码如下:
package tutorial;
public interface RoleAware {
void setRole(String role);
}
接着,创建Action类tutorial.AuthorizatedAccess模拟访问受限资源,它作用就是通过实现RoleAware获取角色,并将其显示到ShowUser.jsp中,代码如下:
package tutorial;
import com.opensymphony.xwork2.ActionSupport;
public class AuthorizatedAccess extends ActionSupport implements RoleAware {
private String role;
public void setRole(String role) {
this .role = role;
}
public String getRole() {
return role;
}
@Override
public String execute() {
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 {
public Map < String, String > getRoles() {
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 {
private String role;
private Map session;
public String getRole() {
return role;
}
public void setRole(String role) {
this .role = role;
}
public void setSession(Map session) {
this .session = session;
}
@Override
public String execute() {
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所示:
图2 Login.jsp
选中Employee,点击Submit,出现图3所示页面:
图3 ShowRole.jsp
总结
拦截器是Struts 2比较重要的一个功能。通过正确地使用拦截器,我们可以编写高可复用的代码。
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包
本文使用的是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 {
Set < String > getUserNames();
}
清单3 tutorial.ChatService.java
然后,再创建一个默认实现ChatServiceImpl,代码如下:
package tutorial;
import java.util.HashSet;
import java.util.Set;
public class ChatServiceImpl implements ChatService {
public Set < String > getUserNames() {
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 {
private static final long serialVersionUID = 8445871212065L ;
private ChatService chatService;
private Set < String > userNames;
public void setChatService(ChatService chatService) {
this .chatService = chatService;
}
public Set < String > getUserNames() {
return userNames;
}
@Override
public String execute() {
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
上述代码有二点值得大家注意的:
- Struts 2会为每一个请求创建一个Action对象,所以在定义chatAction时,使用scope="prototype"。这样Spring就会每次都返回一个新的ChatAction对象了;
- 因为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是通过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 {
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) {
this .contentType = contentType;
}
public void setMyFileFileName(String fileName) {
this .fileName = fileName;
}
public void setMyFile(File myFile) {
this .myFile = myFile;
}
public String getImageFileName() {
return imageFileName;
}
public String getCaption() {
return caption;
}
public void setCaption(String caption) {
this .caption = caption;
}
private static void copy(File src, File dst) {
try {
InputStream in = null ;
OutputStream out = null ;
try {
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 ) {
out.write(buffer);
}
} finally {
if ( null != in) {
in.close();
}
if ( null != out) {
out.close();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static String getExtention(String fileName) {
int pos = fileName.lastIndexOf( " . " );
return fileName.substring(pos);
}
@Override
public String execute() {
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() { return this .uploads; }
public void setUpload(File[] upload) { this .uploads = upload; }
public String[] getUploadFileName() { return this .uploadFileNames; }
public void setUploadFileName(String[] uploadFileName) { this .uploadFileNames = uploadFileName; }
public String[] getUploadContentType() { return this .uploadContentTypes; }
public void setUploadContentType(String[] uploadContentType) { 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() {
return this .uploads;
}
public void setUpload(List < File > uploads) {
this .uploads = uploads;
}
public List < String > getUploadFileName() {
return this .uploadFileNames;
}
public void setUploadFileName(List < String > uploadFileNames) {
this .uploadFileNames = uploadFileNames;
}
public List < String > getUploadContentType() {
return this .uploadContentTypes;
}
public void setUploadContentType(List < String > contentTypes) {
this .uploadContentTypes = contentTypes;
}
清单16 多文件上传列表绑定Action代码片段
总结
在Struts 2中实现文件上传的确是轻而易举,您要做的只是使用<s:file />与Action的属性绑定。这又一次有力地证明了Struts 2的简单易用。