一、准备工作及实例
1.解压struts-2.1.6-all.zip
apps目录:struts2自带的例子程序
docs目录:官方文档。
lib 目录:存放所有jar文件。
Src 目录:源文件存放地
2.六个基本包
struts2-core-2.1.6.jar :开发的核心类库
freemarker-2.3.13.jar :struts2的UI标签的模板使用freemarker编写
commons-logging-1.0.4.jar :日志包
ognl-2.6.11.jar :对象图导航语言,通过它来读写对象属性
xwork-2.1.2.jar :xwork类库,struts2在其上进行构建
commons-fileupload-1.2.1.jar:文件上传组件,2.1.6版本后必须加入此jar包
特别需要说明的是目前strust2的最新版本是struts-2.1.6,它作为2.1.X的正式版。特别要注意导入commons-fileupload-1.2.1.jar包,在此jar包中包含了RequestContext类,如果不导入该jar包将会报异常。
3.初识struts2配置文件
(1).web.xml文件
主要完成对StrutsPrepareAndExecuteFilter的配置(在以前的版本中是对FilterDispatcher配置,新版本同样支持用FilterDispatcher配置),它的实质是一个过滤器,它负责初始化整个Struts框架并且处理所有的请求。这个过滤器可以包括一些初始化参数,有的参数指定了要加载哪些额外的xml配置文件,还有的会影响struts框架的行为。除了StrutsPrepareAndExecuteFilter外,Struts还提供了一个ActionContexCleanUp类,它的主要任务是当有其它一些过滤器要访问一个初始化好了的struts框架的时候,负责处理一些特殊的清除任务。
(2).struts.xml文件
框架的核心配置文件就是这个默认的struts.xml文件,在这个默认的配置文件里面我们可以根据需要再包括其它一些配置文件。在通常的应用开发中,我们可能想为每个不同的模块单独配置一个struts.xml文件,这样也利于管理和维护。这也是我们要配置的主要文件。
(3).struts.properties(参default.properties)
在Struts框架使用了很多属性,我们可以通过改变这些属性来满足我们的需求。要改变这些属性,只需在struts.properties文件中指定属性的key和value即可。属性文件可以放在任何一个包含在classpath中的路径上,但是通常我们都把它放在/WEB-INF/classes目录下面。我们可以在struts-default.properties文件中找到一个属性的列表。
(4)struts-default.xml
此文件是struts2框架默认加载的配置文件,它定义了struts2一些核心bean和拦截器,它会自动包含(included)到struts.xml文件中(实质是通过<package extends="struts-default">),并为我们提供了一些标准的配置。我们可以在struts2-core.jar中找到这个文件。
(5)其它配置文件
velocity.properties,struts-default.vm,struts-plugin.xml
4.让MyEclipse提示xml信息
当我们在编写struts.xml时,发现eclipse并不会给出帮助提示,那是因为MyEclipse默认并不支持struts2,所以我们需要手工导入dtd以支持提示。步骤:[window][preferences][MyEclipse][Files and Editors][XML][xml Catelog]然后在右边点add添加:location为dtd文件所在的位置(struts-2.0.dtd文件struts2-core-2.1.6.jar中可以得到),KeyType选择URI,Key为struts-2.0.dtd文件中文档声明的内容(http://struts.apache.org/dtds/struts-2.0.dtd),在struts.xml文件中也有此key值。
5.如何使用alt+/提示
在MyEclipse6.5中,默认的提示为Ctrl+Space,而它会与我们的输入法切换冲突,使提示失效。找到key,先取消Content Assist命令的绑定,再用“alt+/”来绑定。
6.实例
步骤一,新建myStruts2项目,并导入struts2的六个基本jar包。
步骤二,建立LoginAction文件,主要代码如下:
package com.asm;
import com.opensymphony.xwork2.Action;
public class LoginAction implements Action {
private String username;
private String password;
...省略get/set方法
public String execute() throws Exception {
if (username.equals("struts2")) {
return "loginSuccess";
} else {
return "loginFailure";
}
}
}
说明:实现了Action接口,主要是为了保证execute的正确定义,其实我们也可以不实现此接口,只要能保证execute方法书写的正确书写(方法名,返回值)。
步骤三,在struts.xml文件中注册LoginAction。此配置文件要放在src目录下,实质就是成为classpath环境变量下的文件。主要代码如下:
<?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="myFirst" namespace="/" extends="struts-default">
<action name="login" class="com.asm.LoginAction">
<result name="loginSuccess">/success.jsp</result>
<result name="loginFailure">/failure.jsp</result>
</action>
</package>
</struts>
说明:package后面会有详细说明。action元素中的name属性值指定了此action所指定的请求路径为“login.action”。后面login.jsp中的<form action=...>属性值就会参照此name属性。
步骤四、提供jsp页面
login.jsp主要代码:
<body>
<form action="<%=request.getContextPath()%>/login.action" method="get">
户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="login">
</form>
</body>
failure.jsp主要代码
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib uri="/struts-tags" prefix="s" %>
<html>
<body>
登录失败,错误的用户名:<s:property value="username"/><br>
<a href="<%=request.getContextPath()%>/login.jsp">返回</a>
</body>
</html>
说明:使用了标签库,在struts2中使用标签库非常简单,只需要像上面那样导入标签库便可以使用所有的struts2的所有标签
success.jsp主要代码
<body> 登录成功!</body>
步骤五、配置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">
<filter>
<filter-name>struts2</filter-name>
<!-- <filter-class>org.apache.struts2.dispatcher.FilterDispatcher</filter-class>
-->
<filter-class> org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
说明:注释掉的部分为以前2.1.4版本中用的核心filter类。StrutsPrepareAndExecuteFilter类的init方法将会读取类路径下默认的配置文件struts.xml,并以javabean形式存放在内存中,以后struts2对用户的每次请求将使用内存中数据,而不是重读struts.xml文件。
步骤六、发布测试。
简要分析执行流程:
当输入login.jsp访问jsp页面填写完相关信息并提交给login.action时,它会首先被在web.xml中配置的过滤器监听到,过滤器会去查找strust.xml文件,并结合namespace查找名为login的action,查找到此action便交给其处理,LoginAction内部会执行execute方法,并返回结果result(result也是参照的struts.xml中action下的result配置)。 关于表单传参,主要是参照的action中的方法名,而非属性名。
7.开启struts2自带的开发模式常量
在以前的开发中,当修改一些配置时总是不能及时地更新到服务器,我们总会重新部署或重启来更新改变的内容,在struts2中可以通过一个常量来达到此目的。即在struts.xml中的<struts>元素下增加如下内容:<constant name="struts.configuration.xml.reload" value="true" /> 这样配置后,当配置文件修改保存时就会及时更新到服务器中。其它一些常量:
<!-- 指定WEB应用的编码集,相当于调用HttpServletRequest.setCharacterEncodint方法,如果使用了velocity或freemarker,它也用于指定输出的编码格式 -->
<constant name="struts.i18n.encoding" value="UTF-8" />
<!-- 指定请求后缀为.action,指定多个请求后缀用逗号分隔 -->
<constant name="struts.action.extension" value="action" />
<!--设置浏览器是否缓存静态内容,建议:开发阶段关闭,运行时开启 -->
<constant name="struts.serve.static.browserCache" value="false" />
<!--当struts.xml配置文件修改后,系统是否重新加载该文件,开发阶段打开此功能 -->
<constant name="struts.configuration.xml.reload" value="true" />
<!-- 开发提示:出错时打印更详细的信息-->
<constant name="struts.devMode" value="true" />
<!-- 指定请求的后缀可以是.do或.action -->
<constant name="struts.action.extension" value="do,action" />
注意:在struts2.1.6版本中存在一个bug:即配置了struts.i18n.encoding常量也不能解决中文乱码问题,原因是此版本在获取请求参数后才调用了setCharacterEncoding()方法进行编码设置。解决此bug的方法是配置一个filter,并在doFilter方法中增加如下代码:request.setCharacterEncoding(“UTF-8”); 在以后的2.1.8版本中解决了此问题及2.1.6中存在的其它bug,建议新项目使用2.1.8版本。
8.vo传参模式
Copy上面的myStruts2项目,改名为myStruts2Vo项目。作如下修改:在LoginAction中有两个字段:username,password。把此两个属性重构到com.asm.vo.User类中,然后在LoginAction中提供User对象及相应的get/set方法。现在需要注意的是在login.jsp中会有如下的修改:
户名:<input type="text" name="user.username"><br>
密码:<input type="password" name="user.password"><br>
关键就是改掉name属性值。其它基本无变动。 后话:假如此此User对象并不能和Model层的相应对象完全对应,我们还应借助此User对象在Action中构建出Model层的相应对象,这样,在exectue方法中便能通过构建的Model对象作为参数与Model层交互。
9.ModerDriven传参模式(不建议采用)
Copy上面的myStruts2Vo项目,改名为myStruts2Model项目。重点是修改LoginAction,修改后的主要内容如下:
package com.asm;
import com.asm.vo.User;
import com.opensymphony.xwork2.Action;
import com.opensymphony.xwork2.ModelDriven;
public class LoginAction implements Action, ModelDriven<User> {
private User user = new User();
public String execute() throws Exception {
if (user.getUsername().equals("struts2")) {
return "loginSuccess";
} else {
return "loginFailure";
}
}
public User getModel() {
return user;
}
}
说明:它实现了ModelDriven接口,并使用了泛性机制(必须),因此要求jdk1.5以上。
现在需要注意的是在login.jsp中name属性值为User中两个字段,和第一个实例一样。说明:此方式一般不会使用,在此略作了解。
10.为什么要使用struts2代替struts1.x
(1)struts2的execute方法中的参数不会依赖于servletAPI,实现了也servlet解耦,是一种无侵入式的设计。
(2)struts2提供了拦截器,利用拦截器可以进行AOP编程,实现权限拦截等功能。
(3)struts2提供了类型转换器,我们可以很容易地对请求参数转换成需要的类型。
(4)提供了同种表现层技术支持,如JSP、freeMarker、velocity等
(5)可以对指定的方法进行校验,可以轻松地实现表单校验功能
(6)提供了全局范围、包范围和action范围的国际化资源文件管理实现。
二、struts.xml配置及例程
1.配置文件的优先级
在struts2中一些配置(比如常量)可以同时在struts-default.xml(只读性),strtus-plguin.xml(只读性),struts.xml,struts.properties和web.xml文件中配置,它们的优先级逐步升高,即是说后面的配置会覆盖掉前面相同的配置。
2.配置形式
下面以对struts.i18n.encoding=UTF-8的配置为例进行说明:
在struts.xml配置形式如下:
<constant name="struts.i18n.encoding" value="gbk"></constant>
在struts.properties的配置形式如下:
struts.i18n.encoding=UTF-8
在web.xml中配置如下:
<filter>
<filter-name>struts2</filter-name>
<filter-class> org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter
</filter-class>
<init-param>
<param-name>struts.i18n.encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
说明:官方声称配置了此常量可以解决中文乱码问题,但实事上并不能达到目的,在前面的三个项目中,如果我们在表单中输入中文,其结果是会出现乱码。解决此问题参看[一.7的注意]。这是struts2.1.6中的一bug,它的下一版2.1.8已解决此问题。
3.package配置相关
属性名 |
是否必须 |
说明 |
Name |
是 |
Package的唯一标识,不允许同名 |
Extends |
否 |
指定要继承的包 |
Namespace |
否 |
指定名称空间 |
Abstract |
否 |
声明包为抽象否 |
下面我们建立struts2package项目来进行package相关测试:
说明:在上面的配置文件中所用到的Test1Action和Test2Action这两个Action都只是继承了com.opensymphony.xwork2.ActionSupport类,而ActionSupport默认返回的就是“success”,所以当点击上面的链接分别转到了forward目录下的test1.jsp和test2.jsp。下面重点来看这个package元素的namespace属性及action的name属性,它们共同定义了action所映射到的实质文件。上图展示了链接地址和action的对应关系,所以当我们要想访问一个action所关联到的jsp文件时,应该用namespace+action的name 关于它的内容测试可以参考struts2package项目。
补充:通常情况下,action元素的name是属性值是不能出现“/”的,所以希望通过action中name属性来实现多级映射,需要在sturts.xml中增加如下属性:
<constant name="struts.enable.SlashesInActionNames" value="true"/> 这样配置后就可以再action的name元素中使用“/”了。比如:
<package name="tt3" extends="struts-default">
<action name="test3/test3" class="com.asm.Test3Action">
<result name="success">/forward/test3.jsp</result>
</action>
</package>
然后输入<a href="<%=path%>/test3/test3.action">test3</a><br>链接地址就可以访问了
强调:namespace默认值“”,即不配置namespace属性。它的意思是:如果action不能进行完整路径匹配,则会来此namespace下进行匹配,比如:.../test/test/test.action,如果参照namespace及action的name不能找到也之完全对应的action,它会再到依次追溯到上级目录中查找,即是说它会以…/test/test.action这样的路径来对应namespace和action的name进行查找。如果返回到最终的目录仍找不到,它就会到namespace="/"对应的包下查找名为test的action,如果仍找不到,它就会去默认的namespace下查找名为test的action,如果找到则执行此action。另外,namespace也可以配置成namespace="/"。它代表配置为项目的根。 总结action的名称探索顺序:完全对应、逐步追溯到上级目录查找、"/"下查找、默认namespace下查找。
为什么要提出namespace,主要是避免多人共同开发项目出现名字冲突。如果不使用namespace,多个人所写的action中可能出现重名的现象,这样当项目合并时就会出现冲突。而有了namespace可以在项目开发时由项目经理给每一个人分不同的namespace,这样每个开发人员只需要保证自己所写的action不同名即可。
namespace引发的链接问题:当我们为action配置了namespace时,访问此action的形式总会是如下形式:.../webappname/xxx/yyy/ActionName.action 而当此action成功执行跳转到某个jsp页面时,如想在此jsp页面写链接,一定要写绝对路径,因为相对路径是相对.../webappname/xxx/yyy/,而如果以后我们修改了action的namespace时,相对路径又要变,所以链接不能写成相对路径。 以下介绍绝对路径的写法:通常用myeclipse开发时建立一个jsp文件,默认总会有如下内容:
<%
String path = request.getContextPath();
String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/";
%>
我们写绝对路径可以参此内容。还可以参<head>下的<base href="<%=basePath%>"> 来完成绝对路径的书写。
4.分工合作include:指定多个配置文件
比如让jack来单独开发一个action,在jack.xml中的配置文件为:
<struts>
<package name="jack" namespace="/jack" extends="struts-default">
<action name="test4" class="com.asm.Test4Action">
<result name="success">/forward/test4.jsp</result>
</action>
</package>
</struts>
然后在struts.xml文件中增加如下内容:<include file="jack.xml"></include> 它实质就是把jack.xml中的<package>及其内容写进struts.xml中的<struts>根元素下。
链接:<a href="<%=path %>/jack/test4.action">test4</a> 这样便可以访问到了forward目录下的test4.jsp了。
5.tomcat认证访问
接上例:namespce的作用除了在前面提到的避免协同开发名字冲突外,还为认证提供一个条件。比如jack开发的东西所关联到的页面需要权限才能被访问。由于多为tomcat中的内容,下面只列出步骤。
步骤一,tomcat的conf目录下tomcat-users.xml内容如下:
<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
<role rolename="manager"/>
<role rolename="admin"/>
<user username="jack" password="jack" roles="admin,manager"/>
<user username="tom" password="tom" roles="manager"/>
</tomcat-users>
步骤二,在web.xml中增加如下内容:
<security-constraint>
<web-resource-collection>
<web-resource-name>jack</web-resource-name>
<url-pattern>/jack/*</url-pattern>
<http-method>POST</http-method>
<http-method>GET</http-method>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
<security-role>
<role-name>admin</role-name>
</security-role>
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>input authentication message</realm-name>
</login-config>
这样配置完成后,当我们访问.../jack中的任何内容都会要求输入密码认证信息,认证时输入tomcat-users.xml配置的admin权限的用户名和密码即可访问(这里就只有jack用户名可以访问)
6.初识拦截器
拦截器能在action被调用之前和被调用之后执行一些“代码”。Struts2框架的大部分核心功能都是通过拦截器来实现的,如防止重复提交、类型转换、对象封装、校验、文件上传、页面预装载等等,都是在拦截器的帮助下实现的。每一个拦截器都是独立装载的(pluggable),我们可以根据实际的需要为每一个action配置它所需要的拦截器。
在myStruts2项目下,重新对配置文件作如下修改:
<package name="myFirst" namespace="/" extends="struts-default">
<interceptors>
<interceptor name="timer"
class="com.opensymphony.xwork2.interceptor.TimerInterceptor" />
<interceptor name="params" class="com.opensymphony.xwork2.interceptor.ParametersInterceptor" />
</interceptors>
<action name="login" class="com.asm.LoginAction">
<interceptor-ref name="timer"></interceptor-ref>
<interceptor-ref name="params"></interceptor-ref>
<result name="loginSuccess">/success.jsp</result>
<result name="loginFailure">/failure.jsp</result>
</action>
</package>
首先在package中定义了两个拦截器,然后在login action中引用了这两个拦截器,需要说明的是这里使用的拦截器都是系统自带的拦截器。其实在extends所继承的struts-default中就包含了很多拦截器,也包括我们这里所用的拦截器,但如果在此action中不使用params拦截器,将会报空指针错,因为params拦截器的作用是传递表单参数,如果不使用此拦截器就不能在action中得到表单参数,所以引用时会报空指针错。虽然extends继承的strust-default自带有params拦截器,但是当我们自己引用了拦截器时,继承struts-default将不会再为我们分配默认的拦截器(有点类似构造器),但是我们仍然可以通过<interceptor-ref name="defaultStack"/>来继续使用struts-defalut的拦截器。补充:由于上面的package继承于struts-default,而我们这里所用到的timer和params都是在struts-defalut中定义过,所以即使我们在<interceptors>中没有定义过这两个拦截器,也可以直接在action中引用。
使用</interceptor-stack>组合多个拦截器:比如我们想把上面的params和timer这两个拦截器组合:
<interceptor-stack name="timer_param">
<interceptor-ref name="timer" />
<interceptor-ref name="params" />
</interceptor-stack>
然后再在action引用<interceptor-ref name="timer_param"/>”,效果和分别引用两个是一样的。其实我们使用strtus-default中的<interceptor-ref name="defaultStack"/>也是使用interceptor-stack方式。
7.Action中的method属性
在struts1.x中我们知道通过继承DispatchAction可以实现把多个Action进行统一操作,在struts2中实现action的统一操作也很简单。我们以crud操作为例,把crud集中到一个Action中。
步骤一、建立CRUDAction,内容如下:
package com.asm;
import com.opensymphony.xwork2.ActionSupport;
public class CRUDAction extends ActionSupport {
public String add() {
return "success";
}
public String del() {
return "success";
}
public String update() {
return "success";
}
public String query() {
return "success";
}
}
步骤二、配置此Action,为了清晰明了,专为此Action,建立一个配置文件crud.xml,主要内容如下:
<struts>
<package name="crud" extends="struts-default" namespace="/crud">
<action name="add" class="com.asm.CRUDAction" method="add">
<result name="success">/crud/addSuccess.jsp</result>
</action>
<action name="del" class="com.asm.CRUDAction" method="del">
<result name="success">/crud/delSuccess.jsp</result>
</action>
<action name="update" class="com.asm.CRUDAction" method="update">
<result name="success">/crud/updateSuccess.jsp</result>
</action>
<action name="query" class="com.asm.CRUDAction" method="query">
<result name="success">/crud/querySuccess.jsp</result>
</action>
</package>
</struts>
分析:上面的method方法的值来源于CRUDAction中方法的名字,这样当我们访问上面的每一个Action时,它实质是和method指定的方法关联上。
步骤三、把crud.xml配置文件并入struts.xml中,只需增加如下代码:
<include file="jack.xml"></include>
步骤四、编写相应的jsp页面,在此略去crud文件夹下的四个跳转jsp页面(addSuccess.jsp等),重点是crud.jsp页面。内容如下:
<html>
<%
String path=request.getContextPath();
%>
<body>
<a href="<%=path %>/crud/add.action">添加数据</a><br>
<a href="<%=path %>/crud/del.action">删除数据</a><br>
<a href="<%=path %>/crud/query.action">查询数据</a><br>
<a href="<%=path %>/crud/update.action">修改数据</a><br>
</body>
</html>
步骤五、发布测试。
补充扩展,动态调用DMI:不使用method实现统一.我们在crud.xml中增加如下内容:
<action name="op" class="com.asm.CRUDAction">
<result name="success">/crud/op.jsp</result>
</action>
然后再在crud.jsp中定义如下链接:
<a href="<%=path %>/crud/op!add.action">添加数据</a><br>
<a href="<%=path %>/crud/op!del.action">删除数据</a><br>
<a href="<%=path %>/crud/op!query.action">查询数据</a><br>
<a href="<%=path %>/crud/op!update.action">修改数据</a><br>
注意查看上面的链接地址,它们都是针对op action,然后再加地上“!+CRUDAction中相应的方法名”,最后再写上.action即可以访问到统一页面op.jsp。这样做虽然能减少页面,但是由于它们实质用到的是同一个Action,所以这就意味着我们要使用的拦截器相同,相同的跳转result。实际中这种方式很少使用,在此略作了解。如果不想使用动态方法调用,我们可以通过常量来关闭,即在struts.xml中增加如下配置:
<constant name="struts.enable.DynamicMethodInvocation" value="false"/>
扩展2:在CRUDAction中使用do。举例:我们在CRUDAction中增加一个新的方法,内容如下:
public String doMain(){
return "success";
}
然后再在在crud.xml中增加如下内容:
<action name="main" class="com.asm.CRUDAction" method="main">
<result name="success">/crud/main.jsp</result>
</action>
注意:配置中method属性值是doMain中去掉do后M小写。然后再在crud.jsp中增加如下链接:
<a href="<%=path %>/crud/main.action">main页面</a><br>
随后便可以访问到.../crud/main.jsp页面了。
8.使用ForwardAction实现页面屏蔽。
我们在jsp页面之间写链接总会是.../xxx.jsp,而如果我们想屏蔽掉具体的jsp,只需要所jsp页面配置成一个ForwardAction即可实现。示例如下:在根目录下有一个index.jsp主页,我们strtus.xml中作如下配置:
<package name="def" extends="struts-default">
<action name="forward">
<result >/index.jsp</result>
</action>
</package>
说明:如果没有未action指定class,默认就是ActionSupport类,如果没有为action指定method属性,则默认执行execute方法,如果没有指定result的name属性,默认值为success。知道了这些再结合ActionSupport的源码就不难理解实现转发的原理了。
随后再在前面第7点扩展中用到的op.jsp中增加如下代码:
<a href="<%=request.getContextPath()%>/forward.action">forward</a>
最后再测试访问op.jsp,在op.jsp中页面中直接点链接便可以跳到index.jsp,观察地址栏发现此时跳到index页面是进行的服务器跳转,如果我们在上面的配置中的result增加type属性变成<result type="redirect">/index.jsp</result>,实现的跳转就是客户端跳转。 补充:像这种forward形式的action实质是执行的ActionSupport 这个Action。因此配置它的result可以参看此类的api文档,比如它常用的result name有:success、login、input等。
8.使用default-Action配置统一访问
default-action-ref,当访问没有找到对应的action时,默认就会调用default-action-ref指定的action.同样在上面的package中增加如下内容:
<default-action-ref name="error"></default-action-ref>
<action name="error">
<result>/other/error.jsp</result>
</action>
上面一段内容就是说当我们访问的action不能被找到时便指向名为error的action中去,接着我们在下面配置了这个error Action。但是要注意,一个package内只配置一个<default-action-ref>,如果配置多个,就无法预测结果了. 此时我们只要输入.../myStruts2/luanFangWen.action这样的形式,它就会去访问这个默认的<default-action-ref>,通常我们会为它配置一个错误页面,以提示用户访问的页面不存在。 在web开发中,我们还可以把这个默认的action访问配置成主页,这样当用户访问一些不存在的action时,总会跳到主页上去。
通过此配置,只要是访问一个不存在的action便会转向到.../other目录下的error.jsp页面。但是如果访问是其它的不存在资源则仍是报tomcat所标识的404错误,我们可以在web.xml中作如下配置:
<error-page>
<error-code>404</error-code>
<location>/other/404error.jsp</location>
</error-page>
这样配置后,访问错误页面将跳到.../other/404error.jsp页面中去。补充说明:如果我们用ie访问时,如果选中了[工具][IE选项][高级][浏览][显示友好的http错误信息],则配置的错误页面将失效,因为找不到资源时会报HTTP404错误,而ie截取到此错误进行了它自身的友好处理,所以我们设置<error-page>就失效。
小结Action
在struts2中一个普通的java类只要有public String execute()这样的方法都可以配置成一个Action,另外我们可以实现Action接口来使java类成为一个Action,但通常的做法是继承ActionSupport类,这也是以后的项目中惯用的方法,也是推荐的首选方法。 与struts1.x不同的是:在struts2中每一个Action被请求访问时都会new出这个Action对象,所以Action本身不存在线程安全的问题。
9.使用通配符
建立struts2wildcard项目,此实例基本仿照前面前面第7点的实例改写而成。为了使用通配符,只需要改写配置文件即可。此实例未使用通配时的配置文件如下:
<action name="addUser" class="com.asm.UserAction" method="addUser">
<result name="success">/user/addUser.jsp</result>
</action>
<action name="delUser" class="com.asm.UserAction" method="delUser">
<result name="success">/user/delUser.jsp</result>
</action>
<action name="queryUser" class="com.asm.UserAction" method="queryUser">
<result name="success">/user/queryUser.jsp</result>
</action>
<action name="updateUser" class="com.asm.UserAction" method="updateUser">
<result name="success">/user/updateUser.jsp</result>
</action>
我们注释掉上面的配置,使用通配符只需如下内容即可达到相同的效果:
<action name="*User" class="com.asm.UserAction" method="{1}User">
<result name="success">/user/{1}User.jsp</result>
</action>
原理:当有.../addUser.action请求时,如果不能在当前应用中找到完全相同的addUser名字的Action时,通配符配置这时就起作用了,按通配原则,它便和上面的name="*User"相配成功,这里不难明了*此时代指的内容是add,再来看method恰恰是引用第一个*的内容,所以它的method此时的完整名为addUser,它刚好和com.asmUserAction中的addUser方法相对,所以它会去addUser方法,再来看下面的result配置所指代的页面,它也用到了{1},所以它的完整页面是/addUser.jsp。其实如果我们有良好的编程命名习惯,所有的Action我们都只需要进行一次配置。举例:规定所有的Action类都用XXXAction来命名,类中所有的CRUD方法都用add/del/update/query。Jsp页面也用add/del/update/query_XXX.jsp这样的形式。即配置文件可以写成如下形式:
<action name="*_*" class="com.asm.{2}Action" method="{1}">
<result name="success">.../{1}_{2}.jsp</result>
</action>
Name中第一个*代表CRUD操作的名字,第二个*代表类的名字。所以访问链接地址举例如下:
.../del_User.action将访问到User类的del方法,成功后跳到del_User.jsp页面。补充说明{0}是代表name中所有的*组合。
10.使用0配置:ZERO Annotation
11.Result配置详解
说明:在前面的许多案例中我们所用到的Action基本都继承自ActionSupport这个类,而在这个类中我们定义了五个字段:SUCCESS,NONE,ERROR,INPUT,LOGING。我们可以直接返回这些字段值,这些字段值实质是被定义成:String SUCCESS=”success”这样的形式,所以我们只要在Result元素中用它们的小写即可。
<result>标准完整形式如下:
<result name="success" type="dispatcher">
<param name="location">/default.jsp</param>
</result>
如果我们都采用默认的形式,最终可以简写成:<result>/default.jsp</result>
探讨type类型:
Type类型值 |
作用说明 |
对应类 |
chain |
用来处理Action链 |
com.opensymphony.xwork2.ActionChainResult |
dispatcher |
用来转向页面,通常处理JSP |
org.apache.struts2.dispatcher.ServletDispatcherResult |
redirect |
重定向到一个URL |
org.apache.struts2.dispatcher.ServletRedirectResult |
redirectAction |
重定向到一个Action |
org.apache.struts2.dispatcher.ServletActionRedirectResult |
plainText |
显示源文件内容,如文件源码 |
org.apache.struts2.dispatcher.PlainTextResult |
freemarker |
处理FreeMarker模板 |
org.apache.struts2.views.freemarker.FreemarkerResult |
httpheader |
控制特殊http行为的结果类型 |
org.apache.struts2.dispatcher.HttpHeaderResult |
stream |
向浏览器发送InputSream对象,通常用来处理文件下载,还可用于返回AJAX数据。 |
org.apache.struts2.dispatcher.StreamResult |
velocity |
处理Velocity模板 |
org.apache.struts2.dispatcher.VelocityResult |
xslt |
处理XML/XLST模板 |
org.apache.struts2.views.xslt.XSLTResult |
以上对type类型作简要的说明,下面来看实例:当一个Action处理后要返回的Result是另一个Action时,作如何配置,关键就是配置type类型。下面建立struts2result项目说明
步骤一:建立两个Action:TestAction、Test2Action
步骤二:web.xml配置省略。struts.xml主要配置内容如下:
<struts>
<package name="resultTest" extends="struts-default">
<action name="test" class="com.asm.TestAction">
<result name="success" type="chain">
<param name="actionName">test2</param>
</result>
</action>
<action name="test2" class="com.asm.Test2Action">
<result name="success">/test2Suc.jsp</result>
</action>
</package>
</struts>
说明:在名为“test”的action中,我们配置result元素的type类型值为chain,意为将继续把Action传递到下一个名为test2的Action中去,在test2.action中会把页面转向到test2Suc.jsp中去。在type类型为chain时,它的param有4个值可配,除了这里用到的name=”actionName”外(必须配置,否则报错),还有name=namespace|method|skipActions。其中namespace指定要转向到action的名字空间,由于此处是转到Action位于同一个namespace下,而namesapace的默认值the current namespace,所以可以省略不写(需要说明的是如果要跳到别的名称空间的action中去,除了使用namespace指定外,还可以用:/要跳去action所在名称空间的值/要跳去的action的name值)。Method用于指定转向到一个目标action所调用的方法,默认是调用下一个action的execute方法,所以此处仍可以省略。SkipActions是一个可选的属性,一般不用。具体可以参看chain所对应类的api帮助。
在本实例中,我们还在TestAction中设定一个username字段,并在execute方法执行为它赋了值,并在test2Suc.jsp中引用了此值。其实这种做法在web开发中还是很有用处,比如可以代替隐藏域。需要注意的是之所以在action的传递中能把设定的这个值保存下去,主要是因为转向都是服务器跳转。如果我们跳转时采取了客户端跳转,比如在test2 action的result中指定type类型为redirect,要想传递参数可以在result指向的jsp页面中附加参数即可,我们可以在test2 action的result中写成:
<result name="success" type="redirect">
/test2Suc.jsp?username=${username}
</result> 随后在test2Suc.jsp页面中引用时会出现三个问题:1.EL表达式引用失效,(EL表达式应该使用${param.username}形式)。我们也可以使用<%=request.getParameter("username")%>获取参数值。 2.由于在前面的TestAction中设定的值为中文,而附加到这里的uri请求的参数后面时会出现乱码问题。(可以使用URI编码再解码解决此问题)3.值栈取值失效:因为每一次request共享同一个值栈,所以服务器端的forward跳转也是能共享同一值栈得。但是着当test action执行后把请求交由test2 action时,test2 action采取的是redirect重定向到test2Suc.jsp页面,这时其实就是重发的一次request,所以在test action保存的值栈内容全部失效。这也就是为什么我们要附加参数的原因。而参数是保存在actionContext中,所以采用了#的方式来取出值。图示说明:
步骤三,编写链接页面index.jsp。发布测试:
全局result:
如果我们所有的action均有可能跳到相同的页面,则不防使用全局result。为了方便引用我们专门建立一个package来存放公共的result。在会用到个全局的跳转时,只需要把继承自这个公共的package即可。
建立公共包,代码如下:
<package name="pubResult" extends="struts-default" abstract="true">
<global-results>
<result name="error">/error.jsp</result>
</global-results>
</package>
由于它下面没的action配置,所以我们可以像默认的struts-default包一样,声明abstract=true,这样声明表示此packgage下不会有action,它一般是用来让别的package继承。随后再在要用到全局result中引用这个公共的package。代码如下:
<package name="testGlobal" extends="pubResult" >
<action name="error" class="com.asm.ErrorAction"></action>
<action name="error2" class="com.asm.Error2Action"></action>
</package>这样操作相当于把全局的result加到了此package下的所有action中去。
动态Result:了解
步骤一:建立DynaAction,主要代码如下:
package com.asm;
public class DynaAction extends ActionSupport {
private String username;
private String nextAction;
public String execute() throws Exception {
if (username.equals("admin")) {
nextAction = "admin";
} else if (username.equals("user")) {
nextAction = "user";
} else {
nextAction = ERROR;
}
return SUCCESS;
}
...省略get/set方法
}
步骤二、建立jsp页面dyna.jsp,主要是为了向DynaAction中传递username参数。
步骤三、相关配置如下:
<package name="dynaTest" extends="pubResult">
<action name="dyna" class="com.asm.DynaAction">
<result name="success" type="chain">${nextAction}</result>
</action>
<action name="admin" >
<result>/admin.jsp</result>
</action>
<action name="user">
<result>/user.jsp</result>
</action>
</package>
分析:当dyna.jsp把参数传递到DynaAction中去时,如果传递的值为admin,我们便设定了nextAction的值admin,在配置文件中我们通过${nextAction}(用在struts配置文件中的ognl,其实nextAction的值是存在值栈中,我们通过${}这样的形式取出。在此只作了解)来获取值便为admin,随后再继续把请求传递到下一个Action中去(此时也即admin.action),为了方便我们设定了两个ForwardAction:admin.action和user.action。这样便可以跳到指定的jsp页面。 原理:dyna.action执行后会继续把请求传递给下一个Action,而下一个Action的到底是哪一个Action,是通过DynaAction中动态指定的,比如这里是根据传递的username的值指定。
12.异常处理
步骤一、建立struts2exception项目下,在该项目下建立登录页面login.jsp。主要代码如下:
<form action="<%=request.getContextPath() %>/login.action">
username:<input type="username" name="username"><br>
<input type="submit" value="login">
</form>
步骤二、建立LoginAction,代码如下:
package com.asm;
public class LoginAction extends ActionSupport {
private String username;
public String execute() throws Exception {
if (username.equals("exception")) {
throw new ClassNotFoundException("类未被找到");
} else if (username.equals("global")) {
throw new Exception("全局异常");
} else {
return SUCCESS;
}
}
...省力get/set方法
}
步骤三、struts.xml配置文件如下:
<struts>
<package name="ex" extends="def">
<action name="login" class="com.asm.LoginAction">
<exception-mapping result="myException"
exception="java.lang.ClassNotFoundException">
</exception-mapping>
<result name="myException">/exception.jsp</result>
<result name="success">/main.jsp</result>
</action>
</package>
<package name="def" extends="struts-default" abstract="true">
<global-results>
<result name="myGlobal">/globalException.jsp</result>
</global-results>
<global-exception-mappings>
<exception-mapping result="myGlobal"
exception="java.lang.Exception">
</exception-mapping>
</global-exception-mappings>
</package>
</struts>
分析:异常处理机制较为简单,所以在此只略作说明。当登录时输入“exception”时,在LoginAction中会抛出会一个ClassNotFoundException异常,此异常我们采取的局部异常处理的方式,如果登录时输入“globla”,则会抛出Exception异常,此异常我们采取的是全局异常的处理方式,在ex包中我们继承了全局异常所在的包。提示:<exception-mapping>中的result属性的值来源于<result>元素中的name属性的值。从实例可以看出,我们一般把这种全局性的异常放在一个抽象包中供其实包继承。
三、在Action获取Scope对象
引言:在前面的Action操作中,关键就是Action中的exectue方法,但是此方法并没有request、session、application等对象作为参数,自然就不能利用这些对象来操作。下面我们建立struts2scope项目,并用四种方式来获取这些对象:
方式一、与Servlet解耦合的非IOC方式
获取的scope对象与容器无关,通过ActionContext获取。
LoginAction代码如下:
package com.asm;
public class LoginAction extends ActionSupport {
private String username;
ActionContext context;
Map request;
Map session;
Map application;
public String execute() throws Exception {
context=ActionContext.getContext();
request=(Map) context.get("request");
session=context.getSession();
application=context.getApplication();
request.put("req", "requst属性");
session.put("ses", "sesion属性");
application.put("app", "application属性");
return SUCCESS;
}
...省略username的get/set方法
}
struts.xml配置如下:
<struts>
<package name="scope" extends="struts-default">
<action name="login" class="com.asm.LoginAction">
<result>/loginSuc.jsp</result>
</action>
</package>
</struts>
login.jsp内容如下:
<form action="<%=request.getContextPath() %>/login.action">
用户名:<input type="text" name="username"><br>
<input type="submit" value="login">
</form>
loginSuc.jsp的主要内容如下:
${requestScope.req}
${sessionScope.ses}
${applicationScope.app}
<h4>以下使用scope.getAttribute的形式来接受</h4>
request: <%=request.getAttribute("req") %><br>
session: <%=session.getAttribute("ses") %><br>
application:<%=application.getAttribute("app") %><br>
分析:通过ActionContext的getContext静态方法得到ActionContext对象,然后ActionContext对象调用get方法来获取一个存储在request范围中的对象。我们使用el或通过request.getAttribute这样的方式均可以获取对象值,这说明了这些Map request对象实际是存储在request范围内的对象。
方式二、与Servlet解耦合的IOC方式
我们建立Login2Action,主要代码如下:
package com.asm;
public class Login2Action extends ActionSupport implements RequestAware,SessionAware,ApplicationAware {
private String username;
Map request;
Map session;
Map application;
public String execute() throws Exception {
request.put("req", "requst属性");
session.put("ses", "sesion属性");
application.put("app", "application属性");
return SUCCESS;
}
public void setRequest(Map<String, Object> request) {
this.request=request;
}
public void setSession(Map<String, Object> session) {
this.session=session;
}
public void setApplication(Map<String, Object> application) {
this.application=application;
}
...省略username的get/set方法
}
注册此Action的name为login2,随后修改登录提交为.../login2.action。便可以发布测试。说明:此方法其实和方式一很相似,只是在方式一中我们需要手动的为Map request赋值,但是在方式二中它是通过实现接口,在重写接口中的方法中完成对Map requset的赋值,所以称之IOC方式。借助此例,略谈下依赖注入与控制反转:所谓依赖注入就是一个对象自己本身的初始化是依赖其它对象。比如这里Map request这些对象会依赖struts2来给其初始化,称为依赖注入,而依赖注入的就表示,这些对象的控制权不再由此类本身掌握,而是交给了别的对象,即是控制权反转了。 强调:方式二是开发中主要用的方式,应重点掌握
方式三、与Servlet耦合的非IOC方式
建立Login3Action,代码如下:
package com.asm;
public class Login3Action extends ActionSupport {
private String username;
HttpServletRequest request;
HttpSession session;
ServletContext application;
public String execute() throws Exception {
request = ServletActionContext.getRequest();
session = request.getSession();
application = ServletActionContext.getServletContext();
request.setAttribute("req", "requst属性");
session.setAttribute("ses", "sesion属性");
application.setAttribute("app", "application属性");
return SUCCESS;
}
...省略username的get/set方法。
}
此方法获取的纯粹的Scope对象,它与容器相关,这些Scope对象操作更强。同样只需要注册此Action并修改登录页面便可进行测试。
方式四、与Servlet耦合的IOC方式
建立Login4Action,代码如下:
package com.asm;
public class Login4Action extends ActionSupport implements ServletRequestAware,ServletContextAware{
private String username;
ActionContext context;
HttpServletRequest request;
HttpSession session;
ServletContext application;
public String execute() throws Exception {
context=ActionContext.getContext();
session=request.getSession();
request.setAttribute("req", "requst属性");
session.setAttribute("ses", "sesion属性");
application.setAttribute("app", "application属性");
return SUCCESS;
}
public void setServletRequest(HttpServletRequest request) {
System.out.println("测试:"+request);
this.request=request;
}
public void setServletContext(ServletContext application) {
System.out.println("测试:"+application);
this.application=application;
}
...省略username的get/set方法
}
同样只需要注册此Action并修改登录页面便可发布测试
四、OGNL与ValueStack(VS)
1.值栈入门
下面我们建立struts2ognl项目来练习ognl的使用。
步骤一、搭建strust2的开发环境
步骤二、建立LoginAction,主要代码如下:
package com.asm;
public class LoginAction extends ActionSupport{
private User user;
public String execute() throws Exception {
return SUCCESS;
}
...省略user的get/set方法
}
步骤三、配置此Action,struts.xml的主要内容如下:
<struts>
<constant name="struts.devMode" value="true"></constant>
<package name="ognl" extends="struts-default">
<action name="login" class="com.asm.LoginAction">
<result>/loginSuc.jsp</result>
</action>
</package>
</struts>
步骤四、编写login.jsp页面,主要代码如下:
<body>
<form action="<%=request.getContextPath()%>/login.action" method="get">
用户名:<input type="text" name="user.username"><br>
密 码:<input type="password" name="user.password"><br>
<input type="submit" value="login">
</form>
</body>
步骤五、编写loginSuc.jsp页面,主要代码如下:
<body>
调试:<s:debug></s:debug>
获取值栈中的username属性:<s:property value="user.username"/> <br>
</body>
步骤六、发布测试及说明
当我们输入用户名并成功跳到logSuc.jsp页面后,会得到登录时输入的用户名信息。下面简要说明这一过程:
(1).login.jsp登录提交登录信息给login.action
(2).struts2监听到客户端的login.action请求,按配置文件要求,把此请求交给LoginAction处理。这表示会去new LoginAction(), 当struts2 new出此Action对象后会把这个对象放在context map中,只是这个Action非常特殊,所以放在值栈中,而放在值栈中的对象是可以直接引用的,放在其它context map中的对象引用时会要求加#。
(3).当new LoginAction时,表示它也会初始化此类中的对象,比如这里会去初始化User对象,但是要注意的是如果我们在用户名和密码什么都不输,再来用debug来看值栈中的user是,发现它仍会new此对象,因为尽管我们没用输入值,但是后台的set方法还是要被调用,所以会new出此对象,但是如果我们直接输入.../struts2ognl/login.action时我们会发现跳转到loginSuc.jsp页面时,用debug来看值栈中此User user,发现它的值为null。第二点要注意的是我们必须为User类提供默认的构造方法,否则将会出现如下错误: java.lang.InstantiationException: com.asm.vo.User
总结:1.Action会在请求时被创建,且会把创建的对象放到值栈中。
2.Action中的对象字段只有在需要时才会以new 的形式初始化,而且这些对象字段必须提供默认的构造方法。
3.ValueStack对象贯穿整个Action的生命周期(每个Action类的对象实例会拥有一个ValueStack对象)。当Struts 2接收到一个.action的请求后,会先建立Action类的对象实例,但并不会调用Action方法,而是先将Action类的相应属性放到ValueStack对象的顶层节点(vs对象相当于一个栈)。
补充:值栈(根)对象也可以直接使用EL表达式访问,比如这里可以直接通过${user.username}来获取username的值,我们知道el表达式只能访问四种scope范围内的对象,那为什么这里能访问到值栈对象呢?原因是struts2对HttpServletRequet进行了一次封装,封装的代码主要是重写了getAttribute方法,简述重写此方法的核心代码:首先在原始的HttpServletRequest对象中查找el表达式中的属性,如果找不到再通过ActionContext获取值栈对象,进而再从值栈对象中查找el表达式中要访问的属性。
2.OGNL入门
下面我们在com.asm.vo.User类中增加一个字段private Address addres;,并提供此字段的get/set方法,随后再在login.jsp中增加如下代码:
城 市:<input type="text" name="user.addres.city"><br>
然后再在loginSuc.jsp中增加如下代码:
获取城市属性:<s:property value="user.addres.city"/><br>
然后测试,会得到登录时输入的城市信息(中文会有乱码)。下面借助此例谈ognl的定义:在这个例子中,我们的LoginAction中有一个User对象,而在User对象中又有一个Address对象,这些对象之间依靠这种类的字段进行关联,或者说是依靠字段属性进行导航,这也就是OGNL的定义:Object Graph Navigation Language:对象图导航图语言,它是建立在值栈技术之上的一种全新语言。
补充:用%{}可以取出存在值堆栈中的Action对象,直接调用它的方法.我们在loginSuc.jsp中增加如下内容调用LoginAction中的get方法:
调用值栈对象中的方法:<s:property value="%{get()}"/>
LoginACtion中增加的get方法如下:
public String get(){
return "这是User中的get方法";
}
3.普通方法访问
首先在User中增加一个成员方法,代码如下:
public String get(){
return "这是User中的get方法";
}
在LoginAction中也有类似的get方法,随后再在loginSuc.jsp中增加如下代码:
调用值栈对象中的普通方法(2):<s:property value="user.username.length()"/><br>
调用值栈对象中的普通方法(1):<s:property value="user.get()"/><br>
调用LoginAction中的普通方法:<s:property value="get()"/><br>
最后测试,发现这些方法都可以访问到。
4.静态方法访问
在LoginAction中增加如下方法:
public static String getSta() {
return "这是LoginAction中的静态方法";
}
然后在loginSuc.jsp中增加如下代码:
调用Action中的静态方法:<s:property value="@com.asm.LoginAction@getSta()"/><br>
调用LoginAction中的静态方_方式(2):<s:property value="@vs@getSta()"/><br>
说明:我们在方式二中用到@vs,只有那些值栈中的对象才可以这样写。
然后访问,发现访问不到,因为在struts2.1.6的版本中,struts.ognl.allowStaticMethodAccess的默认值为false,我们只需在struts.xml中增加如下内容:
<constant name="struts.ognl.allowStaticMethodAccess" value="true"/>
再来访问时便可以访问到。
5.默认类Math的访问
在loginSuc.jsp中增加如下代码:
调用Math类中的静态方法:<s:property value="@java.lang.Math@min(1,2)"/><br>
调用Math类中的静态方法_方式(2):<s:property value="@@min(1,2)"/><br>
调用Math类中的字段:<s:property value="@@PI"/><br>
说明:因为是默认的类,所以可以省略类名
6.调用普通类的构造方法
建立一个新的类:Student,在此省略代码。
然后在loginSuc.jsp中增加如下代码:
调用普通类中的构造方法 :
<s:property value="new com.asm.vo.Student('jack','20','85.5')"/><br>
调用普通类中的构造方法并访问其字段 :
<s:property value="new com.asm.vo.Student('jack','20','85.5').name"/>
说明:第一种是只new出对象,显示的时候其实是调用对象的toString方法。
7.集合对象初步
首先在LoginAction中增加如下字段并提供相应的get/set方法:
private List myList = new ArrayList();
private Set mySet = new HashSet();
private Map myMap = new HashMap();
然后再在execute方法中初始化这些集合对象,代码如下:
myList.add("list1");
myList.add("list2");
myList.add("list3");
myList.add("list4");
mySet.add("set1");
mySet.add("set3");
mySet.add("set1");
mySet.add("set2");
myMap.put("m1", "map1");
myMap.put("m3", "map3");
myMap.put("m2", "map2");
最后在loginSuc.jsp中增加如下代码:
获取List:<s:property value="myList"/><br>
获取List中的第一个元素:<s:property value="myList[0]"/><br>
获取Set:<s:property value="mySet"/><br>
获取Set中的第一个元素(set无序,不能取到):<s:property value="mySet[0]"/><br>
获取Map:<s:property value="myMap"/><br>
获取Map中的key=m1的元素的值:<br>
方式一:<s:property value="myMap.m1"/>
方式二:<s:property value="myMap['m1']"/><br><hr>
获取List的大小:
<s:property value="myList.size"/>|<s:property value="myList.size()"/><br>
获取Map中所有键:<s:property value="myMap.keys"/><br>
获取Map中所有值:<s:property value="myMap.values"/><br>
最后测试,这些东西不多作解释。
8.集合对象进阶
首先在LoginAction中增加如下字段并提供相应的get/set方法:
private List studentList = new ArrayList();
然后再在execute中为其初始化赋值,代码如下:
studentList.add(new Student("jack", 20, 86.0f));
studentList.add(new Student("lily", 22, 96.5f));
studentList.add(new Student("tom", 23, 56.5f));
最后在loginSuc.jsp中增加如下代码:
获取List中的Student对象:<s:property value="studentList"/><br>
利用投影获取List中的name属性:<s:property value="studentList.{name}"/><br>
利用投影获取List中的age属性:<s:property value="studentList.{age}"/><br>
利用投影获取List中的第一个对象的name属性:<s:property value="studentList.[0]{name}"/> 或者<s:property value="studentList.{name}[0]"/><br>
利用选择获取List中grade>60的student信息:
<s:property value="studentList.{?#this.grade>60}"/><br>
利用选择获取List中grade>60的student名字信息:
<s:property value="studentList.{?#this.grade>60}.{name}"/><br>
利用选择获取List中grade>60的第一个student名字信息:
<s:property value="studentList.{?#this.grade>60}.{name}[0]"/><br>
利用选择获取List中grade>60的第一个student名字信息(链表):
<s:property value="studentList.{^#this.grade>60}.{name}"/><br>
利用选择获取List中grade>60的最后一个student名字信息(链表):
<s:property value="studentList.{$#this.grade>60}.{name}"/><br>
说明:这里重点是说明?#的使用,结合此例来看,studentList中有许多Stutdent对象,我们可以用条件来限制取哪些对象,这些条件必须以?#开始,并且条件要用{}括起。而this是指在判断studentList中的对象是否符合条件的当前对象。?#是指取出符合条件的所有Student对象,而^#是指取出符合条件的第一个对象,$#是指取出符合条件的最后一个对象。
9.N语法top语法
我们在loginSuc.jsp中增加如下下代码:
N语法[0]:<s:property value="[0]"/><br>
N语法[1]:<s:property value="[1]"/><br>
N语法[0].top:<s:property value="[0].top"/><br>
N语法[1].top:<s:property value="[1].top"/><br>
N语法top:<s:property value="top"/><br>
N语法取值:<s:property value="[0].user.username"/><br>
N语法取值:<s:property value="top.user.username"/><br>
说明:规定栈顶的对象为[0],而我们只使用[0]的意思是从值栈中第一个对象取,一直取至栈底。N的意思是从值栈中的第N个对象开始,取到栈底为止。如果要想访问某个对象,需要使用[N].top,它的意思是取出符合N语法的栈顶对象,比如在这里,[0]会取出两个对象,而[0].top是取出这两个对象的栈顶对象。纯top可以简洁地取出值栈中的栈顶对象。
为什么要提出N语法,当我们通过chain链访问时,值栈中可能有两个以上的Action对象,如果这些对象中存在相同的属性,N便能正确区分他们。通常,这些Action对象的入栈顺序是:先访问先入栈。
从上面的N语法取值实例中,我们知道[N]top语法的一个重要作用就是能通过它们引用值栈对象中的属性。结合前面的五种[N]top语法实例,不难理解这里的取值实例。
补充:在此实例中,我们用<s:debug>调试会发现,值栈中还有一个DefaultTextProvider对象(因为此Action继承自ActionSupport),它的作用是获取资源文件中的内容(其实本质是ActionSupport重写了getText()方法),这也就是在国际化问题中我们能直接调用它的getText()方法的原因。
10.获取Stack Context中的信息
我们知道,除了可以从值栈中获取信息,还可以从Stack Context中获取信息,只是要加上#,下面我们通过scope对象来演示。首先是在LoginAction中增加如下字段:
Map myRequest;
Map mySession;
随后再用前面提到的“在Action中获取Scope对象”的方式二来完成这些对象的初始化。即实现RequestAware和SessionAware接口。然后再在execute方法中增加如下内容:
myRequest.put("req", "Req属性");
mySession.put("ses", "Ses属性");
最后在loginSuc.jsp中增加如下代码:
获取Request属性:<s:property value="#request.req"/><br>
获取Session属性:<s:property value="#session.ses"/><br>
获取parameters属性:<s:property value="#parameters.mes"/>
说明:我们获取这些对象都用了#,因为这些对象都是存在一般的Context Map中,而不是存在值栈中。别最后一个信息的获取是因为我们在login.jsp中增加了如下代码:
<input type="hidden" name="mes" value="the message is transfer by hidden">
关于这些scope的更多信息可以参看下表:
名称 |
作用 |
例子 |
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顺序访问其属性 |
#application.userName相当于application.getAttribute("userName") |
11.总结$ # %的区别
$用于i18n和struts配置文件
#取得ActionContext的值
%将原来的文本串解析为ognl,对于本来就是ognl的文本不起作用。形式:%{要解析的文本串}
12.总结OGNL[重点]
OGNL是Object Graphic Navigation Language(对象图导航语言)的缩写,它是一个开源项目。Struts2使用OGNL作为默认的表达式语言。
相对于EL表达式,它提供了平时我们需要的一些功能,如:支持对象方法调用,支持各类静态方法调用和值访问,支持操作集合对象。OGNL有一个上下文的概念,这个上下文件实质就是一个Map结构,它实现了java.utils.Map接口,在struts2中上下文的实现为ActionContext,下面是上下文的结构示意图:
当struts2接受一个请求时,会迅速创建ActionContext,ValueStack,action。然后把action存放进ValueStack,所以action的实例变量可以接受OGNL访问。
访问上下文中的对象需要使用#号标注命名空间,如#application、#session。另外OGNL会设定一个根对象,在struts2中根对象就是ValueStack值栈对象,如果要访问根对象中对象的属性,则可以省略#命名空间,直接访问该对象的属性即可。在struts2中,根对象的实现类为OgnlValueStack,该对象不是我们想象的只存放单个值,而是存放一组对象,在OgnlValueStack类里有一个List类型的变量,就是使用这个List变量来存放一组对象。在root变量(List类型)中处于第一位的对象叫栈顶对象,通常我们在Ognl表达式里直接写上属性的名称即可访问root变量里对象的属性,搜索顺序是从栈顶对象开始寻找,如果栈顶对象不存在该属性,就会从第二个对象寻找,如果没有找到就从第三个对象寻找,依次往下寻找。 注意:struts2中 ,OGNL表达式需要配合struts的标签才可以使用。
五、拦截器
在前面我们已经初步使用过拦截器,下面继续细细探讨。
1.概述strust2中的拦截器
拦截器是Struts2框架的核心,它主要完成解析请求参数、将请求参数赋值给Action属性、执行数据校验、文件上传等工作。Struts2设计的灵巧性,拦截器起了关键性的作用,当需要扩展Struts2功能时,只需要提供对应拦截器,并将它配置在Struts2容器中即可;如果不需要该功能时,也只需要取消该拦截器的配置即可。
Struts2内建了大量的拦截器,这些拦截器以name-class对的形式配置在struts-default. xml文件中,其中name是拦截器的名字,就是以后我们使用该拦截器的唯一标识;class则指定了该拦截器的实现类,如果我们定义的package继承了Struts2的默认struts-default包,则可以自由使用它下面定义的拦截器,否则必须自己定义这些拦截器。
2.自定义拦截器
自定义拦截器需要特别注意的是不要忘记引入struts2默认的拦截器。为了实现某些操作,我们可以自定义拦截器,自定义拦截器有三种方式定义。分别为实现Interceptor接口,继承抽象类AbstractInterceptor,继承MethodFilterInteceptor类。
方式一,实现Interceptor接口。
准备工作,新建struts2interceptor项目。构建一个登录环境:当我们点登录链接时,便成功登录(为了方便,这里不进行验证)。即在link.jsp页面中写如下链接:<a href="<%=request.getContextPath()%>/login.action">登录</a> 然后,我们点击此链接便可以登录。login.action在strutst.xml中的的配置如下:
<package name="interceptor" extends="struts-default">
<action name="login" class="com.asm.LoginAction">
<result name="success">/success.jsp</result>
</action>
</package>
com.asm.LoginAction为了简单,com.asm.LoginAction总是返回SUCCESS;这样请求这个Action总会返回到.../success.jsp页面。
编写拦截器:MyInterceptor类,内容如下:
package com.asm;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.Interceptor;
public class MyInterceptor implements Interceptor {
public void destroy() {
}
public void init() {
}
public String intercept(ActionInvocation invocation) throws Exception {
System.out.println("开始拦截");
String result = invocation.invoke();
System.out.println("结束拦截");
return result;
}
}
为了使用此拦截器,我们必须将此拦截器进行注册,随后再在要使用此拦截器的Action中引用。即首先在<package>中注册,内容如下:
<interceptors>
<interceptor name="myIpt" class="com.asm.MyInterceptor"></interceptor>
</interceptors>
注册完成后,如果我们要在login.action中使用此拦截器,只需要在<action>中增加如下内容:
<interceptor-ref name="myIpt"></interceptor-ref>
这样便成功为LoginAction配置了我们自定义的拦截器MyInterceptor,下面只需发布测试。
实例流程分析:当我们为LoginAction配置了拦截器时,并且有客户端请求此Action时,会首先被此拦截器拦住,然后执行System.out.println("开始拦截"),随后我们调用invocation.invoke()方法,它会把请求继续传递给下一个拦截器,下一个拦截器也会继续执行相应代码后再调用invoke方法继续传递,直到请求到达最后一个拦截器,它会把请求传递给Action,比如,我们这里只用到了一个拦截器,当它执行完成后,会把请求直接转交到LoginAction处理,LoginAction处理完成后,它会返回结果给MyInterceptor拦截器。
方式二、继承AbstractInterceptor抽象类
创建拦截器类MyAbstractInterceptor:主要代码如下:
package com.asm;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
public class MyAbstractInterceptor extends AbstractInterceptor {
public String intercept(ActionInvocation invocation) throws Exception {
System.out.println("Abs开始拦截");
String result = invocation.invoke();
System.out.println("Abs结束拦截");
return result;
}
}
然后注册此拦截器,在<interceptors>元素进行进行配置,内容如下:
<interceptor name="myAbs" class="com.asm.MyAbstractInterceptor"></interceptor>
随后再在LoginAction中引用此拦截器,即在<action name="login" ...>配置如下内容:
<interceptor-ref name="myAbs"></interceptor-ref>
最后发布测试。
方式三、继承MethodFilterInteceptor类
创建拦截器类MyMethodFilterInterceptor,主要代码如下:
package com.asm;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor;
public class MyMethodFilterInterceptor extends MethodFilterInterceptor{
protected String doIntercept(ActionInvocation invocation) throws Exception {
System.out.println("method开始拦截");
String result=invocation.invoke();
System.out.println("method结束拦截");
return result;
}
}
然后注册此拦截器,在<interceptors>元素进行进行配置,内容如下:
<interceptor name="myMet" class="com.asm.MyMethodFilterInterceptor">
</interceptor>
随后再在LoginAction中引用此拦截器,即在<action name="login" ...>配置如下内容:
<interceptor-ref name="myMet"></interceptor-ref>
最后发布测试。
分析:当配置到此,实质便为LoginAction配置了三个拦截器,当我们点击登录时会在控制台打印出如下语句:
开始拦截
Abs开始拦截
method开始拦截
--先执行拦截器,再执行此Action
method结束拦截
Abs结束拦截
结束拦截
其实当我们点击登录时,本来是要访问LoginAction,最后会把LoginAction的执行结果传递给访问者。但是当我们配置了拦截器时,当我们去访问Action时,会首先被拦截,随后拦截器执行一些操作后才会继续把请求传递下去。下面作图说明拦截流程:
结合现实理解:比如我们要去某楼层找某人(LoginAction)取一个资源(LoginAction处理后返回的结果,这里表现为success.jsp),(1)进楼层时会被大门保安拦截检查(第一个拦截器:MyInterceptor拦截器),(2)检查通过后再进电梯时会被电梯保安员检查(第二个拦截器:MyAbstractInterceptor拦截器),(3)检查通过后再上到某楼层会被楼层保安员检查(第三个拦截器:MethodAction拦截器
),(4)检查通过后会找到某个(LoginAction),并与某个交谈(LoginAction处理),随后某个人和我们会带着请求的资源出去,(5)出去时,会依次被楼层,电梯,大门保安员检查,最终检查通过。某个人把资源给我们(实质就是返回请求资源给客户端)。 其实拦截器的执行流程和过滤器差不多,所以我们不防用过滤器的眼光来看这些拦截器。
注意:我们在为LoginAction配置拦截器时,都没使用默认的拦截器,是原因这里的测试可以不用,但是以后在我们使用自定义的拦截器是,一定要加上默认的拦截器,否则会导致许多不可预知的结果。
补充:从上面的图并结合代码,我们可以看出拦截器的核心过程应该是ActionInvocation对这些拦截器回调处理,下面我们建立com.asm.interceptor.simulation来模拟这一过程,具体的代码参源文件,在此略去。在此我们作图分析ActionInvocation的实现过程:
补充2:上面分别使用了三种方式来创建自定义的拦截器,第一种方式是最原始的实现方式,第二种方式的好处是我们可以不必重写所有的方法,较常用。第三种方式进行了扩展,下面将会展示它的扩展性能。
3.使用来MethodFilterInterceptor灵活拦截
步骤一、建立MethodAction,代码如下:
package com.asm;
import com.opensymphony.xwork2.ActionSupport;
public class MethodAction extends ActionSupport{
public String m1(){
return SUCCESS;
}
public String m2(){
return SUCCESS;
}
public String m3(){
return SUCCESS;
}
}
步骤二、注册此Action,并为此Action配置拦截器。配置内容如下:
<action name="*_*" class="com.asm.MethodAction" method="{2}">
<result name="success">/{2}Suc.jsp</result>
<interceptor-ref name="myMet">
</interceptor-ref>
</action>
我们为此Action配置了前面写的MyMethodFilterInterceptor拦截器,并在link.jsp中增加如下链接:
<a href="<%=request.getContextPath()%>/Method_m1.action">m1</a><br>
<a href="<%=request.getContextPath()%>/Method_m2.action">m2</a><br>
<a href="<%=request.getContextPath()%>/Method_m3.action">m3</a><br>
当点m1时会访问到m1Suc.jsp页面, 点m2、m3会分别访问到m2Suc.jsp、m3Suc.jsp页面。现在假如我们想访问m2、m3时不被拦截,我们只需修改MyMethodFilterInterceptor注册:修改内容为:
<interceptor name="myMet" class="com.asm.MyMethodFilterInterceptor">
<param name="excludeMethods">m2,m3</param>
</interceptor>
它的作用和增加<param name="includeMethods">m1</param>等价。上面是指定m2,m3方法调用时不被拦截,这里是指定只拦截m1。除了这种在注册拦截器时指定拦截外,还可以在引用拦截器时指定,即如下形式:
<interceptor-ref name="myMet">
<param name="excludeMethods">m2,m3</param>
<param name="includeMethods">m1</param>
</interceptor-ref>
上面的两处<param>配置是等价的,但是如果〈param〉配置冲突,谁起作用?即如果我们对m1配置了excludeMethods同时又配置了includeMethods时,谁起作用,我们可以进行这些冲突的验证。以下是验证结果:
引用配置(在Action引用拦截器时配置)时,以includeMethods的配置为准。一旦我们为拦截器使用了<param>配置,而对m1这样的方法不配置任何,就不会被拦截。但是如果不使用<param>,它们全部都要被拦截。
注册配置时(在注册拦截器时配置),情况和“引用配置”完全一样。
引用配置和注册配置冲突时,以引用配置为准。
4.使用默认的execAndWait拦截器
当我们进行数据库查询等相关的操作时,如果服务器负荷过重可能不能及时把数据查询出来,进而会在状态拦显示“正在打开...”,但却一直转不到相关的页面,这将给客户端带来不便,甚于很多人会因此不愿使用网站的所有服务。对此我们可以在客户提交时,马上转到一个页面,并在该页面显示“您的请求已提交,服务器正在查询,请等待...”的内容,这样客户将不会陷于无赖的等待中。 对于此要求,struts2可以轻松帮我们完成。下面新建struts2wait项目演示此实例。
建立LoginAction,代码如下:
package com.asm;
public class LoginAction extends ActionSupport {
public String execute() throws Exception {
Thread.sleep(5000);
return SUCCESS;
}
}
说明:为了模拟服务器负荷过重,查询时间要很长。我们在使用了线程休眠的方式。
随后配置此Action,配置的主要内容如下:
<action name="login" class="com.asm.LoginAction">
<interceptor-ref name="defaultStack"></interceptor-ref>
<interceptor-ref name="execAndWait"></interceptor-ref>
<result name="wait">/wait.jsp</result>
<result name="success">/success.jsp</result>
</action>
注意:在配置前我们先是使用了默认的拦截器,再此强调在我们为Action配置拦截器时,应该总是配上默认的拦截器。随后我们使用了execAndWait拦截器,如需要配置此拦截器,此拦截器一定要配置在最后,否则会出现一些难预知的结果。如果使用此拦截器,我们通常还会配置wait的result结果集,因为“On the initial request or any subsequent requests (before the action has completed), the wait result will be returned. The wait result is responsible for issuing a subsequent request back to the action, giving the effect of a self-updating progress meter”,大概意思就是当我们请求的Action在未执行完,就是未返回结果时,会首先把wait result返回,而在wait result所指定的页面中通常会再次发送请求给原始的Action。所以wait.jsp的主要内容如下:
<head>
<meta http-equiv="refresh" content="1;login.action">
</head>
<body> 查询请求已提交,正在查询数据,请等待... </body>
在此页面中,我们指定了每隔1秒便发送请求到login.action中去。这样,客户端便可以及时获取查询结果。结合此实例,我们简要分析流程:当我们发出请求到此Login.Action中去时,首先会被exeAndWait拦截器拦截到,这样它便跳转到wait.jsp页面,在wait.jsp页面中每隔1秒我们会继续发送此Action的请求,当再次请求到达LoginAction时,如果它已经返回,则会跳到此Action返回的页面,如果LoginAction未返回,则继续停留在wait.jsp中,再隔1秒又再次发送请求到LoginAction中去。
其实如果服务器能很快查询出结果,我们则不需要用到wait.jsp页面,我们只需在<interceptor-ref name="execAndWait"></interceptor-ref>中增加如下一段配置:
<param name="delay">6000</param> 这样便延迟请求到达wait.jsp页面,这样当请求到达时它会在LoginAction中执行6秒时间再到wait.jsp,而6秒LoginAction足以执行完并返回结果,所以当拦截器
执行时首先检查到此Action已经返回结果。则拦截器会直接用此返回页面,如果此时发现LoginAction并未执行完,它便会把wait resutl指定的页面返回。需要说明的是,通常我们设定的延迟最多一秒,这里为了演示,设置的很长。图示此拦截器原理:
关于此拦截器的详细的配置及文档说明可以参看ExecuteAndWaitInterceptor类的api信息。
5. TokenInterceptor防止表单重复提交。
由于某些原因,用户在进行类似表单提交的操作后,以为表单未被提交,会进行多次的重复提交。为了避免用户多次提交给服务器带来负荷。我们会对表单提交这样的操作进行一些处理,以告诉用户不要重复提交。下面我们建立struts2token项目,使用struts2的token拦截器来实现此案例。
步骤一,编写login.jsp页面,内容如下:
<%@ page language="java" pageEncoding="UTF-8"%>
<%@ taglib uri="/struts-tags" prefix="s" %>
<html>
<body>
<form action="<%=request.getContextPath()%>/login.action" >
姓名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="登录">
<s:token></s:token>
</form>
</body>
</html>
说明,此登录页面中的关键技术就是使用了标签库中的<s:token></s:token>标签,它的作用就是在用户访问此页面时会生成一个sessionId,在提交时会服务器会据此验证表单是否已提交。“To set a token in your form, you should use the token tag. This tag is required and must be used in the forms that submit to actions protected by this interceptor”,这句话的大概意思就是我们必须要在提交的表单中使用这个token tag,这样提交到的Action便能配置TokenInterceptor拦截器验证表单是否重复提交。
步骤二,编写LoginAction,主要代码如下:
package com.asm;
public class LoginAction extends ActionSupport {
public String execute() throws Exception {
System.out.println("---->执行execute方法...");
return SUCCESS;
}
}
步骤三,struts.xml主要配置内容如下:
<struts>
<package name="tokenTest" extends="struts-default">
<action name="login" class="com.asm.LoginAction">
<result name="success">/success.jsp</result>
<result name="invalid.token">/subError.jsp</result>
<interceptor-ref name="token"></interceptor-ref>
<interceptor-ref name="defaultStack"></interceptor-ref>
</action>
</package>
</struts>
说明:在此Action下,我们配置了token拦截器,另注意到在此Action下我们还配置了一个“invalid.token”result,因为“This interceptor uses a fairly primitive technique for when an invalid token is found: it returns the result invalid.token, which can be mapped in your action configuration”。它的大概意思就是:提交时服务器如果根据token标签产生的sessionId判断出表单已提交,它则返回invalid.token指向的视图。比如这里,如果重复提交则会转到.../subError.jsp中去。另不要忘记了引入默认的拦截器栈。补充:关于token拦截器更多细节可以访问org.apache.struts2.interceptor.TokenInterceptor类的api说明。
步骤四,编写配置中所用到jsp页面,这些页面编写简单,在此省去。
步骤五、发布测试,请注意访问login.jsp页面时,查看源文件时会发现增加了两个隐藏域信息。
步骤六、更换拦截器:我们还可以使用tokenSession拦截器,它的功能比上面的增强,它能保证持有相同sessionId的并发请求等待第一个完成之后才能被提交处理,但是它返回的是action执行后的result.接着上例,我们只需要在配置中作如下修改:把上面的token拦截器改成<interceptor-ref name="tokenSession"></interceptor-ref> 即可。随后便可以测试,测试时会发现如果我们重复提交,它总是返回到上一次的success.jsp页面,但是它并不是经过LoginAction中的execute处理后返回(我们System.out.print语句在重复提交时并未打印出来),而是此拦截器判断出是重复后直接返回上一次提交转向的页面。
6.使用拦截器实现权限验证
为了说明此问题,我们建立struts2auth项目,流程图如下:
简短说明:当我们访问main.jsp页面,并试图通过此页面中的链接地址:note.action来访问到.../WEB-INF/note.jsp页面时,由于访问的note.action配置了拦截器,所以会被拦截,如果拦截器判断登录则可以访问,否则会跳到登录页面。如果我们从登录页面直接到main.jsp页面,再来访问note.action时,同样被拦截但是由于登录过,所以可以访问到此action对应的内容。由这里的分析可以看出关键点就登录成功时给出标志提供给拦截器判断是否成功登录。
步骤一,搭建好相关的开发环境,并准备好登录页面login.jsp,代码如下:
<form action="<%=request.getContextPath()%>/login.action" method="post">
姓名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
步骤二,建立相应的Action:LoginAction。代码如下:
package com.asm;
public class LoginAction extends ActionSupport {
private String username;
Map session;
public String execute() throws Exception {
if(username.equals("admin")){
session = ActionContext.getContext().getSession();
session.put("loginSign", "loginSuccess");
return SUCCESS;
}else{
return LOGIN;
}
}
...省略username的get/set方法
}
说明:我们这里是设定了只有登录用户名为admin时,此Action才设置登录标志。另这里获取Session对象采取的是“与Servlet解耦合的非IOC方式”。
步骤三,编写拦截器类,代码如下:
package com.asm.interceptor;
public class AuthInterceptor extends AbstractInterceptor {
public String intercept(ActionInvocation invocation) throws Exception {
Map session = invocation.getInvocationContext().getSession();
// session=ActionContext.getContext().getSession();
if (session.get("loginSign") == null) {
return "login";
} else {
String result = invocation.invoke();
return result;
}
}
}
步骤四,配置此Action相关,主要配置内容如下:
<struts>
<package name="tokenTest" extends="struts-default">
<interceptors>
<interceptor name="auth"
class="com.asm.interceptor.AuthInterceptor">
</interceptor>
<interceptor-stack name="authStack">
<interceptor-ref name="auth"></interceptor-ref>
<interceptor-ref name="defaultStack"></interceptor-ref>
</interceptor-stack>
</interceptors>
<action name="login" class="com.asm.LoginAction">
<result name="success">/main.jsp</result>
<result name="login">/login.jsp</result>
</action>
<action name="note">
<result>/WEB-INF/note.jsp</result>
<result name="login">/login.jsp</result>
<interceptor-ref name="authStack"></interceptor-ref>
</action>
</package>
</struts>
说明:结合前面的一些代码来看,当我们为note.action配置了前面写所的AuthInterceptor拦截器时,如果我们要访问note.action,拦截器会首先判断是否登录,如果登录则继续把请求传递下去,如果没有登录则会返回到登录页面。
步骤五、编写相关的其它jsp页面,然后发布测试。此实例应重点是进一步掌握拦截器的配置使用。作为“实现资源权限访问”,此实例不具参考价值。
7.拦截器中的注解
AnnotationWorkflowInterceptor:Invokes any annotated methods on the action。意思是此拦截器可以调用在Action中任何有注解的方法。下面我们来演示它的使用,具体步骤如下:
步骤一,建立struts2annotationInt项目,并建立LoginAction类,代码如下:
package com.asm;
...省略导入的包
public class LoginAction extends ActionSupport {
private String username;
@Before
public String myBefore() {
System.out.println("调用myBefore方法");
return LOGIN;
}
@After
public void myAfter() throws InterruptedException {
Thread.sleep(5000);
System.out.println("----调用myAfter方法");
}
@BeforeResult
public void myBeforeResult() {
System.out.println("----调用myBeforeResult方法");
}
public String execute() throws Exception {
System.out.println("调用execute方法");
return SUCCESS;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
System.out.println("---调用set方法" + username);
this.username = username;
}
}
说明:要想使用方法成为被拦截器监视的注解方法,只需在方法关加上@...这样的形式并导入相关的类即可。
步骤二,编写相关的jsp及配置该Action,主要配置内容如下:
<struts>
<package name="ano" extends="struts-default">
<interceptors>
<interceptor name="anno" class="com.opensymphony.xwork2.interceptor.annotations.AnnotationWorkflowInterceptor">
</interceptor>
<interceptor-stack name="annoStack">
<interceptor-ref name="anno"></interceptor-ref>
<interceptor-ref name="defaultStack"></interceptor-ref>
</interceptor-stack>
</interceptors>
<action name="login" class="com.asm.LoginAction">
<result name="success">/success.jsp</result>
<result name="login">/login.jsp</result>
<interceptor-ref name="annoStack"></interceptor-ref>
</action>
</package>
</struts>
结合配置说明:当我们为LoginAction配置了AnnotationWorkflowInterceptor拦截器时,LoginAction中的所有注解方法才真正生效。下面重点是来讨论这些方法的执行顺序及作用。
加@Before注解的方法:will be invoked before the action method. If the returned value is not null, it is returned as the action result code。意思是在action的execute方法执行之前被调用,但是此方法如果返回不为空的话,它的返回结果将是真正的返回结果,比如这里我们return LOGIN,这样无论以什么用户名登录,它总会返回到login result(这里为login.jsp页面) 。但是从执前结果来看,在返回前仍执行了标记为@BeforeResult的方法:will be invoked after the action method but before the result execution。意思是在返回结果集前调用此方法。下面我们把public String myBefore()方法中的return LOGIN注释掉,并让修改此方法的返回类型为void。随后登录测试(注意要重新部署当前项目),可以发现执行结果如下:
调用myBefore方法
---调用set方法
调用execute方法
----调用myBeforeResult方法
----调用myAfter方法
从执行的顺序来看,标记为@After的方法最后执行,并且可以发现:它会延时5秒执行,但是在延时执行时,浏览器并没有成功跳到success.jsp页面,而是在5秒后,控制台打印出myArter方法中的内容同步跳转到success.jsp页面。@After :will be invoked after the action method and result execution。意为在execute方法执行并且返回结果后此方法被调用。但是从测试来看,标记为@After的方法是会影响到结果的返回(延时返回)。 强调:注意方法的执行顺序,相关的内容可以参看AnnotationWorkflowInterceptor类的api文档。
8.使用PreResultListener实现回调
在进行本实例前请前复习:五.2自定义拦截器。因为PreResultListener对象一般是绑定在拦截器上使用。
下面我们新建struts2PreResultListener项目进行测试。
步骤一,建立类,实现PreResultListener接口,主要代码如下:
package com.asm;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.interceptor.PreResultListener;
public class MyPreResultListener implements PreResultListener {
public void beforeResult(ActionInvocation invocation, String res) {
// System.out.println(invocation.getAction());
// System.out.println(invocation.getResultCode());
/**回调Action中的方法:
* LoginAction lg = (LoginAction) invocation.getAction(); try {
* lg.execute(); } catch (Exception e) { e.printStackTrace(); }
*/
System.out.println("检验到PreResultListener被执行");
}
}
步骤二,copy前面在自定义拦截器中用到的三个拦截器,并绑定MyPreResultListener对象,首先是在MyInterceptor类中,我们只需要修改intercept方法即可,代码如下:
public String intercept(ActionInvocation invocation) throws Exception {
invocation.addPreResultListener(new MyPreResultListener());
System.out.println("开始拦截");
String result = invocation.invoke();
System.out.println("结束拦截");
return result;
}
随后在MyMethodFilterInterceptor类中作类似修改。为了区别,我们在MyAbstractInterceptor类中不绑定MyPreResultListener对象。
步骤三,编写struts.xml文件,主要配置内容如下:
<struts>
<package name="interceptor" extends="struts-default">
<interceptors>
<interceptor name="myIpt" class="com.asm.MyInterceptor">
</interceptor>
<interceptor name="myAbs"
class="com.asm.MyAbstractInterceptor">
</interceptor>
<interceptor name="myMet"
class="com.asm.MyMethodFilterInterceptor">
</interceptor>
</interceptors>
<action name="login" class="com.asm.LoginAction">
<interceptor-ref name="myIpt"></interceptor-ref>
<interceptor-ref name="myAbs"></interceptor-ref>
<interceptor-ref name="myMet"></interceptor-ref>
<result name="success">/success.jsp</result>
</action>
</package>
</struts>
步骤四,编写相应的jsp页面,发布测试。
说明:此实例的只是简要地演示了PreResultListener的使用,所以相对简单。对于其它相关操作,我们可以从MyPreResultListener类注释掉的内容中找到一此端倪。强调:从执行结果来看,PreResultListener对象会在返回结果前执行,请注意结合拦截器执行的顺序来看。此实例目前作为了解。
六、使用标签
1.基础表单标签
准备工作:建立struts2tag项目,搭建好struts2的开发环境。在html我们常用的基础表单标签主要有文本域、密码域、提交、重置四种。它们在strust2中可以通过标签来生成。下面建立login.jsp页面,与这四种标签相关的内容如下:
<%@ page language="java" pageEncoding="utf-8"%>
<%@ taglib uri="/struts-tags" prefix="s" %>
<html>
<body>
<s:form action="login" method="post" namespace="/my">
<s:textfield label="用户名" name="user.username" required="true" requiredposition="right"/>
<s:password label="密码" name="user.password" required="true" />
<s:reset value="重置" align="left"/>
<s:submit value="注册" align="left"/>
</s:form>
</body>
</html>
说明:label中的内容是显示在表单前的提示内容,required设为true,表示此表单项为必填内容。
2.单选按钮和复选框:
<s:radio list="#{1:'男',0:'女'}" value="1" label="性别" name="user.sex"/>
<s:checkboxlist list="#{1:'足球',2:'排球',3:'蓝球',4:'网球'}" name="user.love" label="爱好"/>
3.三种方式实现下拉列表
<s:bean id="p" name="com.asm.NativePlaceFormAction"></s:bean>
<s:bean name="com.asm.NativePlaceMapFormAction" id="pMap"></s:bean>
<s:bean name="com.asm.NativePlaceProFormAction" id="pp"></s:bean>
<s:select list="#p.place" label="籍贯" name="user.place"/>
<s:select list="#pMap.place" label="籍贯2" name="user.place"/>
<s:select list="#pp.place" listKey="pId" listValue="pName" label="籍贯3" name="user.place" headerKey="-1" headerValue="---省---" emptyOption="true"/>
说明:三种方式实现下拉列表分别对应了三个java类,这三个类的内容为:
NativePlaceFormAction主要代码为:
package com.asm;
public class NativePlaceFormAction extends ActionSupport {
private List<String> place;
public NativePlaceFormAction(){
place=new ArrayList<String>();
place.add("山东省");
place.add("山西省");
place.add("河南省");
place.add("河北省");
place.add("四川省");
place.add("云南省");
}
...省略place的get/set方法
}
NativePlaceMapFormAction主要代码为:
package com.asm;
public class NativePlaceMapFormAction extends ActionSupport {
private Map<Integer, String> place;
public NativePlaceMapFormAction() {
place = new HashMap<Integer, String>();
place.put(1, "山东省");
place.put(2, "山西省");
place.put(3, "河南省");
place.put(4, "河北省");
place.put(5, "四川省");
place.put(6, "云南省");
}
...省略place的get/set方法
}
NativePlaceProFormAction主要代码为:
package com.asm;
public class NativePlaceProFormAction extends ActionSupport {
private List<Object> place;
public NativePlaceProFormAction(){
place=new ArrayList<Object>();
new Province(1,"山东省","济南");
place.add(new Province(1,"山东省","济南"));
place.add(new Province(2,"山西省","太原"));
place.add(new Province(3,"河南省","郑洲"));
place.add(new Province(4,"河北","石家庄"));
place.add(new Province(5,"四川","成都"));
place.add(new Province(6,"云南","昆明"));
}
...省略place的get/set方法
}
说明:此三种实现效果一样,但是在它们提交时传递给服务器的参数不同,具体可以参看login.jsp页面的源码。另外,这三种实现其实都依赖了<s:bean>设定的对象,如果我们不希望依赖<s:bean>来设定,可以通过配置action来实现:下面我们以NativePlaceFormAction说明:首先在struts.xml中配置此action,配置内容如下:
<action name="npf" class="com.asm.NativePlaceFormAction">
<result>/login2.jsp</result>
</action>
随后,我们在login.jsp中增加如下内容:
<a href="<%=request.getContextPath()%>/my/npf.action">另一个注册页面</a>
其中login2.jsp中的关键内容为:
<s:select list="place" label="籍贯" name="user.place"/>
我们可以发现:在login2.jsp中填写list的值时并没有用ognl表达式,因为我们通过npf.action来访问时,此Action已经被写入到了值栈中,所以我们可以直接引用。 后面所用到的实例,我们都会把这样的类做成Action,这样如果我们想通过这种方式访问便只需要在struts.xml中配置即可
4.二级联动
<s:bean name="com.asm.TwoSelectAction" id="ts"></s:bean>
<s:doubleselect
list="#ts.place"
listKey="pId" listValue="pName"
name="user.place"
doubleList="#ts.citys[top]"
doubleListKey="cId" doubleListValue="cName"
doubleName="user.city"
label="籍贯4(二级联动)">
</s:doubleselect>
它所依赖的TwoSelectAction类的主要代码如下:
package com.asm;
public class TwoSelectAction extends ActionSupport {
private List<Province> place;
private Map<Province,List<City>> citys;
...省略place 、citys中get/set方法
public TwoSelectAction(){
place= new ArrayList<Province>();
citys=new HashMap<Province,List<City>> ();
Province p1=new Province(1,"山东省","济南");
Province p2=new Province(2,"山西省","太原");
Province p3=new Province(3,"河南省","郑洲");
Province p4=new Province(4,"河北","石家庄");
Province p5=new Province(5,"四川","成都");
Province p6=new Province(6,"云南","昆明");
place.add(p1);
place.add(p2);
place.add(p3);
place.add(p4);
place.add(p5);
place.add(p6);
//山东省的市:
City c1=new City(1,"济南");
City c2=new City(2,"招远市");
City c3=new City(2,"寿光市");
List p1City=new ArrayList();
p1City.add(c1);
p1City.add(c2);
p1City.add(c3);
//山西省的市:
City c4=new City(4,"太原市");
City c5=new City(5,"大同市");
City c6=new City(6,"晋中市");
List p2City=new ArrayList();
p2City.add(c4);
p2City.add(c5);
p2City.add(c6);
//河南省的市:
City c7=new City(7,"郑州市");
City c8=new City(8,"卫辉市");
City c9=new City(8,"信阳市");
List p3City=new ArrayList();
p3City.add(c7);
p3City.add(c8);
p3City.add(c9);
//河北省的市:
City c10=new City(10,"石家庄");
City c11=new City(11,"晋州市");
City c12=new City(12,"鹿泉市");
List p4City=new ArrayList();
p4City.add(c10);
p4City.add(c11);
p4City.add(c12);
//四川省的市:
City c13=new City(13,"成都");
City c14=new City(14,"南充");
City c15=new City(15,"绵阳");
List p5City=new ArrayList();
p5City.add(c13);
p5City.add(c14);
p5City.add(c15);
//云南省的市:
City c16=new City(16,"昆明市");
City c17=new City(17,"安宁市");
City c18=new City(18,"曲靖市");
List p6City=new ArrayList();
p6City.add(c16);
p6City.add(c17);
p6City.add(c18);
citys.put(p1,p1City );
citys.put(p2,p2City );
citys.put(p3,p3City );
citys.put(p4,p4City );
citys.put(p5,p5City );
citys.put(p6,p6City );
}
}
简要分析:此实例有些繁琐,主要思想:我们的place对象主要为一级列表服务,只要理解了前面的下拉列表,这里不难理解一级列表。而二级列表中我们使用#ts.citys[top]取出的一个List对象,这样也正是下拉列表所要求的对象类型(List,Map),而top是非常关键的,它明确指出我们取出的是栈顶的对象,这样就能根据一级列表的值来动态生成这个List对象。
5.其它表单标签
<s:select name="singer" list="{}" label="歌星" headerKey="0" headerValue="--歌手名单--" emptyOption="true">
<s:optgroup list="#{1:'任贤齐',2:'刘德华',3:'周杰伦'}" label="男歌手"/>
<s:optgroup list="#{1:'萧亚轩',2:'蔡依林',3:'she'}" label="女歌手"/>
</s:select>
<s:combobox label="来源调查" list="{'朋友介绍','电视广告','网络广告'}" name="from" />
<s:updownselect
list="{'java','C#','VC','php','vb','vc','python'}"
moveDownLabel="下移一位"
moveUpLabel="上移一位"
selectAllLabel="全部选中"
label="您常用编程语言排名"
/>
<s:optiontransferselect
leftTitle="选择喜欢做的事:"
list="{'听歌','看电影','编程','玩游戏','chat'}"
name="love"
headerKey="0"
headerValue="喜欢做的事"
emptyOption="true"
rightTitle="选择讨厌做的事:"
doubleList="{'跳舞','唱歌','打篮球','旅游','shopping'}"
doubleName="hate"
doubleHeaderKey="0"
doubleHeaderValue="不喜欢的事"
doubleEmptyOption="true"
label="个人兴趣说明"
leftUpLabel="上移"
leftDownLabel="下移"
rightUpLabel="上移"
rightDownLabel="下移"
addToLeftLabel="<—添加"
addToRightLabel="添加—>"
addAllToLeftLabel="<—添加(All)"
addAllToRightLabel="添加(All)—>"
selectAllLabel="全选"
/>
<s:checkbox label="接受服务条款" value="false" name="user.accept"/>
有了前面的标签学习,这些标签很容易理解,只需结合显示效果和查看源码来加深它们的理解。但是特别要注意的是<s:checkbox>标签与</s:checkboxlist>的区别。
补充:使用struts2生成的表单标签会在标签内嵌套一些特殊的格式,在使用了struts2生成的标签所在网页内查看源代码可以发现多了一些如<tr><td>这样的格式化代码。如果不想struts2增加这些多余的格式化代码,可以在struts.xml中配置如下内容:
<!-- struts2生成的表单标签使用默认的主题,即不附加格式化标签 -->
<constant name="struts.ui.theme" value="simple"/>
6.其它常用标签的使用(代码参名为“补充”的文件夹下的tag.jsp)
(1)<s:set>标签
此标签主要用于设置一些属性值。
Scope:指定变量被设置的范围,该属性可以接受application、session、request、page或Action。如果没有设置该属性,则默认放置在OGNL Context中,我们可以通过#号来引用。
Value:赋给变量的值,如果没有设置该属性,则将ValueStack栈顶的值赋给变量。
Id/name/var:属性的引用名称,id/name均过时,建议用var来取代他们。
(2)<s:property>
Default:可选属性,如果需要输出的属性值为null,则显示属性指定的值
Escape:可选属性,指定是否格式化html代码。
Value:可选属性,指定需要输出的属性值,如果没有指定该属性,则默认输出ValueStack栈顶的值
Id:可选属性,指定该元素的标识
(3)<s:Iterator>
Value:可选属性,指定迭代的集合,如果没有指定该属性,则使用ValueStack栈顶的集合
Id:可选属性,指定集合里元素的id(已被标记为过时)
Status:可选属性,该属性指定迭代时当前迭代对象的一个实例,并把此实例放在ognl的上下文中,我们可以通过#号来引用这个实例。该实例包含如下几下方法:
Int getCount:返回当前迭代了几个元素。
Int getIndex:返回当前被迭代的元素的索引
Boolean isEven:返回当前被迭代的元素的索引是否是偶数
Boolean isOdd:返回当前被迭代的元素的索引是否是奇数
Boolean isFirst:返回当前被迭代的元素是否是第一个元素
Boolean isLast:返回当前被迭代的元素是否是最后一个元素
说明:因为iterator会把每次迭代的实例放在值栈的栈顶,而<s:property>默认访问的是值栈的栈顶元素。所以如下代码可行:
<s:set var="list" value="{'第一个','第二个','第三个'}"/>
<!-- iterator迭代的特点:会把迭代的对象放到值栈的栈顶 -->
<s:iterator value="#list">
<s:property/>
</s:iterator>
如果想用status来实现一些功能,可参下面的代码:
<br/>-------------------奇数红色,偶数蓝色---------------<br/>
<s:iterator value="#list" status="i">
<font color='<s:if test="#i.even">blue</s:if><s:else>red</s:else>' >
<s:property/>
</font><br/>
</s:iterator>
(4)url标签
<br/><br/>-----------使用url---------------<br/>
<s:set var="age" value="25" scope="request"/>
<s:url action="asm" namespace="/" >
<s:param name="age" value="#request.age"></s:param>
</s:url>
说明:它会根据action及namespace并附加上下文路径构建一个链接。
<br/><!-- value的值中一定要用单引号引起,这样才表示它的值是一个字串 -->
<s:set var="bdUrl" value="'http://www.baidu.com'" />
<s:url value="#bdUrl" /> <br/>
<s:url value="%{#bdUrl}" />
说明:由于url标签的value属性默认不支持ognl,所以我们要使用%{}来表示{}中的#bdUrl是一个ognl表达式。
七、国际化
尽管国际化不是重点内容,但是也有必要了解它的使用。在struts2中国际化有三种级别:分别是针对某个Action的action级别,针对package的package级别,针对webapp的webapp级别。下面我们建立struts2i18n项目来演示国际化在struts2中的使用。
1.action级别下的国际化
步骤一、首先是建立login.jsp及LoginAction,由于它们经常使用,在此省去它们的代码。
步骤二、建立资源文件,由于LoginAction在com.asm包中,所以我们应在com.asm包下我们建立两个资源文件:一个是中文LoginAction_zh_CN.properties、一个是英文LoginAction_zh_CN.properties。注意它们的名字相对固定,前面与Action的名字相同,后面是语言和国家代码。
英文资源文件内容如下:
login_page=login page
login_username=userName
login_password=password
login_sex=sex
login_male=male
login_female=female
login_submit=login
login_reset=reset
login_suc=Welcome {0}
中文资源文件,需要特别注意:我们应使用Myeclipse自带的MyEclipse properties Editer编辑器来打开此资源文件,并在properties视图下进行编辑,这样它会把中文进行编码(我们切换到source视图下可以看到经编码后的中文)。 这一步非常重要,否则会出现乱码。
步骤三,修改login.jsp中的内容:
<%@ page language="java" pageEncoding="utf-8"%>
<%@ taglib uri="/struts-tags" prefix="s"%>
<html>
<body>
<s:text name="login_page"/><br>
<s:label key="login_username"/>
<s:form action="/login.action" method="post">
<!--
<s:textfield label="用户名" name="username" required="true" />
-->
<s:textfield label="%{getText('login_username')}" name="username" />
<!--
<s:password label="密码" name="password" required="true"/>
-->
<s:password key="login_password" name="password" />
<!--
<s:radio list="#{1:'男',2:'女'}" value="1" label="性别" name="sex" />
-->
<s:radio list="#{1:getText('login_male'),2:getText('login_female')}" value="1" label="%{getText('login_sex')}" name="sex" />
<s:submit key="login_submit" /> <s:reset key="login_reset"/>
</s:form>
</body>
</html>
说明:对资源文件的引用,我们采取了两种方式:有的是通过在label中使用%{getText('资源文件中的key')}这样的形式,有的是通过key=资源文件中的key这种形式。需要注意在radio标签中list对资源文件的引用。另外需要注意:
<s:text name="login_page"/><br>
<s:label key="login_username"/>
它们的区别:前面是纯文本,后者是一个块。我们可以通过查看login.jsp的源码来证明。
步骤四、当我们直接访问login.jsp时会报错,因为在login.jsp中用到了资源文件,而资源文件又依赖于LoginAction,所以我们只能通过此Action来跳到login.jsp。但是使用包范围、全局范围的资源文件时,可以直接访问login.jsp文件实现国际化。操作步骤如下:
首先在LoginAction中增加一个方法:
public String doGoLogin() {
return LOGIN;
}
随后再在struts.xml中配置如下内容:
<package name="i18n" extends="struts-default" namespace="/">
<action name="login" class="com.asm.LoginAction">
<result name="success">success.jsp</result>
<result name="login">login.jsp</result>
</action>
</package>
接着再编写一个link.jsp页面,内容如下:
<a href="<%=request.getContextPath() %>/login!goLogin.action">登录</a>
直接访问Action中的方法 格式:doX(大写)xxx ---- ActionName!x(小写)xxx.action 注意此方法和前面二.7中相关方法的区别。 我们通过此Action跳转到login.jsp这样便能成功访问到login.jsp页面。
步骤五、在success.jsp中使用资源文件,主要内容如下:
<s:text name="login_suc">
<s:param value="%{username}"></s:param>
</s:text>
说明:在前面的资源文件中,我们配置了login_suc=Welcome {0},其中{0}表示占位参数,这里我们使用<s:param>来给此参数赋值。
步骤六、测试:在ie的internet选项中改变语言实现国际化的访问。
2.配置package的资源文件
同样在建立com.asm包下建立两个资源文件(package级别的资源文件名必须以package开头):取名为:package_zh_CN.properties,它的内容为:pack=pack属性值 和package_en_US.properties,它的内容为:pack=packageAttributeValue
然后再在login.jsp页面中增加如下内容:
<h4>测试包资源文件</h4>
<s:text name="pack"></s:text>
这样便完成了package级别的资源文件配置,最后发布测试。
3.app级别的资源文件
在src目录下建立两个资源文件,取名为myapp_en_US.properties,它的内容为:
app=appAttributeValue 和myapp_zh_CN.properties,它的内容为:
然后还需要在strust.xml中增加如下配置:
<constant name="struts.custom.i18n.resources" value="myapp"></constant>
注意:name是固定值,而value来自于这个资源文件的基名。
最后在login.jsp中增加如下内容:
<h4>测试app级别资源文件</h4>
<s:text name="app"></s:text>
这样便完成了app级别的资源文件配置,随后发布测试。
说明:action级的资源文件优先级别最高,app最低。Pack级别的资源文件可作用于同一个包,app级别的资源文件可作用于当前项目。
补充:在jsp页面中直接访问某个资源文件,struts2为我们提供了i18n标签,使用此标签我们可以在类路径下直接从某个资源文件中获取国际化数据,而无需任何配置:
<s:i18n name="XXX"> --xxx为类路径下资源文件的基名
<s:text name="">
<s:param></s:param>
</s:text>
</s:i18n>
而如果要访问的资源文件在类路径的某个包下(如action或package级别的资源文件),可以这样访问:
<s:i18n name="com/asm/资源文件基名">--com.asm为包名
4.使用资源文件的原理
我们建立ReadResourceFileTest类,代码如下:
package com.asm;
import java.util.Locale;
import java.util.ResourceBundle;
public class ReadResourceFileTest {
public static void main(String[] args) {
ResourceBundle rb=ResourceBundle.getBundle("com.asm.LoginAction", Locale.US);
System.out.println(rb.getString("login_suc"));
}
}
补充:在Action类(必须继承自ActionSupport)中获取资源文件的值的方法,可以使用如下代码:
String value = this.getText("资源文件的键名");
//获取资源文件的对应的值。如果想给资源文件中的占位符赋值,可以使用getText的重载方法。
ActionContext.getContext().put("XXX",value);//存放在request范围,供jsp获取此值
5.选择使用资源文件
其实在我们成功访问到login.jsp页面后,只要在地址栏中增加参数request_locale=en_US便可以正确切换到登录页面为英文。当然我们可以再链接根据此参数写这个资源文件的链接。当然我们也可借助一个新Action来实现,操作步骤如下:在login.jsp中增加如下代码:
<a href="change.action?request_locale=zh_CN">
<s:text name="chinese"></s:text>
</a>
<a href="change.action?request_locale=en_US">
<s:text name="english"></s:text>
</a>
change.action对应的配置为:
<action name="change" class="com.asm.ChangeLangAction">
<result>/login.jsp</result>
</action>
ChangeLangAction的主要代码如下:
package com.asm;
public class ChangeLangAction extends ActionSupport {
public String execute() throws Exception {
return SUCCESS;
}
}
以上是第一种方法,特别要注意,由于使用了不同Action,所以要资源文件这时只有pack级别和app级别的才起作用,所以这时还应把action级别的资源文件内容增加到app级别的资源文件中去。下面使用第二种方法,原理基本和上面一样,只需在此ChangeLangAction中增加一个新的字段String lang及相应的get/set方法,再增加一个新的方法changeLang,代码如下:
public String changeLang() throws Exception {
Locale locale = null;
System.out.println(lang);
if (lang.equals("zh")) {
// 显示中文
locale = Locale.CHINA;
System.out.println("======" + lang+locale);
} else {
// 显示英文
locale = Locale.US;
}
ActionContext.getContext().setLocale(locale); ServletActionContext.getRequest().getSession().setAttribute("WW_TRANS_I18N_LOCALE", locale);
return SUCCESS;
}
配置内容为:
<action name="cl" class="com.asm.ChangeLangAction" method="changeLang">
<result>/login.jsp</result>
</action>
在login.jsp中对应的链接为:
<a href="cl.action?lang=zh">
<s:text name="chinese"></s:text>
</a>
<a href="cl.action?lang=en">
<s:text name="english"></s:text>
</a>
这样操作后,当我们成功访问到login.jsp后,便可以点击链接来随意切换访问英文或中文页面。
八、验证机制
注意:要想实现校验,action必须继承自ActionSupport类。
1.基于手工编码的校验
我们建立struts2validate项目 ,其中reg.jsp页面主要代码如下:
<body>
<s:head/>
<h3>注册页面</h3>
<s:form method="post" action="reg" >
<s:bean name="com.asm.AgeAction" id="aa"></s:bean>
<s:textfield name="user.username" label="用户名"/>
<s:property value="errors.user.username"/>
<s:password name="user.password" label="密码"/>
<s:password name="user.password2" label="确认密码"/>
<s:select list="#aa.ageMap" name="user.age" label="年龄" headerValue="填写真实年龄" headerKey="0"/>
<s:reset value="重置" align="left" />
<s:submit value="注册" align="left"/>
</s:form>
</body>
说明:<s:head/>可以用来对验证信息进行一些美化效果处理,另在此页面中我们用到了一个AgeAction用来动态生成“年龄”表单项,在前面的表单标签中已用过类似的做法。AgeAction的代码如下:
package com.asm;
public class AgeAction extends ActionSupport {
private Map<Integer, String> ageMap;
public AgeAction() {
ageMap = new HashMap();
for (int i = 1; i <= 120; i++) {
ageMap.put(new Integer(i), i + "");
}
}
...省略ageMap的get/set方法
}
Reg action的配置如下:
<package name="validate" extends="struts-default">
<action name="reg" class="com.asm.RegAndLoginAction" method="reg">
<result name="success">/regSuc.jsp</result>
<result name="login">/reg.jsp</result>
</action>
</package>
根据配置,我们来看它的对应Action: RegAndLoginAction,代码如下:
package com.asm;
public class RegAndLoginAction extends ActionSupport {
private User user;
public String reg() throws Exception {
if (user.getUsername() == null || user.getUsername().equals("")) {
this.addFieldError("user.username", "用户名不能为空");
} else if (!Pattern.matches("^[a-zA-Z][a-zA-Z0-9_]{3,14}$", user.getUsername())) {
this.addFieldError("user.username", "用户名只能是以字母开头,后面可以跟字母、数字或下滑线,长度只能是4-15位");
} else if (user.getPassword() == null || user.getPassword().equals("")) {
this.addFieldError("user.password", "密码不能为空");
} else if (!user.getPassword().equals(user.getPassword2())) {
this.addFieldError("user.password2", "两次输入的密码不一致,请重新输入");
} else if (user.getAge() < 16) {
this.addFieldError("user.age", "未满16岁,不能注册");
}
if (this.hasFieldErrors()) {
return LOGIN;
}
System.out.println("reg success....");
return SUCCESS;
}
...省略user的get/set方法
}
说明:当reg.jsp提交给此Action对应的reg方法处理时,它会调用addFieldError把错误信息加到FiledError中去,关于这点,我们可以在前台reg.jsp页面中用<s:debug>调试时,可以看到值栈中的此Action对象中的fieldErrors对应着我们添加的错误信息,因此这点也就为我们取出验证信息提供一个参考,即是说我们可以取出此验证信息,对它进行美化处理,而不是按struts2默认来显示。 后面,我们接着对登录页面用login方法进行了类似的验证(在此省略),所以此action取名为regAndLoginAction.
补充:当我们把login.jsp页面的验证写完后,可以发现reg和login这两个方法显示相当的繁琐,对此我们可以专门把验证分别放在validateReg和validateLogin方法中去。我们新建一个Action来演示,新的RegAndLogin2Action主要代码如下:
package com.asm;
public class RegAndLogin2Action extends ActionSupport {
private User user;
@Override
public void validate() {
System.out.println("校验的统一出口,对所有方法进行校验:这里可以放一些公共的验证");
}
public void validateReg() {
...省略,对reg方法进行验证
}
public void validateLogin() {
...省略,对login方法进行验证
}
public String reg() throws Exception {
System.out.println("reg success....");
return SUCCESS;
}
public String login() throws Exception {
System.out.println("login success....");
return SUCCESS;
}
...省略user的get/set方法
}
说明:当reg.jsp提交给此Action对应的reg方法处理时,它会首先调用此reg方法专属的验证方法valiadteReg(注意取名规则:validate+方法名<首字母大写>),此方法验证完成后,会调用validate方法,此方法完成后才会调用reg方法。因此一般情况下,我们会把一些公共的验证放在validate方法中,而这些所有的验证方法也只进行验证处理,并把错误信息封装到fieldError字段中(或者其它字段)。reg这些真正执行的方法只进行一些其它处理(比如把注册信息写进数据库)。测试时需要修改把前面的配置注释掉,写上下面的配置:
<action name="login" class="com.asm.RegAndLogin2Action" method="login">
<result name="success">/logSuc.jsp</result>
<result name="input">/login.jsp</result>
</action>
<action name="reg" class="com.asm.RegAndLogin2Action" method="reg">
<result name="success">/regSuc.jsp</result>
<result name="input">/reg.jsp</result>
</action>
说明:配置中有一个input result的配置,因为带有validate的方法进行验证时,如果验证失败,会返回input所对应的result结果集。
简析校验流程:
(1)类型转换器请求参数执行类型转换,并把转换后的值赋给action中属性。
(2)如果在执行类型转换过程中出现异常,系统会将异常信息保存到ActionContext,conversionError拦截器将异常信息添加到fieldErrors里,不管类型转换是否出现异常都会进入第(3)步。
(3)系统通过反射技术调用action中的validateXxx()方法
(4)再调用action中的validate()方法
(5)经过上面4步,如果系统中的fieldErrors存在错误信息(即存放错误信息的集合size大于0),系统自动将请求转发至名为input的视图。如果系统中的fieldErrors没有任何错误信息,系统将执行action中的处理方法。
注意:经过以上过程的分析,可以知道如果类型转换失败,也会到input视图。
2.基于XML配置形式的校验
新建struts2validateXML项目,在此项目中,基本的代码和上面的struts2validate项目相似,只是在上一个项目中我们在Action的具体方法中进行了验证处理,现在先修改RegAndLoginAction的代码如下:
package com.asm;
public class RegAndLoginAction extends ActionSupport {
private User user;
public String reg() throws Exception {
System.out.println("reg success....");
return SUCCESS;
}
public String login() throws Exception {
System.out.println("login success....");
return SUCCESS;
}
...省略user的get/set方法
}
下面我们在action所在的包下建立一个对此Action进行校验的xml文件,文件名为:RegAndLoginAction-validation.xml,取名原则就是actionClassName-validation.xml 。它会对此Action中的所有方法进行校验。主要代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE validators PUBLIC
"-//OpenSymphony Group//XWork Validator 1.0.3//EN"
"http://www.opensymphony.com/xwork/xwork-validator-1.0.3.dtd">
<validators>
<field name="user.username">
<field-validator type="requiredstring">
<message>用户名不能为空</message>
</field-validator>
<field-validator type="regex">
<param name="expression">^[a-zA-Z][a-zA-Z0-9_]{3,14}$</param>
<message>
用户名只能是以字母开头,后面可以跟字母、数字或下滑线,长度只能是4-15位
</message>
</field-validator>
</field>
<field name="user.password">
<field-validator type="requiredstring">
<message>密码不能为空</message>
</field-validator>
</field>
</validators>
进行此配置,相当于在RegAndLoginAciton中增加用validate ()方法进行验证。如果我们想对某个方法进行验证,配置文件应取名为actionClassName-ActionName-validation.xml,比如我们对reg方法进行验证,在前面用到validateReg方法,这里只需增加RegAndLoginAction-reg-validation.xml配置文件即可,它的作用和validateReg方法相同,在此省略此配置文件内容。
关于验证的配置文件中用到的验证类型可以参看文档或者叁看压缩包中的配置参照文件,下面对校验器类型进行简单说明:
Required-必须校验器:要求field的值不能为null
Requiredstring-必须字串校验器:不能为null,用长度大于0,默认情况下会对字串去前后空格
int、[long、short、double]:整数值[long型、短整形、double型]型值必须在指定范围。参数min指定最小值,参数max指定最大值
date-日期校验器:日期校验类型,符合日期格式,用可以使用min/max来指定日期范围
expression-OGNL表达式校验器:expression参数指定ognl表达式,该逻辑表达式基于值栈进行求值,返回true时校验通过,否则不通过,该校验器不可用在字段校验器风格的配置中
fieldexpression-字段ognl表达式校验器:要求field满足一个ognl表达式,expression参数指定ognl表达式,该逻辑表达式基于值栈进行求值,返回true校验通过,否则不通过
email-邮件地址校验器:非空用为合法的邮件地址
url-网址校验器:非空用为合法的url地址
visitor-复合属性校验器:它指定一个校验文件用于校验复合属性中的属性
conversion-转换校验器:指定在类型转换失败时,提示的错误信息
stringlength-字符器长度校验器:要求字段必须在指定的范围内,否则校验失败。minLength参数指定最小长度,maxLength参数指定最大长度。Trim参数指定校验field之前是否去除字串前后的空格
regex-正则表达式校验器:校验字段是否与expression参数指定的正则表达式匹配。caseSensitive参数指定进行匹配时是否区分大小写,默认为true,即区分大小写。
补充:基于xml校验的一些特点
当为某个Action提供了ActionClassName-validation.xml和ActionClassName-ActionName-validation.xml两种规则的校验文件时,系统会按下面的顺序寻找校验文件:(1)ActionClassName-validation.xml (2)ActionClassName-ActionName-validation.xml
系统寻找到第一个校验文件时还会继续搜索后面的校验文件,当探索到所有校验文件时,会把校验文件里的所有校验规则汇总,然后全部应用于action方法的校验。如果两个校验文件中指定的校验规则冲突,则会只使用后面文件中的校验规则。
当action继承了另一个action,父类action的校验文件会先被搜索到。
假定UserAction继承BaseAction:
<action name="user" class="com.asm.UserAction" method="execute">访问上面的action,系统会先搜索父类的校验文件:BaseAction-validation.xml,BaseAction-user-validation.xml,接着搜索子类的校验文件:UserAction-validation.xml,UserAction-user-validation.xml.应用于上面action校验规则为四个文件的总和。
九、文件上传下载(了解)
首先建立struts2UpDownLoad项目,搭建好struts2基本的开发环境。
1. 上传实例
2. 步骤一:upload.jsp代码如下:
<s:form action="upload" method="post" enctype="multipart/form-data">
<s:file name="file" label="上传的头像(格式:jpg,gif,bmp)"></s:file>
<s:submit value="上传"/> <s:reset value="取消"/>
</s:form>
注意:在form标签中我们用到了enctype实体,这是上传时必须用到得。
步骤二,建立struts.xml。对upload.action的配置如下:
<action name="upload" class="com.asm.UploadAction">
<param name="savePath">img</param>
<result>/upSuc.jsp</result>
<result name="input">upload.jsp</result>
<interceptor-ref name="defaultStack">
<param name="fileUpload.maximumSize">1024*1024</param>
<param name="fileUpload.allowedTypes">
image/bmp,image/pjpeg,image/gif
</param>
</interceptor-ref>
</action>
在这里唯一需要说明的是<interceptor-ref>下的参数问题,在以前如果要为某个特定的拦截器传递参数需要在<interceptor>下配置pararm参数,在此处我们用.形式来配置fileUpload拦截器的参数。这用做即可以保证默认的拦截器栈起作用,也可以向fileUpload拦截器传递参数。第一个参数是限制上传图片的大小(除了可以这样限制图片大小,也可以配置一个常量的方法来限制上传文件的大小,配置的内容为:<constant name="struts.multipart.maxSize" value="文件大小"/>),第二个参数是限制上传图片的格式只能为bmp,pjpeg,gif关于这些参数可以参看fileupload拦截器对应类的api文档。
另还需注意:在action下的“<param name="savePath">img</param>”代码可以为UploadAction的savePath字段传递值,这样作的好处是在项目正式发布后,我们可以通过修改struts.xml中的配置来灵活给savePath赋值。而如果直接在java源代码中初始化savePath的值,在项目运行后就不能简单修改。这种用法主要是为了给客户提供一个灵活可配的特定初始化方式。
步骤三、编写UploadAction,主要代码如下:
package com.asm;
public class UploadAction extends ActionSupport {
private String savePath;
private File file;
private String fileFileName;
private String fileContentType;
public String execute() throws Exception {
String path=ServletActionContext.getServletContext().getRealPath(savePath);
String savaFileName=path+"\\"+fileFileName;
//System.out.println(savaFileName);
BufferedInputStream bis=null;
BufferedOutputStream bos=null;
try{
bis=new BufferedInputStream(new FileInputStream(file));
bos=new BufferedOutputStream(new FileOutputStream(savaFileName));
byte []buf=new byte[(int) file.length()];
int len=0;
while((len=bis.read(buf))!=-1){
bos.write(buf,0,len);
}}catch(Exception e){
e.printStackTrace();
}finally{
if(bis!=null)
bis.close();
if(bos!=null)
bos.close();
}
return SUCCESS;
}
...省略以上四个字段的get/set方法
}
说明:其实上传的难点就是在此action的处理上。首先是从配置文件中读取文件的保存路径,然后联合fileFileName(命名规则是上传的文件对应的字段名+FileName,如果要得到上传文件的类型,固定的写法应是上传的文件对应的字段名+ContentType,比如这里应为fileContentType)来确定完整的保存路径,并最终为创建BufferedOutputStream作准备。BufferedInputStream是通过前台upload.jsp页面传递的file构建。特别要注意处理流,如果书写不当可能会使上传文件循环写入,使得硬盘容量不够。还要注意对流的关闭问题。 补充:关于文件的操作可以使用commons-io.jar包的FileUtils类下的copyFile方法来进行文件的拷贝,比如这里调用copyFile方法(file,要保存的目录)
上传成功后,upSuc.jsp的主要内容如下:
<body>
<h4>上传成功,以下是你刚上传的图片:</h4>
<img src="<%=request.getContextPath() %>/<s:property value="savePath+'/'+fileFileName"/>"> <br>
保存路径为:<s:property value="savePath+'/'+fileFileName"/>
</body>
说明:当上传成功后,会显示上传的图片。
扩展实例:如果想上传多个文件,可以在Action中使用File[] files来接受上传的文件(jsp中对应的上传文件的参数均应为files)。对应的上传文件的名字,使用String[] fileFileName。然后循环files数组保存File文件对象。
2.下载实例
下载页面的doload.jsp的主要代码如下:
<a href="download.action?downloadName=img/a.bmp">下载图片</a><br>
<a href="download.action?downloadName=img/music.rar">下载千千静听</a><br>
对应的download action配置如下:
<action name="download" class="com.asm.DownloadAction">
<result name="success" type="stream">
<param name="inputName">targetFile</param>
<param name="contentType">
image/bmp,application/x-zip-compressed
</param>
</result>
</action>
说明:type类型指明了结果集为流类型,并且为流类型结果集配置了参数,inputName指定流的来源,这里来源为targetFile,所以在下面的Action中有getTargetFile方法, contentType指明下载时的文件类型。
DownloadAction的主要代码如下:
package com.asm;
public class DownloadAction extends ActionSupport {
private String downloadName;
public String execute() throws Exception {
return SUCCESS;
}
public InputStream getTargetFile(){
return ServletActionContext.getServletContext().getResourceAsStream(downloadName);
}
public void setDownloadName(String downloadName) {
this.downloadName = downloadName;
}
}
说明:下载实例在此略作了解,具体可以借助apache组织提供的上传下载开源项目理解。
十、类型转换
建立struts2conversion项目,并搭建好struts2的基本开发环境
1.基于Action的直接属性转换
建立t.jsp页面,内容如下:
<s:form action="phone" method="post">
<s:textfield name="thePhone" label="电话"/>
<s:submit value="提交"/>
<s:reset value="重置"/>
</s:form>
此action对应的配置如下:
<action name="phone" class="com.asm.action.PhoneAction">
<result name="success">tSuc.jsp</result>
<result name="input">/t.jsp</result>
</action>
对应的PhoneAction类的代码如下:
package com.asm.action;
public class PhoneAction extends ActionSupport {
private Telephone thePhone;
public String execute() throws Exception {
return SUCCESS;
}
...省略thePhone的get/set方法
}
说明,如果直接这样执行将会出错,因为前台t.jsp传的String默认是不能被转成这里的Phone对象,所以我们必须使用类型转换,而且我们配置了input result就是告诉我们如果类型转换失败,将会停在t.jsp页面,而且会报错。下面接着看怎么类型转换。在这里我们要把010-123456这样的电话换成:区号:010 电话:123456这样的形式时。具体的操作步骤如下:
创建类型转换类TelephoneConversion,代码如下:
package com.asm.conversion;
public class TelephoneConversion extends StrutsTypeConverter {
public Object convertFromString(Map context, String[] values, Class toClass) {
System.out.println("执行字串到Telephone对象的转换");
Telephone tp=new Telephone();
String [] tel=values[0].split("-");
tp.setSecNum(tel[0]);
tp.setPhone(tel[1]);
return tp;
}
public String convertToString(Map context, Object obj) {
System.out.println("执行Telephone对象到字串的转换");
Telephone tp=(Telephone) obj;
return "区号:"+tp.getSecNum()+"\t电话:"+tp.getPhone();
}
}
说明:类型转换类必须实现TypeConverter接口,而这里的StrutsTypeConverter类便是TypeConverter接口实现类DefaultTypeConverter的子类。此类中有两个方法,一个方法实现把字串转成其它对象,一个方法实现把其它对象转成字串。在convertFromString方法中,我们实现把客户端传递的字串转成Telephone对象,这样就能让PhoneAction的setThePhone方法得以正确执行。而后面的方法是为了我们要取值时进行的处理,比如在tSuc.jsp中我们要得到此值,需要把Telephone对象转换字串。其实如果没有convertToString方法,只要我们重写Telephone的toString方法也能达到目的。
写完类类型转换类后,我们还应告知struts2,所以我们还需建立一个properties文件。我们在PhoneAction的同包下建立PhoneAction-conversion.properties文件,它的主要代码如下:
thePhone=com.asm.conversion.TelephoneConversion
说明:这句话的意思是说我们要把PhoneAction(通过properties文件名可以知道要转换的是此Action)下的thePhone使用TelephoneConversion进行转换。其实我们也可以配置全局的properties文件说明,比如我们在src目录下建立xwork-conversion.properties文件(名字固定),它的内容如下:
com.asm.vo.Telephone=com.asm.conversion.TelephoneConversion
说明:它的意思是只要遇到Telephone对象,就要用后面的转换器来实行转换。
2.基于Action的间接属性vo转换
t2.jsp主要内容如下:
<s:form action="up" method="post">
<s:textfield name="user.thePhone" label="电话"/>
<s:submit value="提交"/>
<s:reset value="重置"/>
</s:form>
我们建立UserPhoneAction类,它的主要代码如下:
package com.asm.action;
public class UserPhoneAction extends ActionSupport {
private User user;
public String execute() throws Exception {
return SUCCESS;
}
...省略user的get/set方法
}
User类的代码如下:
package com.asm.vo;
public class User {
private Telephone thePhone;
...省略thePhone的get/set方法。
}
说明:通过这两个类及t2.jsp页面,我们知道,前台传递的thePhone对象不时直接传递,而是采用了vo模式,所以当我们配置类型转换时,要特别注意。因为前面我们使用了全局的类型转换,所以这里不会出错,但是当我们去掉前面的全局转换时,配置类型转换的properties文件就应在User类对应的包下配置User-conversion.properties文件,它的主要内容如下:
thePhone=com.asm.conversion.TelephoneConversion
说明及总结:类型转换的配置文件如果不采用全局的配置时,我们就应以要转换的类型的直接持有类为基准:比如,这里的thePhone的直接持有类为User对象,所以我们就应以User为基准写properties文件名。
十一、注解配置
在此先略去注解配置的实例,具体可以参看官方提供的文档。其实在熟悉struts及相关的一些内容后,再来看文档是比较容易理解得。只是要注意使用注解Annotition时:(1)要多导入一个jar包:struts2-convention-plugin-2.1.6.jar。(2)需要在web.xml中增加如下内容:
<filter>
<filter-name>struts2</filter-name>
<filter-class> org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter
</filter-class>
<!-- 增加了以下内容 -->
<init-param>
<param-name>actionPackages</param-name>
<param-value>com.struts2.action</param-value>
</init-param>
</filter>
十二、总结
本教程对struts2的基本知识进行了一些说明,关于struts2的更多详细内容应参看struts2的官方文档及提供的app实例。
下面对struts2的基本执行流程作一简要说明,此流程说明可以结合官方提供的struts2结构图来看:
n 客户端提交一个(HttpServletRequest)请求,如上文在浏览器中输入
http://localhost: 8080/appName/...就是提交一个(HttpServletRequest)请求。
n 请求被提交到一系列(主要是3层)的过滤器(Filter),如(ActionContextCleanUp、其他过滤器(SiteMesh等)、 FilterDispatcher)。注意:这里是有顺序的,先ActionContext CleanUp,再其他过滤器(Othter Filters、SiteMesh等),最后到FilterDispatcher。
n FilterDispatcher是控制器的核心,就是MVC的Struts 2中实现控制层(Controller)的核心。(有点struts1.x中ActionServlet的感觉)
n FilterDispatcher询问ActionMapper是否需要调用某个Action来处理这个(HttpServlet Request)请求,如果ActionMapper决定需要调用某个Action,FilterDispatcher则把请求的处理交给ActionProxy。
n ActionProxy通过Configuration Manager(struts.xml)询问框架的配置文件,找到需要调用的Action类。例如,用户注册示例将找到UserReg类。
n ActionProxy创建一个ActionInvocation实例,同时ActionInvocation通过代理模式调用Action。但在调用之前,ActionInvocation会根据配置加载Action相关的所有Interceptor(拦截器)。 关于ActionInvocation的执行过程我们在五、2自定义拦截器最后的补充中已经进行了较详细说明。
n 一旦Action执行完毕,ActionInvocation负责根据struts.xml中的配置找到对应的返回结果result。