Struts2学习笔记
Struts2是一个流行的基于Java的MVC框架,它基于WebWork,因而最初被称为WebWork2。它具有以下特性:
- 基于POJO的表单和Action。Struts1的ActionForm已经被废弃,Action现在也没有任何接口的限定。任何一个Java类都可以作为Action使用
- 改进的表单标签、新标签
- 集成的Ajax支持
- 易于和其它框架整合,例如Spring
- 支持模板技术,利用模板生成视图。这些模板技术包括Velocity、FreeMarker等
- 插件支持
- 需要更少的配置
类图和处理流程参考:MVC模式
Struts2由以下核心组件:Action、ActionContext、拦截器、值栈(ValueStack)、OGNL、结果/结果类型、视图构成:
关于Struts2的架构,需要注意:
-
与经典的MVC框架不同,Struts2的Action充当了Model角色,而不是控制器角色。Action有两大职责:
- 在一个方法中封装请求处理逻辑
- 作为数据传递的容器
- 结果路由,Action的返回值负责选择一个结果
-
控制器角色,则由一个Servlet过滤器:FilterDispatcher但当(此类已废弃,目前由StrutsPrepareAndExecuteFilter代替)
- 结果映射到一个视图
- Action执行时的上下文,存放在基于ThreadLocal的ActionContext中。此上下文包含了ValueStack、请求、会话等对象
- ValueStack是保存所有请求相关数据的存储区域,得益于ThreadLocal技术,这些数据在请求处理的全过程中都可以被访问
- OGNL是一种表达式语言,可以用于操控值栈中的数据
- 为了请求一些资源(即页面),用户发送请求到服务器
- 调用拦截器进行前置处理,实现验证,文件上传等功能
- FilterDispatcher 查看请求,然后确定哪个Action处理它,然后调用Action
- 拦截器进行后置处理
- 处理视图,返回给浏览器
Spring MVC和Struts是Java Web领域最流行的两个MVC框架,它们的可扩展性都足够好,能够满足绝大部分应用场景的需要。它们的比较如下:
- Spring MVC严格的分离控制器、JavaBean模型、视图等组件。而Struts2不是
- Spirng MVC是Spring框架的一部分,可以和Spring其它组件无缝继承
- Spring MVC更容易启用RESTful的Web服务
- Spring MVC对JSON、XML等Ajax常用请求/响应格式的支持非常好,能根据请求头自动决定使用JSON还是XML。Struts需要通过扩展结果类型来手工实现XML/JSON的支持;要实现对JSON、XML请求体的支持,必须扩展拦截器
- Spring MVC拦截器支持明确区分的preHandle、postHandle、afterCompletion三阶段,很容易在Handler(相当于Action)执行完毕之后,改变处理流程。然而Struts2不允许在postProcessing阶段改变处理流程,这导致定制404错误页面这样的功能难以基于拦截器实现
- Struts的拦截机制可以针对不同包灵活的定制
- Struts内置了Ajax支持
- 数据传递方式不同:Spring MVC采用方法参数注入;Struts采用JavaBean属性注入。前者的优势是,数据所属的业务非常明确,后者则会把多个业务的数据混在一起,大家都是属性——除非你编写很多细粒度的Action
- Spring的@Controller是单例的,而Action实例针对每次请求创建
本章以一个用户管理系统为例,阐述如何搭建Struts + Spring的基本框架,以及Struts的基本用法。工程结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/home/alex/JavaEE/projects/idea/ssm-study ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ │ └── cc │ │ │ └── gmem │ │ │ └── study │ │ │ └── ssm │ │ │ ├── action │ │ │ │ └── UserAction.java │ │ │ ├── entity │ │ │ │ └── User.java │ │ │ └── service │ │ │ └── UserService.java │ │ ├── resources │ │ │ ├── applicationContext.xml │ │ │ ├── log4j.properties │ │ │ └── struts.xml │ │ └── webapp │ │ ├── css │ │ │ └── style.css │ │ └── WEB-INF │ │ ├── jsp │ │ │ ├── AccessDenied.jsp │ │ │ └── Users.jsp │ │ └── web.xml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
<dependencies> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.15</version> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.13</version> </dependency> <dependency> <groupId>javax.annotation</groupId> <artifactId>jsr250-api</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>javax.inject</groupId> <artifactId>javax.inject</artifactId> <version>1</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>3.1.2.RELEASE</version> </dependency> <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-core</artifactId> <version>2.3.24</version> </dependency> <!-- 很多注解也定义在这个依赖中 --> <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-convention-plugin</artifactId> <version>2.3.24</version> </dependency> <!-- 此插件让Action能够被依赖注入,但不需要作为Spring Bean扫描、由Spring创建实例 --> <dependency> <groupId>org.apache.struts</groupId> <artifactId>struts2-spring-plugin</artifactId> <version>2.3.24</version> </dependency> </dependencies> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <context-param> <param-name>contextConfigLocation</param-name> <param-value> classpath*:applicationContext.xml </param-value> </context-param> <listener> <listener-class> org.springframework.web.context.ContextLoaderListener </listener-class> </listener> <!-- Struts2.1 新版本的过滤器 --> <filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class> </filter> <filter-mapping> <filter-name>struts2</filter-name> <!-- 映射/*,而不是 */.action**,这意味着所有的URL将被struts的过滤器解析 --> <url-pattern>/*</url-pattern> </filter-mapping> </web-app> |
1 2 3 4 5 6 7 8 9 10 11 |
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd "> <context:component-scan base-package="cc.gmem.study.ssm"/> </beans> |
Struts默认配置文件为struts.xml,你可以在此文件中引入其它配置文件。很多配置信息可以用注解代替。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE struts PUBLIC "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN" "http://struts.apache.org/dtds/struts-2.0.dtd"> <struts> <!-- 启用开发模式,可以得到一些日志信息。更改配置文件不需要重启 --> <constant name="struts.devMode" value="true"/> <!-- 指定URL的后缀,具有此后缀的URL,由对应的Action负责处理 --> <constant name="struts.action.extension" value="action" /> <!-- 包可以把相关的动作分为一组 --> <!-- Struts的URL构成:http://host:port/Servlet上下文/包命名空间/动作名.action --> <package name="usermgr" extends="struts-default" namespace="/usermgr"> <action name="home" class="cc.gmem.study.ssm.action.UserAction" method="home"> <!-- 结果命名了视图:当Action执行结果为success时,使用Users.jsp作为视图 --> <result name="success">/WEB-INF/jsp/Users.jsp</result> <!-- 可以定义多个结果 --> <result name="error">/WEB-INF/jsp/AccessDenied.jsp</result> </action> </package> </struts> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
package cc.gmem.study.ssm.action; import cc.gmem.study.ssm.entity.User; import cc.gmem.study.ssm.service.UserService; import com.opensymphony.xwork2.ActionSupport; import javax.inject.Inject; import java.util.List; // Action类可以选择扩展ActionSupport public class UserAction extends ActionSupport { // 自动进行依赖注入,不需要作为Spring Bean @Inject private UserService service; private List<User> users; // 添加getter,把Action的property变为值栈的Key,暴露给其它Struts组件 public List<User> getUsers() { return users; } // 对动作的唯一要求:是一个0参方法,返回字符串或者Result对象 public String home() throws Exception { users = service.loadAllUsers(); return SUCCESS; // 此常量来自ActionSupport } } |
1 2 3 4 5 6 |
public class User { private int id; private int age; private String name; // getter/setter/constructor略 } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Service public class UserService { private List<User> userdb; @PostConstruct public void init() { userdb = new ArrayList<User>(); userdb.add( new User( 1, 30, "Alex Wong" ) ); userdb.add( new User( 2, 30, "Meng Lee" ) ); userdb.add( new User( 3, 30, "Caicai Wong" ) ); } public List<User> loadAllUsers() { return userdb; } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%-- 声明使用Struts的标签库 --%> <%@ taglib prefix="s" uri="/struts-tags" %> <html> <head> <title>Users List</title> <%-- 静态资源的解析与框架无关 --%> <link rel="stylesheet" type="text/css" href="${pageContext.request.contextPath}css/style.css"/> </head> <body> <%-- 你可以使用EL表达式,或者标签库来访问值栈 --%> <div>Loaded ${users.size()} users <span class="comment">(<s:property value="users.size()"/> users available)</span>:</div> <div> <%-- 使用标签库来进行条件分支、迭代等操作 --%> <table> <tr> <th>ID</th> <th>NAME</th> <th>AGE</th> </tr> <s:iterator value="users"> <tr> <td><s:property value="id"/></td> <td><s:property value="name"/></td> <td><s:property value="age"/></td> </tr> </s:iterator> </table> </div> </body> </html> |
即使你启用了struts2-spring-plugin插件,默认情况下也仅仅是支持从Application Context获得注入,Action的生命周期仍然是由Struts2自己管理的。
要让Spring管理这些Action,你必须:
- 在Spring中扫描Action类
- 如果使用基于XML的Struts配置,则需要把action元素的class字段改为Spring Bean的ID
一般情况下,我们为每个请求创建一个Action,然而Spring Bean默认是单例的。因此在集成Spring时需要注意配置:
1 |
<bean id="userAction" class="cc.gmem.study.ssm.action.UserAction" scope="prototype" /> |
不管你使用XML还是注解方式进行配置,当框架运行时,Action和其它组件都被一起存放到称为包(Package)的逻辑容器内。在配置文件中包使用package元素表示。
包提供了一种基于功能(比如开放功能、需授权功能)或者业务共性(必须系统管理、设备管理)来分组Action的机制。包提供了继承机制,你可以继承Struts2预定义的包,拓展功能。
属性 | 说明 |
name | 包的名称,唯一标识符。其它组件可以通过此名称引用包 |
namespace |
限定了其内部Action的URL前缀,此前缀可以包含多个/ 两个包可以具有相同的命名空间 如果在命名空间内,找不到匹配的Action,Struts会到默认命名空间("")去寻找名字等于URL末段的Action 注意两个概念的不同:
|
extends |
该包继承自的包的名称,可以用逗号分隔,指定多个 当前包会获得所有父包定义的成员,还可以覆盖之 |
abstract | 如果为true,该包仅仅定义可继承组件,不能定义Action |
该包在struts-defaults.xml中定义,你可以获得很多组件——例如一组拦截器栈(interceptor-stack)、一组常用的结果类型(result-type):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<struts> <package name="struts-default" abstract="true"> <result-types>...</result-types> <interceptors> <!-- 拦截器栈,从上向下调用 --> <interceptor-stack name="defaultStack"> <interceptor-ref name="exception"/> <interceptor-ref name="alias"/> <interceptor-ref name="servletConfig"/> <interceptor-ref name="i18n"/> <interceptor-ref name="prepare"/> <interceptor-ref name="chain"/> <interceptor-ref name="scopedModelDriven"/> <interceptor-ref name="modelDriven"/> <interceptor-ref name="fileUpload"/> <interceptor-ref name="checkbox"/> <interceptor-ref name="datetime"/> <interceptor-ref name="multiselect"/> <interceptor-ref name="staticParams"/> <interceptor-ref name="actionMappingParams"/> <interceptor-ref name="params"/> <interceptor-ref name="conversionError"/> <interceptor-ref name="validation"> <param name="excludeMethods">input,back,cancel,browse</param> </interceptor-ref> <interceptor-ref name="workflow"> <param name="excludeMethods">input,back,cancel,browse</param> </interceptor-ref> <interceptor-ref name="debugging"/> <interceptor-ref name="deprecation"/> </interceptor-stack> </interceptors> <!-- 该包默认使用的拦截器栈 --> <default-interceptor-ref name="defaultStack"/> <default-class-ref class="com.opensymphony.xwork2.ActionSupport"/> </package> </struts> |
通常我们会从此包继承,你会获得一个默认的拦截器栈。struts2框架很多核心功能都是通过此拦截器栈完成的。
该组件是Struts2框架的核心,也是日常使用最多的组件。前面我们提到到过Action的三大职责:
- 封装逻辑:Action作为匹配URL的入口点,包含对业务逻辑的调用
- 数据容器:Action类的JavaBean属性自动作为ValueStack的直接Key,你需要的任何数据都可以作为Action属性来声明
- 结果路由:Action返回结果,指定了使用什么视图
注意与Struts1不同,Struts2的Action不是单例的,每个请求都被赋予一个新的Action实例。
任何POJO的方法都可以作为Action,作为Action的方法必须0-arg且返回Result或字符串。可选的,你可以实现Action接口,从而获得一些常量。
该类实现了Action和其它几个有用的接口。提供了:数据验证、错误消息本地化等功能。一般你可以选择从此类继承,编写新的Action。本节介绍此类提供的功能。
虽然Struts2提供了丰富、高度可配置的验证框架,但是ActionSupport可以让你快速的完成表单的基本验证。ActionSupport基于来自默认拦截器栈的workflow(DefaultWorkflowInterceptor)和两个接口(Validateable、ValidationAware)实现验证功能。
要实现验证,你可以覆盖Validateable的唯一方法:
1 2 3 4 5 6 7 8 9 10 11 |
public class UserAction extends ActionSupport { private User user; @Override public void validate() { if(user.getName() ==null){ // 创建和存储错误消息,方法来自ValidationAware,类似的还有addActionError addFieldError( "name", "User name is required"); } } } |
该方法会被workflow自动调用,调用后如果错误消息不为空,则workflow会改变请求处理流程,自动转向名字为input的结果。该结果对应的页面往往就是用户提交表单时的那个页面。
ActionSupport实现了TextProvider、LocaleProvider接口,通过这两个接口可以实现消息本地化:
- TextProvider提供了读取资源束的功能,你可以调用其getText()方法获得消息文本
- LocaleProvider只提供了一个获取当前Locale的方法
前面讨论包的时候我们提到过URL如何映射到Action:
- 由名字空间前缀 + Action名字 + Action后缀,确定一个Action对应的URL
- 对于默认名字空间中的Action,可以不考虑名字空间前缀进行匹配
除了这种简单的映射外,Struts2还支持通配符映射以及类似于Spring MVC的路径变量,这些方式都承担了一些数据绑定的职责。
Struts还支持所谓通配符映射,即在声明包的namespace、Action的name时,指定通配符:
- * ,通配0-N个字符,但是不能跨越斜杠/
- ** ,通配0-N个字符,且可以跨越斜杠/
所有通配符匹配的文本(捕获),被从1开始编号。然后你可以在Action配置文件中使用 {N} 的方式引用它:
1 2 3 |
<action name="/edit*" class="cc.gmem.study.ssm.Edit{1}Action"> <result>{1}.jsp</result> </action> |
如果多个通配符映射匹配了一个URL,那么,在配置文件最后面声明的那个映射被启用。
你可以在很多地方使用通配符捕获:
1 2 3 4 5 6 7 |
<action name="/edit/*" class="cc.gmem.study.ssm.EditAction"> <param name="id">{1}</param> <result> <param name="location">/mainMenu.jsp</param> <param name="id">{1}</param> </result> </action> |
默认的情况下,Action名字中是不允许出现斜杠的,你可以解除此限制:
1 2 |
<constant name="struts.enable.SlashesInActionNames" value="true"/> <constant name="struts.mapper.alwaysSelectFullNamespace" value="false"/> |
然后,就可以这样使用通配符映射了:
1 2 3 |
<action name="/person/*" class="cc.gmem.study.ssm.EditAction"> <param name="id">{1}</param> </action> |
启用此功能:
1 |
<constant name="struts.patternMatcher" value="namedVariable"/> |
把名字空间中的一段捕获为Action属性的例子:
1 2 3 4 5 6 |
<package name="crud" extends="struts-default" namespace="/crud/{entity}"> <!-- Action的entity字段使用捕获来填充 --> <action name="create" class="cc.gmem.study.ssm.action.CrudAction" method="create"> <result name="success">/WEB-INF/jsp/CreateEntity.jsp</result> </action> </package> |
从2.1.9+开始,可以启用此功能:
1 2 3 4 5 6 |
<constant name="struts.enable.SlashesInActionNames" value="true"/> <constant name="struts.mapper.alwaysSelectFullNamespace" value="false"/> <!-- 基于命名变量的路径匹配 --> <constant name="struts.patternMatcher" value="namedVariable" /> <!-- 或者,基于正则式的路径匹配 --> <constant name="struts.patternMatcher" value="regex" /> |
把Action路径中一段捕获为Action属性的例子:
1 2 3 4 5 |
<package name="crud" extends="struts-default" namespace="/crud"> <action name="/update/{entity}/{id}" class="cc.gmem.study.ssm.action.CrudAction" method="update"> <result name="success">/WEB-INF/jsp/update-${entity}.jsp</result> </action> </package> |
1 2 3 4 5 6 7 8 |
public class CrudAction extends ActionSupport { private String entity; private String id; public String update() throws Exception { return SUCCESS; } } |
注意:此功能不能和名字空间路径变量一起使用。
拦截器类似于Servlet过滤器,你可以使用拦截器实现横切(Cross-cutting)的逻辑:
- 在Action被调用之前提供Preprocessing逻辑
- 在Action被调用之后提供Postprocessing逻辑
- 捕获异常并处理
Action被调用时,其关联的拦截器栈会首先被调用。拦截器栈实际上是一系列拦截器构成的链条,之所以称为栈(也必须是栈,这取决于方法调用本身的特点),是因为最先Preprocessing的那个拦截器,最后获得Postprocessing的机会。拦截器由下面的接口定义:
1 2 3 4 |
public abstract class AbstractInterceptor implements Interceptor { // 拦截器接口并没有为预处理、后处理分别定义方法,所谓预、后只取决于调用此方法的时机: public abstract String intercept(ActionInvocation invocation) throws Exception; } |
如果继承struts-default包,你将获得一个默认的拦截器栈。Struts的拦截器栈非常灵活,除了可以使用默认的(满足大部分场景)的栈,你还可以为包、甚至Action定制拦截器栈。定制行为包括顺序重排、添加/删除拦截器等。
Struts框架提供的很多功能是基于拦截器实现的,例如异常处理,文件上传,生命周期回调和验证。Struts把这些通用的逻辑分离到拦截器中,让你的Action尽可能简洁。
当框架接收到一个HTTP请求后,首先需要决定此URL映射到哪个Action。此Action的一个新实例会被加入到一个新的ActionInvocation中。
接着框架查询配置信息,确定哪些拦截器需要按何种顺序触发,并把这些拦截器的引用加入到ActionInvocation中。
此外当前HttpServletRequest、当前Action可用的结果对象,也被ActionInvocation引用。
框架通过调用ActionInvocation.invoke方法,将控制权转移给ActionInvocation,后者是后续处理过程的总指挥。
ActionInvocation维持一个指针,随着拦截器的调用推进,该指针会不断移向下一个拦截器,直到最终指向Action。
ActionInvocation首先调用第一个拦截器的intercept方法。拦截器首先进行进行Preprocessing——准备、过滤、改变或者操控任何请求相关的数据(包括Action本身),Preprocessing完毕后通常会调用ActionInvocation.invoke,这导致下一个拦截器或者最后的Action被调用。
每个拦截器都具有改变请求处理流程的能力,因为请求处理流程本身就依赖于拦截器调用ActionInvocation.invoke方法来驱动。只要拦截器不调用invoke,自己返回一个控制字符串,正常处理流程即被中止——后面的拦截器、Action都不会被调用。例如workflow拦截器如果发现验证有错误,直接返回控制字符串input,input通常映射到出错的表单。注意:流传被中止后,立即开始当前拦截器的Postprocessing。
如果处理流程没有被终止,那么请求最终交由Action处理。Action执行完毕后,结果页面即渲染完毕。
每个拦截器都可以在ActionInvocation.invoke调用的后面添加Postprocessing代码。但此时Action已经被调用,Postprocessing不会影响到页面内容。
Struts2框架提供了一些开箱即用的拦截器,这些拦截器中,很多是默认拦截器栈的成员。括号内标注D的是默认拦截器栈成员。
记录请求处理消耗的时间,它在栈中的位置觉得了它记录的是哪些代码消耗的时间。
简单的日志记录,记录预处理的进入、后处理的退出。
将请求参数绑定到ValueStack公开的属性上。拦截器并不知道数据最终被送到哪里,它只是将其绑定到ValueStack第一个匹配的属性上去。
那么ValueStack中的属性是哪里来的呢?前面提过,Action对象在请求处理开始时即被放到ValueStack上。而对于ModelDriven暴露的模型,则由modelDriven拦截器负责放到ValueStack上。
与请求参数绑定类似,不同的是参数的来源。可以绑定Action声明的静态参数:
1 2 3 |
<action> <param name="dob">1986-09-12</param> </action> |
注意:在默认拦截器栈中,此拦截器先于params拦截器触发,因此请求参数可以覆盖静态参数。
让明明不是Spring Bean的Action,能够被Spring自动注入属性。
用于把Servlet API中的各种对象注入到Action字段中。你可以让Action实现不同的接口以获得相应的注入:
接口 | 注入 |
ServletContextAware | ServletContext |
ServletRequestAware | HttpServletRequest |
ServletResponseAware | HttpServletResponse |
ParameterAware | Map类型的请求参数集 |
RequestAware | Map类型的请求属性集 |
ApplicationAware | Map类型的ServletContext属性 |
PrincipalAware | 安全相关的Principal对象 |
使用此拦截器时,你只需要设置好表单的enctype以及Action属性即可。上传单个文件的例子:
1 2 3 |
<s:form action="upload" method="POST" enctype="multipart/form-data"> <s:file name="pic"/> </s:form> |
1 2 3 4 5 |
public class UploadAction { File pic; String picContentType; String picFileName; } |
如果要上传多个文件,则需要把上面三个属性都改为数组。
该拦截器与Action配合,提供数据验证机制,并在验证失败后改变后续工作流。workflow通过判断ValidationAware.hasErrors()的返回值,确认验证是否失败。
与Action一样,拦截器也可以通过参数来调整行为。workflow提供以下参数:
参数 | 说明 |
inputResultName | 验证失败时,使用的结果的名称,默认Action.INPUT |
excludeMethods | 逗号分隔的方法名,哪些Action入口方法不执行该拦截器 |
前面我们介绍过,ActionSupport基于Validateable接口,支持编程式的验证机制。Struts也支持声明式验证,而validataion过滤器则是声明式验证的核心。
注意:workflow并不关心你使用编程式、还是声明式的验证。它只负责去调用ValidationAware接口。因此,为了让validataion生效,必须将其放在workflow的前面。
用于把预处理代码转移到Action中,让后者决定做什么。该拦截器检查Action是否实现了Preparable接口,如果是则:
- 调用ActionClass.prepareActionName、ActionClass.prepareDoActionName根据firstCallPrepareDo参数决定调用顺序
- 如果alwaysInvokePrepare,调用ActionClass.prepare()方法
该拦截器调用getModel()方法,把模型对象放到ValueStack上接收请求参数。如果没有该拦截器,参数会被params拦截器直接绑定到Action对象上。
该拦截器是程序中丰富异常处理的基础。该拦截器位于默认拦截器栈的第一位,它也应该在所有栈的第一位,这样才能确保它有机会拦截所有异常。
根据异常类型,该拦截器会转向不同的错误页面。例如下面的配置:
1 2 3 4 5 6 |
<global-results> <result name="error">/error.jsp</result> </global-results> <global-exception-mappings> <exception-mapping exception="java.lang.RuntimeException" result="error"></exception-mapping> </global-exception-mappings> |
如果exception拦截器捕获了RuntimeException,它会显示error.jsp。
该拦截器和token-session拦截器可以用于防止表单重复提交。该拦截器检查请求中传入的令牌,如果同一令牌第二次出现,说明这是重复的表单提交。
该拦截器拓展了modelDriven的功能,允许模型跨请求存在(例如存放在Session中),可以实现向导式的业务。
拦截器/栈的声明必须位于package内部:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<package name="pkgname" abstract="true"> <interceptors> <!-- 声明一个拦截器,names是它的逻辑名称,供包内其它组件引用 --> <interceptor name="intname" class="package.IntName"/> <!-- 声明一个拦截器栈--> <interceptor-stack name="stackname"> <!-- 引用前面声明的拦截器、或者拦截器栈 --> <interceptor-ref name="alias"> <!-- 可以为拦截器配置参数 --> <param name="name">value</param> </interceptor-ref> </interceptor-stack> </interceptors> <!-- 声明包内Action默认使用哪个栈 --> <default-interceptor-ref name="stackname"/> </package> |
下面的声明,让Action使用具有两个拦截器的栈:
1 2 3 4 |
<action name="home" class="cc.gmem.study.ssm.action.UserAction" method="home"> <interceptor-ref name="timer"/> <interceptor-ref name="logger"/> </action> |
一般情况下,上面的配置是没意义的,因为Struts2大部分功能都位于默认栈中,因此我们一般可以追加引用默认栈:
1 2 3 4 5 |
<action name="home" class="cc.gmem.study.ssm.action.UserAction" method="home"> <interceptor-ref name="timer"/> <interceptor-ref name="logger"/> <interceptor-ref name="defaultStack"/> <!-- 必须扩展structs-default包,才能引用此栈 --> </action> |
设置拦截器参数,只需要简单声明param子元素即可。那么,如何覆盖默认拦截器栈中的某个拦截器的某个参数呢。也不复杂:
1 2 3 4 |
<interceptor-ref name="defaultStack"> <!-- 使用拦截器名.参数名指定name --> <param name="workflow.excludeMethods">hello</param> </interceptor-ref> |
值栈是Struts2中的一个数据结构,它由一系列对象构成。值栈对外表现为一个虚拟对象:一系列对象属性的聚合。虚拟对象的属性,就是栈上那些对象的属性,如果值栈中有两个对象具有name属性,那么位置更高(更加接近栈顶)的那个对象的属性,作为虚拟对象的属性。
值栈代表当前请求的数据模型(领域数据),是所有OGNL表达式求值时的默认上下文对象。
与请求处理相关的数据,不仅仅包含存放在值栈中的领域数据,一些更基础的、业务无关的数据也必须存储起来。所有这些数据,连同ValueStack,都被存放在ActionContext中。
尽管OSGL表达式默认的解析上下文是ValueStack,你可以通过指定ActionContext属性的名字,让OSGL针对其它对象求值。可用的ActionContext属性包括:
属性 | 说明 |
ValueStack | 默认的求值上下文,值栈 |
parameters | 当前请求中请求参数构成的Map |
application |
当前应用作用域属性的Map |
session | 当前会话作用域属性的Map |
request | 当前请求作用域属性的Map |
attr | 按照页面、请求、会话、应用顺序,返回第一个出现的属性 |
你可以随时使用下面的API得到当前的Action上下文并操控它:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
request = ServletActionContext.getRequest(); response = ServletActionContext.getResponse(); ActionContext context = ActionContext.getContext(); // 获得所有请求参数 Map<String, Object> parameters = context.getParameters(); // 获得当前会话属性 Map<String, Object> session = context.getSession(); // 获得当前Servlet上下文属性 Map<String, Object> application = context.getApplication(); // 获得并操控值栈 ValueStack vs = context.getValueStack(); vs.push( new User( "Alex", 31, "1986-09-12" ) ); vs.push( new User( null, 0, "2017-04-20" ) ); assert vs.findValue( "name" ) == null; |
OGNL(Object-Graph Navigation Language,对象图导航语言)是Struts2默认的表达式语言。它还帮助实现数据绑定、类型转换。在Struts中它是基于字符串的HTTP输入/输出与Java内部对象之间的粘合剂:
在数据进入值栈的阶段,OGNL解析请求中的点号导航,映射到值栈中的属性,根据此属性的类型,找到框架提供的类型转换器,转换值并设置到值栈上。
在数据进入视图的阶段,OGNL解析标签中的点号导航,根据类型转换器,把值栈属性值转换为字符串格式。
你可以在基于Struts2标签库的JSP中使用EL表达式。但是要注意:
- EL表达式是将pageScope、requestScope、sessionScope、applicationScope作为上下文来解析表达式的,此外它还可以访问param、paramValues、header、headerValues、cookie、pageContext等对象
- 在Struts2中EL表达式不是XSS安全的,你必须这样: ${fn:escapeXml(name)} 才能保证安全
在Struts标签中使用OGNL表达式时,如果标签属性不是String类型,则不必对OGNL表达式进行任何转义。如果标签的属性是String类型,需要用 %{ognlExpr} 格式转义:
1 2 3 4 5 6 |
<!-- 字面值 --> <s:tagname strattr="obj.field"/> <!-- 以值栈为上下文估算 --> <s:tagname strattr="%{obj.field}"/> <!-- 以Action上下文的obj属性来估算field --> <s:tagname strattr="%{#obj.field}"/> |
在Struts2等声明性配置文件中使用表达式时,必须使用类似于EL表达式语言的 ${ognlExpr} 格式转义。
OGNL大部分语法都是C风格的,这里用若干示例来阐述:
1 2 3 4 5 6 7 |
// 简单的属性导航 alex.address.tel // 集合导航 list[0] array[0] list[0].address users['alex'].addresses[3].city |
1 2 3 |
// 可以直接赋值给属性导航表达式 alex.address.tel = '13309029039' list[0] = 100 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// 使用过Set set.iterator // 返回Set的迭代器 // 列表、数组属性 list.size // list.size() array.length // array.length list.isEmpty // list.isEmpty() {1, 2, 3} // 动态创建List,这是一个直接量 // Native构造法 new int[] { 1, 2, 3 } new int[5] // 使用Map map['stringkey'] map.stringkey map[123] map['alex'].age map.size map.isEmpty #{1:"one", 2:"two"} // 动态创建Map,这是一个直接量 // 指定Map的Java类型 #@java.util.LinkedHashMap@{ "foo" : "foo value", "bar" : "bar value" } // 使用迭代器 iter.next // 获得下一个元素 iter.hasNext // 判断是否存在下一个元素 // 集合过滤,语法:collectionName.{? expr} users.{? #this.age > 30} // 获得一个子集,每个用户的age大于30 // 得到集合第一个匹配的元素 objects.{^ #this instanceof String } // 得到集合中最后一个匹配的元素 objects.{$ #this instanceof String } // 集合投影,语法:collectionName.{expr} users.{firstName + '' + lastname} // 根据两个字段建立新的集合 objects.{ #this instanceof String ? #this : #this.toString()} |
1 2 3 4 5 6 7 8 9 |
// 字面值 true // boolean 'c' // char 'str' // String 123.45 // double 123b // BigDecimal 123h // BigInteger {1, 2, 3} // List {'alex':30} // Map |
1 2 3 4 5 6 |
// 0参方法调用 user.getAge() // 1参方法调用。注意:括号(ensureLoaded(), name) 内的是逗号表达式,最后一个作为表达式的值 method( (ensureLoaded(), name) ) // 2参方法调用 method( ensureLoaded(), name ) |
1 2 3 |
// 静态成员访问: @fullclassname@property_or_methodcall @cc.gmem.study.ssm.User@DEFAULT_AGE @cc.gmem.study.ssm.User@load(1) |
1 2 3 4 5 |
// 当前上下文对象,在Struts中通常是值栈 #this.getClass().getName() // 在链式子表达式中访问当前上下文,这里上下文是一个数字 listeners.size().(#this > 100? 2*#this : 20+#this) |
OGNL表达式解析时,默认使用的上下文对象是ValueStack,此时栈顶对象的属性会被优先读取。使用此默认上下文时,不需要任何特殊语法,例如你要访问值栈的alex.name属性,只需要用表达式 alex.name 。
要改变OSGL的解析上下文,必须使用井号前缀:
1 2 3 4 5 |
// actionContextProperty['property'].ognlExpr #session['alex'].name // actionContextProperty.property.ognlExpr #session.alex.name |
如果你在点号后面直接使用括号,那么括号内的表达式的上下文是点号前的那个对象:
1 2 |
// ensureLoaded()、name的上下文是parent属性 headline.parent.(ensureLoaded(), name) |
1 2 3 4 |
// 定义一个Lambda #fact = :[#this<=1? 1 : #this*#fact(#this-1)] // 调用Lambda #fact(30H) |
OGNL可以使用绝大部分来自Java的操作符,但是需要注意行为的差异、操作符扩展:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 逗号操作符,借用自C语言,返回最后一个子表达式的值 ensureLoaded(), name // in、not in操作符,判断元素是否存在于集合中 name in {null,"Untitled"} id not in {1, 2, 3} || baid // 很多操作符都有对应的单词化版本: ! e1 // not e1 e1 || e2 // e1 or e2 e1 && e2 // e1 and e2 e1 | e2 // e1 bor e2 e1 ^ e2 // e1 xor e2 e1 & e2 // e1 band e2 e1 == e2 // e1 eq e2 e1 != e2 // e1 neq e2 e1 < e2 // e1 lt e2 e1 <= e2 // e1 lte e2 e1 > e2 // e1 gt e2 e1 >= e2 // e1 gte e2 e1 << e2 // e1 shl e2 e1 >> e2 // e1 shr e2 e1 >>> e2 // e1 ushr e2 |
使用Struts2时,你可以通过几种方式完成数据绑定:
- 为Action声明多个简单属性,每个属性容纳一个表单字段
- 为Action声明一个对象属性,把整个表单绑定到此对象,表单字段的名称必须使用点号导航
- 实现泛型接口ModelDriven,把表单绑定到特化后的泛型参数类型的对象上
从基于文本的请求参数到各种类型的Java对象的绑定,必然牵涉到数据类型转换。Struts2自带了大量常用的类型转换器,你也可以开发自己的类型转换器。
表单页面如下:
1 |
<s:textfield name="name" label="User Name" /> |
Action如下:
1 2 3 4 |
public class UserAction extends ActionSupport { private String name; // getter/setter略 } |
结果页面如下:
1 |
User Name: <s:property value="name"/> |
结合数据收集、处理、显示这三个步骤,我们可以看到单个属性的绑定非常简单。就是根据表单字段名和Action属性名的相同性进行绑定。
表单页面如下:
1 |
<s:textfield name="user.name" label="User Name" /> |
Action如下:
1 2 3 4 5 6 7 8 9 10 11 |
public class UserAction extends ActionSupport { private User user; // getter/setter略 @Override public void validate() { if(user.getName() ==null){ // 注意Field总是要和表单字段名对应 addFieldError( "user.name", "User name is required"); } } } |
结果页面如下:
1 |
User Name: <s:property value="user.name"/> |
还是差不多,区别只是用点号导航来定位属性,而Action属性和点号导航根对象名字相同。
这是一个泛型接口,只有一个方法getModel(),即返回当前业务使用的模型对象。你的Action必须实现此方法:
1 2 3 4 5 6 7 8 9 |
public class UserAction implements ModelDriven<User> { private User user; public User getModel() { // 你必须实现此方法并初始化模型 return new User(); } } |
需要注意的是,Action执行时,此方法已经被框架调用过,并在请求处理过程中一直作为数据绑定的容器。因而你不能在Action中改变user变量指向的对象。
使用ModelDriven的好处是避免不必要的点号导航。以用户管理的例子来说,你的表单字段名称和简单属性绑定时一样。
Struts2内建了从HTTP字符串到下列Java类型的转换支持:
- 转换为String:直接使用原始值
- 转换为Boolean/boolean:true和false字符串分别转换为对应的布尔值
- 转换为Character/char:直接使用原始值
- 各种数字类型及其包装类:直接使用原始值
- 转换为Date:使用当前Locale的SHORT格式来解析原始值
- 转换为数组:每个原始值的元素,依次转换数组元素类型的Java对象
- 转换为List:默认创建元素为字符串的列表
- 转换为Map:默认创建键值为字符串的Map
为了使用这些转换器,你必须构建OGNL表达式。这些表达式可能是表单字段的名称,或者Struts2标签的属性。
注意:尽管OGNL内置了类型转换接口ognl.TypeConverter,但是Struts2没有直接从它扩展出上述内置类型转换器,而是使用了适配器模式。Struts2自己的转换器接口为:com.opensymphony.xwork2.conversion.TypeConverter
OGNL/Struts2的类型转换器接口,可以针对任意类型之间的相互转换。但是在我们数据绑定的场景中,仅仅需要字符串与其它类型的转换,因此可以选择从
1 2 3 4 5 6 |
public abstract class StrutsTypeConverter extends DefaultTypeConverter { protected Object performFallbackConversion(Map context, Object o, Class toClass) { return super.convertValue(context, o, toClass); } public abstract String convertToString(Map context, Object o); } |
扩展。使用该扩展点,还可以访问一个代表了当前Action上下文的Map对象。
你可以通过两种方式注册类型转换器:
- 配置到ActionClassName-conversion.properties,则转换器仅仅用于一个Action类:
12# Action类的customField,使用的转换器:customField=package.CustomFieldTypeConverter - 配置到xwork-conversion.properties,则转换器用于全局:
12# CustomField类型,使用的转换器:package.CustomField=package.CustomFieldTypeConverter
Struts默认的日期格式可能不太受欢迎,我们可以实现自己的日期类型转换器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import org.apache.struts2.util.StrutsTypeConverter; public class DateConverter extends StrutsTypeConverter { public static final String DATE_FORMAT = "yyyy-MM-dd"; public static final String[] DATE_FORMATS = { "yyyy-MM-dd", "yy-M-d" }; @Override public Object convertFromString( Map context, String[] values, Class toClass ) { String dateStr = values[0]; try { return DateUtils.parseDate( dateStr, DATE_FORMATS ); } catch ( ParseException e ) { return null; } } @Override public String convertToString( Map context, Object o ) { Date d = (Date) o; return DateFormatUtils.format( d, DATE_FORMAT ); } } |
修改全局设置:
1 |
java.util.Date=com.ecfund.base.struts2.DateConverter |
你可以使用OGNL表达式作为表单字段的name,这样就可以自动绑定到ValueStack属性。
如果属性是基本类型、字符串,映射很简单,直接根据点号导航匹配,然后调用内置转换器转换就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<!-- 方式一: --> <s:textfield name="ages" /> <s:textfield name="ages" /> <s:textfield name="ages" /> <!-- 方式二: --> <s:textfield name="names[0]" /> <s:textfield name="names[1]" /> <s:textfield name="names[2]" /> <!-- 如果数组/列表属性元素是对象,可以使用属性导航: --> <s:textfield name="users.age" /> <s:textfield name="users[1].age" /> |
这两种方式都可以实现数组、列表类型属性的绑定:
- 第一种方式提交1个请求参数,值为3个字符串构成的数组。而ages属性类型为int[],因此框架为每个字符串执行String-int的转换
- 第二种方式提交3个请求参数
转换为List时,与数组类似。如果没有使用泛型,则认为列表元素都是字符串类型的;如果使用了泛型,执行相应的类型转换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<!-- 方式一: --> <s:textfield name="nameAddrMapping.alex" /> <s:textfield name="nameAddrMapping.meng" /> <s:textfield name="nameAddrMapping.cai" /> <!-- 方式二: --> <s:textfield name="nameAddrMapping['alex']" /> <s:textfield name="nameAddrMapping['meng']" /> <s:textfield name="nameAddrMapping['cai']" /> <!-- 如果映射属性的Value是对象,可以使用属性导航: --> <s:textfield name="nameAddrMapping.alex.tel" /> <s:textfield name="nameAddrMapping['alex'].tel" /> |
与List类似,Map映射也支持基于泛型的类型推导、类型转换。
Action执行的最后一件事情,就是返回一个控制字符串——其中包含结果的逻辑名称,框架根据此名称,找到对应的视图进行渲染。
结果是一个比较抽象的Struts2概念,它是对MVC模式中,视图关注点的封装。在经典的Web应用中,这些视图关注点就是返回给客户的HTML页面;而在基于Ajax的应用中,视图关注点则是JSON、XML等纯数据。
如果仅仅使用JSP作为视图技术,你基本不需要了解结果的细节。struts-defaults包的默认的dispatcher结果类型支持JSP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<package name="struts-default" abstract="true"> <result-types> <result-type name="chain" class="com.opensymphony.xwork2.ActionChainResult"/> <!-- dispatcher是默认的结果类型 --> <result-type name="dispatcher" class="org.apache.struts2.dispatcher.ServletDispatcherResult" default="true"/> <result-type name="freemarker" class="org.apache.struts2.views.freemarker.FreemarkerResult"/> <result-type name="httpheader" class="org.apache.struts2.dispatcher.HttpHeaderResult"/> <result-type name="redirect" class="org.apache.struts2.dispatcher.ServletRedirectResult"/> <result-type name="redirectAction" class="org.apache.struts2.dispatcher.ServletActionRedirectResult"/> <result-type name="stream" class="org.apache.struts2.dispatcher.StreamResult"/> <result-type name="velocity" class="org.apache.struts2.dispatcher.VelocityResult"/> <result-type name="xslt" class="org.apache.struts2.views.xslt.XSLTResult"/> <result-type name="plainText" class="org.apache.struts2.dispatcher.PlainTextResult" /> <result-type name="postback" class="org.apache.struts2.dispatcher.PostbackResult" /> </result-types> </package> |
所谓结果类型,就是例如上面的XML中声明的,结果的种类(Class)。实例化后的结果类型,就是结果,两者是类与实例的关系。
所有结果类都必须实现接口:
1 2 3 4 5 6 |
public interface Result extends Serializable { // 继续处理Action调用过程,完成视图显示的服务器端逻辑: // 例如生成网页、生成电子邮件、发送JSM消息,等等 public void execute(ActionInvocation invocation) throws Exception; } |
该结果类型用于JSP、Servlet等类型的视图,dispatcher是struts-default包默认的结果类型。它的参数包括:
- location,默认参数,目标视图的位置
- parse,指定是否将location参数中的 ${expr} 作为OGNL表达式来解析
dispatcher依赖于javax.servlet.RequestDispatcher,后者能够将请求处理权从一个Servlet转向另外一个(通常此Servlet是JSP)。RequestDispatcher提供了两种方法来转移处理权:
- include():临时转移处理权,当前Servlet已经在输出响应内容,而需要另外一个Servlet的输出时,使用此方式
- forward():永久转移处理权,使用此方式时,当前Servlet不能已经输出任何响应
这两种方式都让第二个Servlet得到当前请求、响应对象,让它处理同一个用户请求。
RequestDispatcher与HTTP重定向不同,后者是向浏览器发送一个重定向响应,前者的全部处理都发生在服务器内部,浏览器不知情。
该结果用于将浏览器重定向到另外一个URL,与当前请求有关的全部信息将被丢弃。它的参数包括:
- location,默认参数,目标视图的位置
- parse,指定是否将location参数的 ${expr}作为OGNL表达式来解析
示例: <result type="redirect">http://gmem.cc/${postId}</result>
与redirect类似,但是此结果类型能够识别Action的名称和名字空间,并且指定重定向时使用的请求参数:
1 2 3 4 5 6 7 |
<result type="redirectAction"> <param name="actionName">home</param> <param name="namespace">/usermgr</param> <!-- 下面指定请求参数:--> <param name="param1">staticValue</param> <param name="param2">${dynamicValue}</param> </result> |
Velocity和FreeMarker是Struts2支持的、用于代替JSP的视图技术。与它们对应的结果类型已经内置在Struts2框架中。
使用此结果类型的Action配置示例如下:
1 2 3 |
<action name="home" class="cc.gmem.study.ssm.action.UserAction" method="home"> <result name="success" type="velocity">/WEB-INF/jsp/Users.vm</result> </action> |
基于标签库的代码: <s:property value="users.size()"/> 在Velocity中等价语法是# sproperty("value=users.size()") 。
FreeMarker内置于Struts2框架中,不需要引入额外的JAR包。使用此结果类型的Action配置示例如下:
1 2 3 |
<action name="home" class="cc.gmem.study.ssm.action.UserAction" method="home"> <result name="success" type="freemarker">/WEB-INF/jsp/Users.ftl</result> </action> |
基于标签库的代码: <s:property value="users.size()"/> 在Velocity中等价语法是# <@s.property value="users.size()" /> 。
你可以在包内配置全局结果,这种结果可以被包内所有Action使用。当一个动作返回匹配一个控制字符串后,框架首先检查Action自身配置的结果,如果找不到匹配项,就会转而使用全局结果。
全局结果主要用于错误处理:
1 2 3 |
<global-results> <result name="error">/error.jsp</result> </global-results> |
基于Ajax的Web应用通常使用JSON作为响应格式,这是默认的dispatcher无法满足的。我们可以自己实现一个JSON结果类型,满足Ajax应用的需要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public class JSONResult implements Result { // 结果默认参数名,该参数的值可以直接作为result元素的文本,而不是param子元素 private static final String DEFAULT_PARAM = "property"; // 把值栈的什么属性取出来转换为JSON private String property = "jsonResult"; public String getProperty() { return property; } public void setProperty( String property ) { this.property = property; } public void execute( ActionInvocation invocation ) throws Exception { HttpServletResponse resp = ServletActionContext.getResponse(); // 设置合适的响应类型 resp.setContentType( "text/json;charset=UTF-8" ); PrintWriter out = resp.getWriter(); // 从值栈得到客户端需要的数据并转换为JSON ValueStack vs = invocation.getStack(); Object jsonModel = vs.findValue( getProperty() ); // 把数据转换为JSON格式,写入到响应 out.println( JSONUtils.toJSON( jsonModel ) ); } } |
实现了结果类型后,还必须将其注册到Struts才能生效:
1 2 3 4 5 |
<package name="usermgr" extends="struts-default" namespace="/usermgr"> <result-types> <result-type name="json" class="cc.gmem.study.struts2.JSONResult"></result-type> </result-types> </package> |
1 2 3 4 5 6 7 8 |
<package name="usermgr" extends="struts-default" namespace="/usermgr"> <action name="userinfo" class="cc.gmem.study.ssm.action.UserAction" method="home"> <!-- 由于json不是此包的默认结果类型,因此需要声明 type --> <result name="success" type="json"> <param name="property">user</property </result> </action> </package> |
由于JSONResult的默认参数就是Property,因此result元素可以简写为:
1 |
<result name="success" type="json">user</result> |
当客户端请求/usermgr/userinfo时Action的处理结果是success时,Struts会调用我们的JSONResult,并把值栈上的user属性编码为JSON文本,写到响应体中。
Struts2提供了大量不同类型的标签,这些标签有针对不同视图技术——JSP、Velocity、FreeMarker的语法变体。这些标签可以分为四个类别:数据标签、流程控制标签、UI标签、其它标签。
Struts2标签API定义了一个独立于视图技术的抽象层, 该API指定了标签的公开参数和属性。针对三种视图技术的标签语法,差异并不大,并且有规律可循。
在JSP中使用标签库,必须声明:
1 |
<%@ taglib prefix="s" uri="/struts-tags" %> |
标签用法示例:
1 2 3 4 5 6 7 8 |
<%-- 数据标签:格式化一个属性为文本 --%> <s:property value="name"/> <%-- UI标签:表单 --%> <s:form action="/usrmgr/updateuser"> <s:textfield name="name" label="User Name"/> <s:password name="password" label="Password"/> <s:submit value="Submit"/> </s:form> |
在JSP中,你可以使用EL表达式来访问值栈属性: ${expr} 。
1 2 3 4 5 6 7 |
#sproperty("value=name") #sform("action=/usrmgr/updateuser") #stextfield("label=User Name" "name=name") #spassword("label=Password" "name=password") #ssubmit("value=Submit") #end |
它的语法和JSP语法非常接近:
1 2 3 4 5 6 |
<@s.property value="name" /> <@s.form action="/usrmgr/updateuser"> <@s.textfield name="name" label="User Name" /> <@s.password name="password" label="Password" /> <@s.submit value="Submit" /> </@s.form> |
为Struts标签设置属性时,你需要搞清楚,目标属性期望的是一个字符串字面值还是ValueStack属性的OGNL表达式。
如果某个标签属性的类型是字符串,那么传递给它的值被作为字面值解析;否则,作为OGNL表达式解析。
如果要强制作为OGNL表达式解析,可以使用转义序 %{expr} :
1 2 3 4 |
<!-- 默认值是字面值: --> <s:property value="nonExistingProperty" default="defaultValue"/> <!-- 默认值是OGNL: --> <s:property value="nonExistingProperty" default="%{defaultValue}"/> |
标签 | 说明 | ||
property |
将ValueStack或者ActionContext上其它对象的属性值输出到HTML中,属性列表:
|
||
set |
在某个作用域设置一个属性,属性列表:
示例:
|
||
push | 把一个对象压到值栈的顶端,这个对象的属性将获得最高优先级——优先代表虚拟对象的属性。示例:
在此标签的结束标签处,此对象从值栈弹出 |
||
bean |
可以创建一个对象,然后将其放置到值栈上,或者设置为ActionContext的顶级字段。默认情况下,新对象被压到栈顶,并在结束标签处弹出。属性列表:
示例:
|
||
action |
可以从当前视图层调用其它的Action。属性列表:
|
||
actionerror |
和Action相关的错误 |
||
fielderror |
和字段相关的错误 |
标签 | 说明 | ||
iterator |
可以方便的遍历Collection、Map、Enumeration、Iterator或者数组。该标签支持在ActionContext中保存一个定义遍历状态的变量,你可以通过此变量获取当前循环状态的基本信息。属性列表:
注意:在标签内部,当前正在迭代的元素,会被放到压入ValueStack顶部。示例:
你可以访问IteratorStatus的以下属性/方法:
|
||
if-else |
此标签用于实现分支逻辑,属性列表:
示例:
|
标签 | 说明 | ||
include |
类似于JSP的 <jsp:include> 但是和Struts集成的更好,可以在当前页面包含其它Web资源的输出。属性列表:
你可以使用s:param子标签,向被包含的URL传递请求参数。示例:
|
||
url |
用于构建URL。属性列表:
你可以使用s:param子标签,向生成的URL传递请求参数 |
||
text |
在国际化应用中,该标签用于显示具体语言相关的文本。该标签从框架的资源束中查找消息。属性列表:
|
||
param |
作为特定标签的子标签,向其传递参数,属性列表:
|
使用UI组件,你可以:
- 生成HTML标记。Struts2可以根据你选择的主题,生成不同风格的HTML标记。
- 绑定HTML表单字段和Java对象属性
- 使用框架提供的类型转换机制
- 使用框架提供的验证机制
- 使用框架提供的国际化功能
UI标签对应的HTML文本可能比你想象的更复杂,例如在XHTML主题下,UI标签:
1 |
<s:textfield name="username" label="User Name" /> |
生成的HTML标记是一行表格:
1 2 3 4 |
<tr> <td class="tdLabel"><label for="username" class="label">User Name:</label></td> <td><input type="text" name="username" value="" id="username"/></td> </tr> |
如果你选用其它主题,生成的标记可能不一样。
标签API是一个高层API——它是独立于具体视图技术之上的,我们已经在上面讨论过一些种类的标签了。
不管是哪种标签,Struts都提供了对应的FreeMarker模板用来呈现它的HTML标记。不管标签是在JSP、Velocity还是FreeMarker中使用,它到HTML标记的转换过程都是一样的(基于FreeMarker模板)。如果你想自定义标签,则需要知道如何编写FreeMarker模板。
不管是哪种标签,都有几套不同的FreeMarker模板,这些模板分属于不同的主题。默认情况下,所有标签都使用xhtml主题呈现。Struts2自带的主题包括
- simple:呈现基本的HTML元素
- xhtml:使用表格布局来呈现UI元素
- css_xhtml:使用纯CSS布局来呈现UI元素
- ajax:基于xhtml主题扩展,提供丰富的Ajax组件
如果想改变默认的主题,可以创建一个struts.properties文件,在其中添加:
1 |
struts.ui.theme=css_xhtml |
你可以指定单个标签的theme属性,这样可以细粒度的控制主题。
属性 | 主题 | 说明 |
name | simple | 设置表单输入元素的name属性,如果没有手工设置value属性,则该name属性自动设置到value属性 |
value | simple | Object,指向ValueStack属性的OGNL表达式,用来预填充表单元素 |
key | simple | 从资源束取得本地化的Label,可以传播到name属性、value属性 |
label | xhtml | 为组件创建一个Label,如果设置了key和本地化文本则不需要此属性 |
labelPosition | xhtml | Label的位置,可以是left、top |
required | xhtml | Boolean,如果true,则Label旁边显示一个星号暗示这是个必输项 |
id | simple | HTML元素的id属性 |
cssClass | simple | HTML元素的class属性 |
cssStyle | simple | HTML元素的style属性 |
disabled | simple | HTML的disabled属性 |
tabindex | simple | HTML的tabidex属性 |
theme | 使用何种主题呈现此组件,例如xhtml、css_xhtml、ajax、simple | |
templateDir | 用于覆盖模板所在目录名 | |
template | 用于覆盖模板名,所有UI标签都有了默认模板,但是可以覆盖 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<!-- 格式说明 --> <s:form action="URL或者Action名字" namespace="Action的名字空间" method="POST|GET" validate="是否启用客户端JS验证"> <s:textfield maxLength="最大长度" readonly="true" size="可视长度" /> <s:password maxLength="" size="" readonly="" showPassword="是否启用密码预填充" /> <s:textarea rows="行数" cols="列数" wrap="是否换行" /> <s:checkbox fieldValue="复选框提交的真实值,可以是true/false" value="和fieldValue联用指示是否被选中"/> <s:select list="可迭代对象" listKey="元素是对象时,提交其什么属性" listValue="元素是对象时,显示其什么属性" multiple="是否可多选" size="显示选项数量" /> <s:radio list="可迭代对象" listKey="元素是对象时,提交其什么属性" listValue="元素是对象时,显示其什么属性" /> <s:checkboxlist list="可迭代对象" listKey="元素是对象时,提交其什么属性" listValue="元素是对象时,显示其什么属性" /> <s:label name="显示什么ValueStack属性" label="标签" /> <!-- 类似于DisplayField --> <s:hidden name="对应么ValueStack属性" /> </s:form> <!-- radio 示例--> <s:radio name="entity.status" list="#@java.util.LinkedHashMap@{'0':'否','1':'是'}" cssClass="weui_radio" value="1" /> |
Struts2验证框架主要包含3类组件:
即被验证的数据,通常是Action属性,或者是ModelDriven的模型数据。
指定了验证规则,Struts2支持两种指定元数据的方式:
- ActionClassName-validator.xml,指定一个类的验证规则的XML
- 基于注解的验证配置
实际负责执行验证的一系列组件。框架内建了很多验证器,你也可以自己扩展验证器。
前文我们已经简单的介绍过验证框架,并做了一个简单的例子。
validation拦截器(AnnotationValidationInterceptor)是验证框架的入口点,它的工作流程(主要是父类ValidationInterceptor完成)是:
- validation获得域数据关联的验证元数据
- 根据元数据,找到一系列的验证器,执行验证
- 如果验证失败,则调用ValidationAware.addFieldError等方法,添加错误消息
validation拦截器必须配置在workflow前面,验证框架才能正常工作。validation处理完成后,workflow拦截器继续推进请求处理流程:
- workflow被触发时,首先检查动作是否实现了Validateable接口
- 如果是,则workflow调用validate()方法执行基本验证。如果使用验证框架,该方法一般是空的
- 当validate()返回时,workflow调用ValidationAware接口的hasErrors()方法
- 如果存在错误,workflow中止流程,返回一个input结果
- input结果一般导致页面转向表单提交时的页面
这种方式之前我们已经尝试过,可以实现基本的、编程的验证。
你可以声明一个针对Action的验证配置文件,该文件必须和Action类在同一类路径目录下,该文件必须命名为ActionClassName-validator.xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<!DOCTYPE validators PUBLIC "-//Apache Struts//XWork Validator 1.0.3//EN" "http://struts.apache.org/dtds/xwork-validator-1.0.3.dtd"> <validators> <!-- 字段验证器 --> <!-- 被验证的Action属性,属性必须具有getter/setter --> <field name="age"> <!-- 使用何种验证器 --> <field-validator type="required"> <!-- 验证失败时的消息 --> <message>年龄为必填项</message> </field-validator> <!-- 一个属性可以指定多个验证器 --> <field-validator type="int"> <param name="min">2</param> <param name="max">10</param> <!-- 验证失败消息可以从资源束中获取 --> <message key="age.outofrange"/> </field-validator> </field> <!-- 非字段验证器 --> <validator type="expression"> <param name="expression">age lt 30</param> <message>年龄必须大于30</message> </validator> </validators> |
消息从以下资源束中获取:
1 |
age.outofrange=年龄必须在${min}和${max}之间 |
有时候,一个Action类中有几个方法,这几个方法都作为Action使用,如果这些方法需要不同的验证逻辑,该怎么办呢?
很简单,只需要分别为每个Action编写XML元数据文件就可以了,这些文件需要命名为:ActionClassName-aliasName-validator.xml。其中aliasName就是Action的名字。
我们可以直接使用模型类上的验证元数据,这样更容易实现验证规则的重用。
与关联Action时完全相同,只是文件前缀改为实体类名字,并且和实体类放在一个类路径下。
配置在模型类上的XML元数据不会直接生效,你必须将其关联到一个Action,才能间接的关联到验证框架。
首先,模型类必须是Action的一个属性(可以基于ModelDriven),然后你需要在Action的XML元数据中添加:
1 2 3 4 5 |
<field name="user"> <!-- visitor 将验证逻辑转给字段对应模型类的验证XML元数据负责 --> <field-validator type="visitor"> </field-validator> </field> |
注意,当使用ModelDriven时,模型属性直接暴露为值栈的顶级属性。例如访问用户名称,你只需要声明name,而不是user.name,此时Action的验证配置要做一点修改:
1 2 3 4 |
<field-validator type="visitor"> <!-- 查找user字段(User类)的属性时,不需要user.前缀 --> <param name="appendPrefix">false</param> </field-validator> |
利用visitor验证器使用模型XML元数据时,也可以指定上下文:
1 2 3 |
<field-validator type="visitor"> <param name="context">admin</param> </field-validator> |
上面这个配置,会自动定位并使用XML元数据文件User-admin-validation.xml。
验证器可以分为:
- 字段验证器:仅仅针对一个字段/属性执行验证
- 非字段验证器:针对整个Action的验证器,某些验证规则不能简单的关联到一个字段
内置验证器中,只有expression是非字段验证器。
验证器 | 说明 |
required @RequiredFieldValidator |
验证值不为空 |
requiredstring @RequiredStringValidator |
验证值不为空,且不为空串。参数列表:
|
stringlength @StringLengthFieldValidator |
验证字符串长度在指定的范围内。参数列表:
|
int @IntRangeFieldValidator |
验证整数在指定的范围内。参数列表:
|
double @DoubleRangeFieldValidator |
验证浮点数在指定的范围内 |
date @DateRangeFieldValidator |
验证日期在指定范围内。参数列表:
|
email @EmailValidator |
验证是否合法电子邮件地址 |
url @UrlValidator |
验证是否合法URL |
fieldexpression @FieldExpressionValidator |
根据ValueStack解析OGNL表达式,如果结果为true则验证通过。参数expression |
expression @ExpressionValidator |
与fieldexpression类似,但是用在动作级别。参数expression |
regx @RegexFieldValidator |
根据正则式验证。参数列表:
|
visitor @VisitorFieldValidator |
将对象类型的属性的验证工作交给此对象的类自己的验证元数据 |
要扩展自己的验证器,可以选择继承FieldValidatorSupport或者ValidatorSupport。例如我们这里实现一个密码强度验证器:
1 2 3 4 5 6 7 8 9 10 11 |
public class PasswordIntegrityValidator extends FieldValidatorSupport { public void validate( Object object ) throws ValidationException { String fieldName = getFieldName(); String fieldValue = (String) getFieldValue( fieldName, object ); String passwd = fieldValue.trim(); if ( !matches( passwd ) ) { //如果验证失败,添加错误信息 addFieldError( fieldName, object ); } } } |
新编写的验证器,必须注册到Struts2才能生效:
1 2 3 4 5 6 7 8 |
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE validators PUBLIC "-//Apache Struts//XWork Validator Definition 1.0//EN" "http://struts.apache.org/dtds/xwork-validator-definition-1.0.dtd"> <validators> <validator name="passwordintegrity" class="cc.gmem.study.struts2.PasswordIntegrityValidator"/> </validators> |
后面的章节有示例,需要注意一些事项:
- 可以配置validation拦截器的validateAnnotatedMethodOnly参数,仅仅去验证注解了Validations的Acion方法
- 可以为Action方法配置@SkipValidation,禁止对此方法进行验证
熟悉了基于XML的配置方式后,要转到注解方式很简单,仅仅是形式不同而已。本章主要用示例来说明如何进行注解配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// 从哪个父包扩展 @ParentPackage( "struts-default" ) @Namespace( "/crud" ) // 全局结果 @Results( { @Result( name = "error", location = "/error.jsp" ) } ) // 全局异常映射 @ExceptionMappings( { @ExceptionMapping( exception = "java.lang.RuntimeException", result = "error" ) } ) public class CrudAction extends ActionSupport { // getter/setter略 private String entity; private int id; @Action( value = "/update/{entity}/{id}", results = { @Result( name = SUCCESS, location = "/WEB-INF/jsp/update-${entity}.jsp" ) } ) // 匹配:/crud/update/org/10000.action 转向 /WEB-INF/jsp/update-org.jsp public String update() throws Exception { return SUCCESS; } // 多个Action映射到一个方法 @Actions( { @Action( "/name2" ), @Action( "/name2" ) } ) public String fail() { throw new RuntimeException(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
public class UserAction extends ActionSupport { private String username; private String password; // 非字段验证 @ExpressionValidator( expression = "username != password", message = "" ) public String execute() { return SUCCESS; } // 为某个Action方法声明单独的验证规则,如果设置validateAnnotatedMethodOnly = true,则只有标注了@Validations // 注解的方法才会被验证 @Validations( visitorFields = @VisitorFieldValidator(fieldName = "entity", context = "admin", appendPrefix = true ) ) // 这个注解刚好想法,如果不设置validateAnnotatedMethodOnly=true(默认不设置),则可以禁止某些方法的验证 @SkipValidation // 你可以指定在验证失败时返回的结果,不一定非要input @InputConfig(resultName = "errinfo") public String execute(){ return SUCCESS; } public String home() throws Exception { return SUCCESS; } // 字段验证,必须编写在Getter上 @StringLengthFieldValidator( maxLength = "100" ) @RequiredStringValidator( message = "" ) public String getUsername() { return username; } public void setUsername( String username ) { this.username = username; } } |
通过此工具类声明的静态方法,你可以获得当前HttpServletRequest、HttpServletResponse 、ServletContext、PageContext等对象的引用:
1 2 3 4 5 6 7 8 9 |
public class UserAction { private HttpServletRequest request; private HttpServletResponse response; public String home() { request = ServletActionContext.getRequest(); response = ServletActionContext.getResponse(); return SUCCESS; } } |
调用此类的静态方法 getContext() ,可以得到当前的Action上下文,可以进一步得到值栈。
整合Spring时,如果基于JDK动态代理实现基于AOP的事务控制,并且在Action类上添加事务注解,会报错:
java.lang.NoSuchMethodException: com.sun.proxy.$Proxy25.add()
解决办法:永远不要在控制器层加事务控制。
让你的Action类继承ActionSupport或者实现ValidationAware,这样接口中暴露的getter就可以通过标签库直接访问了:
1 |
<s:property value="fieldErrors"/> |