Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

Struts2学习笔记

21
Feb
2012

Struts2学习笔记

By Alex
/ in Java
/ tags Struts, 学习笔记
0 Comments
简介

Struts2是一个流行的基于Java的MVC框架,它基于WebWork,因而最初被称为WebWork2。它具有以下特性:

  1. 基于POJO的表单和Action。Struts1的ActionForm已经被废弃,Action现在也没有任何接口的限定。任何一个Java类都可以作为Action使用
  2. 改进的表单标签、新标签
  3. 集成的Ajax支持
  4. 易于和其它框架整合,例如Spring
  5. 支持模板技术,利用模板生成视图。这些模板技术包括Velocity、FreeMarker等
  6. 插件支持
  7. 需要更少的配置
Struts2架构

类图和处理流程参考:MVC模式

Struts2由以下核心组件:Action、ActionContext、拦截器、值栈(ValueStack)、OGNL、结果/结果类型、视图构成:

struts_2_architecture

关于Struts2的架构,需要注意:

  1. 与经典的MVC框架不同,Struts2的Action充当了Model角色,而不是控制器角色。Action有两大职责:

    1. 在一个方法中封装请求处理逻辑
    2. 作为数据传递的容器
    3. 结果路由,Action的返回值负责选择一个结果
  2. 控制器角色,则由一个Servlet过滤器:FilterDispatcher但当(此类已废弃,目前由StrutsPrepareAndExecuteFilter代替)

  3. 结果映射到一个视图
  4. Action执行时的上下文,存放在基于ThreadLocal的ActionContext中。此上下文包含了ValueStack、请求、会话等对象
  5. ValueStack是保存所有请求相关数据的存储区域,得益于ThreadLocal技术,这些数据在请求处理的全过程中都可以被访问
  6. OGNL是一种表达式语言,可以用于操控值栈中的数据
请求处理生命周期
  1. 为了请求一些资源(即页面),用户发送请求到服务器
  2. 调用拦截器进行前置处理,实现验证,文件上传等功能
  3. FilterDispatcher 查看请求,然后确定哪个Action处理它,然后调用Action
  4. 拦截器进行后置处理
  5. 处理视图,返回给浏览器
与Spring MVC的比较

Spring MVC和Struts是Java Web领域最流行的两个MVC框架,它们的可扩展性都足够好,能够满足绝大部分应用场景的需要。它们的比较如下:

  1. Spring MVC严格的分离控制器、JavaBean模型、视图等组件。而Struts2不是
  2. Spirng MVC是Spring框架的一部分,可以和Spring其它组件无缝继承
  3. Spring MVC更容易启用RESTful的Web服务
  4. Spring MVC对JSON、XML等Ajax常用请求/响应格式的支持非常好,能根据请求头自动决定使用JSON还是XML。Struts需要通过扩展结果类型来手工实现XML/JSON的支持;要实现对JSON、XML请求体的支持,必须扩展拦截器
  5. Spring MVC拦截器支持明确区分的preHandle、postHandle、afterCompletion三阶段,很容易在Handler(相当于Action)执行完毕之后,改变处理流程。然而Struts2不允许在postProcessing阶段改变处理流程,这导致定制404错误页面这样的功能难以基于拦截器实现
  6. Struts的拦截机制可以针对不同包灵活的定制
  7. Struts内置了Ajax支持
  8. 数据传递方式不同:Spring MVC采用方法参数注入;Struts采用JavaBean属性注入。前者的优势是,数据所属的业务非常明确,后者则会把多个业务的数据混在一起,大家都是属性——除非你编写很多细粒度的Action
  9. Spring的@Controller是单例的,而Action实例针对每次请求创建
整合Spring示例

本章以一个用户管理系统为例,阐述如何搭建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
配置文件
Maven依赖
pom.xml
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>
Web.xml配置
web.xml
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
<?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>
Spring配置 
applicationContext.xml
XML
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默认配置文件为struts.xml,你可以在此文件中引入其它配置文件。很多配置信息可以用注解代替。

struts.xml
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>
源代码
Action
cc.gmem.study.ssm.action.UserAction
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
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
    }
}
Entity
cc.gmem.study.ssm.entity.User
Java
1
2
3
4
5
6
public class User {
    private int id;
    private int age;
    private String name;
    // getter/setter/constructor略
}
Service 
cc.gmem.study.ssm.service.UserService
Java
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;
    }
}
View 
/src/main/webapp/WEB-INF/jsp/Users.jsp
JSP
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>
让Spring管理Action

即使你启用了struts2-spring-plugin插件,默认情况下也仅仅是支持从Application Context获得注入,Action的生命周期仍然是由Struts2自己管理的。

要让Spring管理这些Action,你必须:

  1. 在Spring中扫描Action类
  2. 如果使用基于XML的Struts配置,则需要把action元素的class字段改为Spring Bean的ID
多例Action

一般情况下,我们为每个请求创建一个Action,然而Spring Bean默认是单例的。因此在集成Spring时需要注意配置:

XML
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

注意两个概念的不同:

  1. 根命名空间:即"/",显式定义,与其它命名空间一样,必须严格匹配URL前缀,才会调用其内的Action
  2. 默认命名空间:即"",不需要定义。不被包含在package声明内部的组件,都定义在默认命名空间
extends

该包继承自的包的名称,可以用逗号分隔,指定多个

当前包会获得所有父包定义的成员,还可以覆盖之

abstract 如果为true,该包仅仅定义可继承组件,不能定义Action
struts-default包

该包在struts-defaults.xml中定义,你可以获得很多组件——例如一组拦截器栈(interceptor-stack)、一组常用的结果类型(result-type):

struts-defaults.xml
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
<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框架很多核心功能都是通过此拦截器栈完成的。

Action

该组件是Struts2框架的核心,也是日常使用最多的组件。前面我们提到到过Action的三大职责:

  1. 封装逻辑:Action作为匹配URL的入口点,包含对业务逻辑的调用
  2. 数据容器:Action类的JavaBean属性自动作为ValueStack的直接Key,你需要的任何数据都可以作为Action属性来声明
  3. 结果路由:Action返回结果,指定了使用什么视图

注意与Struts1不同,Struts2的Action不是单例的,每个请求都被赋予一个新的Action实例。

任何POJO的方法都可以作为Action,作为Action的方法必须0-arg且返回Result或字符串。可选的,你可以实现Action接口,从而获得一些常量。

ActionSupport类

该类实现了Action和其它几个有用的接口。提供了:数据验证、错误消息本地化等功能。一般你可以选择从此类继承,编写新的Action。本节介绍此类提供的功能。

基本验证

虽然Struts2提供了丰富、高度可配置的验证框架,但是ActionSupport可以让你快速的完成表单的基本验证。ActionSupport基于来自默认拦截器栈的workflow(DefaultWorkflowInterceptor)和两个接口(Validateable、ValidationAware)实现验证功能。

要实现验证,你可以覆盖Validateable的唯一方法:

Java
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接口,通过这两个接口可以实现消息本地化:

  1. TextProvider提供了读取资源束的功能,你可以调用其getText()方法获得消息文本
  2. LocaleProvider只提供了一个获取当前Locale的方法
URL映射

前面讨论包的时候我们提到过URL如何映射到Action:

  1. 由名字空间前缀 + Action名字 + Action后缀,确定一个Action对应的URL
  2. 对于默认名字空间中的Action,可以不考虑名字空间前缀进行匹配

除了这种简单的映射外,Struts2还支持通配符映射以及类似于Spring MVC的路径变量,这些方式都承担了一些数据绑定的职责。 

通配符映射

Struts还支持所谓通配符映射,即在声明包的namespace、Action的name时,指定通配符:

  1. * ,通配0-N个字符,但是不能跨越斜杠/
  2. ** ,通配0-N个字符,且可以跨越斜杠/

所有通配符匹配的文本(捕获),被从1开始编号。然后你可以在Action配置文件中使用 {N} 的方式引用它:

XML
1
2
3
<action name="/edit*" class="cc.gmem.study.ssm.Edit{1}Action">
    <result>{1}.jsp</result>
</action>

如果多个通配符映射匹配了一个URL,那么,在配置文件最后面声明的那个映射被启用。 

你可以在很多地方使用通配符捕获:

XML
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名字中的斜杠

默认的情况下,Action名字中是不允许出现斜杠的,你可以解除此限制:

struts.xml
XML
1
2
<constant name="struts.enable.SlashesInActionNames" value="true"/>
<constant name="struts.mapper.alwaysSelectFullNamespace" value="false"/>

然后,就可以这样使用通配符映射了:

XML
1
2
3
<action name="/person/*" class="cc.gmem.study.ssm.EditAction">
    <param name="id">{1}</param>
</action>  
路径变量
名字空间路径变量

启用此功能:

XML
1
<constant name="struts.patternMatcher" value="namedVariable"/>

把名字空间中的一段捕获为Action属性的例子:

XML
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>
Action名中的路径变量

从2.1.9+开始,可以启用此功能:

XHTML
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属性的例子:

XML
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>

Java
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)的逻辑:

  1. 在Action被调用之前提供Preprocessing逻辑
  2. 在Action被调用之后提供Postprocessing逻辑
  3. 捕获异常并处理

Action被调用时,其关联的拦截器栈会首先被调用。拦截器栈实际上是一系列拦截器构成的链条,之所以称为栈(也必须是栈,这取决于方法调用本身的特点),是因为最先Preprocessing的那个拦截器,最后获得Postprocessing的机会。拦截器由下面的接口定义:

Java
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尽可能简洁。

拦截器工作原理
ActionInvocation

当框架接收到一个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的是默认拦截器栈成员。

计时器(timer)

记录请求处理消耗的时间,它在栈中的位置觉得了它记录的是哪些代码消耗的时间。

日志记录(logger)

简单的日志记录,记录预处理的进入、后处理的退出。

请求参数绑定(params,D)

将请求参数绑定到ValueStack公开的属性上。拦截器并不知道数据最终被送到哪里,它只是将其绑定到ValueStack第一个匹配的属性上去。

那么ValueStack中的属性是哪里来的呢?前面提过,Action对象在请求处理开始时即被放到ValueStack上。而对于ModelDriven暴露的模型,则由modelDriven拦截器负责放到ValueStack上。

静态参数绑定(static-params,D)

与请求参数绑定类似,不同的是参数的来源。可以绑定Action声明的静态参数:

XML
1
2
3
<action>
    <param name="dob">1986-09-12</param>
</action>

注意:在默认拦截器栈中,此拦截器先于params拦截器触发,因此请求参数可以覆盖静态参数。

自动注入拦截器(autowiring)

 让明明不是Spring Bean的Action,能够被Spring自动注入属性。

servlet-config(D)

用于把Servlet API中的各种对象注入到Action字段中。你可以让Action实现不同的接口以获得相应的注入:

接口 注入
ServletContextAware ServletContext
ServletRequestAware HttpServletRequest
ServletResponseAware HttpServletResponse
ParameterAware Map类型的请求参数集
RequestAware Map类型的请求属性集
ApplicationAware Map类型的ServletContext属性
PrincipalAware 安全相关的Principal对象
文件上传(fileupload)

使用此拦截器时,你只需要设置好表单的enctype以及Action属性即可。上传单个文件的例子:

upload.html
JSP
1
2
3
<s:form action="upload" method="POST" enctype="multipart/form-data">
    <s:file name="pic"/>
</s:form>

UploadAction
Java
1
2
3
4
5
public class UploadAction {
    File pic;
    String picContentType;
    String picFileName;
}

如果要上传多个文件,则需要把上面三个属性都改为数组。 

工作流(workflow,D)

该拦截器与Action配合,提供数据验证机制,并在验证失败后改变后续工作流。workflow通过判断ValidationAware.hasErrors()的返回值,确认验证是否失败。

与Action一样,拦截器也可以通过参数来调整行为。workflow提供以下参数:

参数 说明
inputResultName 验证失败时,使用的结果的名称,默认Action.INPUT
excludeMethods 逗号分隔的方法名,哪些Action入口方法不执行该拦截器
验证(validataion,D)

前面我们介绍过,ActionSupport基于Validateable接口,支持编程式的验证机制。Struts也支持声明式验证,而validataion过滤器则是声明式验证的核心。

注意:workflow并不关心你使用编程式、还是声明式的验证。它只负责去调用ValidationAware接口。因此,为了让validataion生效,必须将其放在workflow的前面。

准备(prepare,D)

用于把预处理代码转移到Action中,让后者决定做什么。该拦截器检查Action是否实现了Preparable接口,如果是则:

  1. 调用ActionClass.prepareActionName、ActionClass.prepareDoActionName根据firstCallPrepareDo参数决定调用顺序
  2. 如果alwaysInvokePrepare,调用ActionClass.prepare()方法
模型驱动(modelDriven,D)

该拦截器调用getModel()方法,把模型对象放到ValueStack上接收请求参数。如果没有该拦截器,参数会被params拦截器直接绑定到Action对象上。

异常(exception,D)

该拦截器是程序中丰富异常处理的基础。该拦截器位于默认拦截器栈的第一位,它也应该在所有栈的第一位,这样才能确保它有机会拦截所有异常。

根据异常类型,该拦截器会转向不同的错误页面。例如下面的配置:

XML
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)

该拦截器和token-session拦截器可以用于防止表单重复提交。该拦截器检查请求中传入的令牌,如果同一令牌第二次出现,说明这是重复的表单提交。

scoped-modelDriven

该拦截器拓展了modelDriven的功能,允许模型跨请求存在(例如存放在Session中),可以实现向导式的业务。

配置拦截器栈
声明拦截器/栈

拦截器/栈的声明必须位于package内部:

XML
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

下面的声明,让Action使用具有两个拦截器的栈:

XML
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大部分功能都位于默认栈中,因此我们一般可以追加引用默认栈:

XML
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子元素即可。那么,如何覆盖默认拦截器栈中的某个拦截器的某个参数呢。也不复杂: 

XML
1
2
3
4
<interceptor-ref name="defaultStack">
    <!-- 使用拦截器名.参数名指定name -->
    <param name="workflow.excludeMethods">hello</param>
</interceptor-ref>
值栈与Action上下文
ValueStack

值栈是Struts2中的一个数据结构,它由一系列对象构成。值栈对外表现为一个虚拟对象:一系列对象属性的聚合。虚拟对象的属性,就是栈上那些对象的属性,如果值栈中有两个对象具有name属性,那么位置更高(更加接近栈顶)的那个对象的属性,作为虚拟对象的属性。

值栈代表当前请求的数据模型(领域数据),是所有OGNL表达式求值时的默认上下文对象。

ActionContext

与请求处理相关的数据,不仅仅包含存放在值栈中的领域数据,一些更基础的、业务无关的数据也必须存储起来。所有这些数据,连同ValueStack,都被存放在ActionContext中。

尽管OSGL表达式默认的解析上下文是ValueStack,你可以通过指定ActionContext属性的名字,让OSGL针对其它对象求值。可用的ActionContext属性包括:

属性 说明
ValueStack 默认的求值上下文,值栈
parameters 当前请求中请求参数构成的Map
application

当前应用作用域属性的Map

session 当前会话作用域属性的Map
request 当前请求作用域属性的Map
attr 按照页面、请求、会话、应用顺序,返回第一个出现的属性
获得当前Action上下文

你可以随时使用下面的API得到当前的Action上下文并操控它:

Java
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

OGNL(Object-Graph Navigation Language,对象图导航语言)是Struts2默认的表达式语言。它还帮助实现数据绑定、类型转换。在Struts中它是基于字符串的HTTP输入/输出与Java内部对象之间的粘合剂:

ognl

在数据进入值栈的阶段,OGNL解析请求中的点号导航,映射到值栈中的属性,根据此属性的类型,找到框架提供的类型转换器,转换值并设置到值栈上。

在数据进入视图的阶段,OGNL解析标签中的点号导航,根据类型转换器,把值栈属性值转换为字符串格式。

混用EL

你可以在基于Struts2标签库的JSP中使用EL表达式。但是要注意:

  1. EL表达式是将pageScope、requestScope、sessionScope、applicationScope作为上下文来解析表达式的,此外它还可以访问param、paramValues、header、headerValues、cookie、pageContext等对象
  2. 在Struts2中EL表达式不是XSS安全的,你必须这样: ${fn:escapeXml(name)} 才能保证安全
转义

在Struts标签中使用OGNL表达式时,如果标签属性不是String类型,则不必对OGNL表达式进行任何转义。如果标签的属性是String类型,需要用 %{ognlExpr} 格式转义:

JSP
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风格的,这里用若干示例来阐述:

属性导航
Java
1
2
3
4
5
6
7
// 简单的属性导航
alex.address.tel
// 集合导航
list[0]
array[0]
list[0].address
users['alex'].addresses[3].city
属性设置
Java
1
2
3
// 可以直接赋值给属性导航表达式 
alex.address.tel = '13309029039'
list[0] = 100
访问集合
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
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()}
直接量 
Java
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
方法调用 
Java
1
2
3
4
5
6
// 0参方法调用
user.getAge()
// 1参方法调用。注意:括号(ensureLoaded(), name) 内的是逗号表达式,最后一个作为表达式的值
method( (ensureLoaded(), name) )
// 2参方法调用
method( ensureLoaded(), name )
静态成员访问
Java
1
2
3
// 静态成员访问: @fullclassname@property_or_methodcall
@cc.gmem.study.ssm.User@DEFAULT_AGE
@cc.gmem.study.ssm.User@load(1)
访问当前上下文
Java
1
2
3
4
5
// 当前上下文对象,在Struts中通常是值栈
#this.getClass().getName()
 
// 在链式子表达式中访问当前上下文,这里上下文是一个数字
listeners.size().(#this > 100? 2*#this : 20+#this)
改变上下文

OGNL表达式解析时,默认使用的上下文对象是ValueStack,此时栈顶对象的属性会被优先读取。使用此默认上下文时,不需要任何特殊语法,例如你要访问值栈的alex.name属性,只需要用表达式 alex.name 。

要改变OSGL的解析上下文,必须使用井号前缀:

Java
1
2
3
4
5
// actionContextProperty['property'].ognlExpr
#session['alex'].name  
 
// actionContextProperty.property.ognlExpr
#session.alex.name
链式子表达式

如果你在点号后面直接使用括号,那么括号内的表达式的上下文是点号前的那个对象:

Java
1
2
// ensureLoaded()、name的上下文是parent属性
headline.parent.(ensureLoaded(), name)
Lambda
Java
1
2
3
4
// 定义一个Lambda
#fact = :[#this<=1? 1 : #this*#fact(#this-1)]
// 调用Lambda
#fact(30H)
操作符

OGNL可以使用绝大部分来自Java的操作符,但是需要注意行为的差异、操作符扩展:

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时,你可以通过几种方式完成数据绑定:

  1. 为Action声明多个简单属性,每个属性容纳一个表单字段
  2. 为Action声明一个对象属性,把整个表单绑定到此对象,表单字段的名称必须使用点号导航
  3. 实现泛型接口ModelDriven,把表单绑定到特化后的泛型参数类型的对象上

从基于文本的请求参数到各种类型的Java对象的绑定,必然牵涉到数据类型转换。Struts2自带了大量常用的类型转换器,你也可以开发自己的类型转换器。

绑定到Action属性
单个属性绑定

表单页面如下:

JSP
1
<s:textfield name="name" label="User Name" />

Action如下: 

Java
1
2
3
4
public class UserAction extends ActionSupport {
    private String name;
    // getter/setter略
}

结果页面如下:

JSP
1
User Name: <s:property value="name"/>

结合数据收集、处理、显示这三个步骤,我们可以看到单个属性的绑定非常简单。就是根据表单字段名和Action属性名的相同性进行绑定。

对象属性绑定

表单页面如下:

JSP
1
<s:textfield name="user.name" label="User Name" />

Action如下: 

Java
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");
        }
    }
}

结果页面如下:

JSP
1
User Name: <s:property value="user.name"/>

还是差不多,区别只是用点号导航来定位属性,而Action属性和点号导航根对象名字相同。 

ModelDriven

这是一个泛型接口,只有一个方法getModel(),即返回当前业务使用的模型对象。你的Action必须实现此方法:

Java
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类型的转换支持:

  1. 转换为String:直接使用原始值
  2. 转换为Boolean/boolean:true和false字符串分别转换为对应的布尔值
  3. 转换为Character/char:直接使用原始值
  4. 各种数字类型及其包装类:直接使用原始值
  5. 转换为Date:使用当前Locale的SHORT格式来解析原始值
  6. 转换为数组:每个原始值的元素,依次转换数组元素类型的Java对象
  7. 转换为List:默认创建元素为字符串的列表
  8. 转换为Map:默认创建键值为字符串的Map

为了使用这些转换器,你必须构建OGNL表达式。这些表达式可能是表单字段的名称,或者Struts2标签的属性。

注意:尽管OGNL内置了类型转换接口ognl.TypeConverter,但是Struts2没有直接从它扩展出上述内置类型转换器,而是使用了适配器模式。Struts2自己的转换器接口为:com.opensymphony.xwork2.conversion.TypeConverter

自定义类型转换器

OGNL/Struts2的类型转换器接口,可以针对任意类型之间的相互转换。但是在我们数据绑定的场景中,仅仅需要字符串与其它类型的转换,因此可以选择从

org.apache.struts2.util.StrutsTypeConverter
Java
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对象。

注册类型转换器

你可以通过两种方式注册类型转换器:

  1. 配置到ActionClassName-conversion.properties,则转换器仅仅用于一个Action类:
    1
    2
    # Action类的customField,使用的转换器:
    customField=package.CustomFieldTypeConverter 
  2. 配置到xwork-conversion.properties,则转换器用于全局:
    1
    2
    # CustomField类型,使用的转换器:
    package.CustomField=package.CustomFieldTypeConverter
覆盖默认日期格式

Struts默认的日期格式可能不太受欢迎,我们可以实现自己的日期类型转换器:

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
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 );
    }
}

修改全局设置:

xwork-conversion.properties
1
java.util.Date=com.ecfund.base.struts2.DateConverter
表单字段绑定到属性

你可以使用OGNL表达式作为表单字段的name,这样就可以自动绑定到ValueStack属性。

如果属性是基本类型、字符串,映射很简单,直接根据点号导航匹配,然后调用内置转换器转换就可以了。

数组和列表映射
JSP
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. 第一种方式提交1个请求参数,值为3个字符串构成的数组。而ages属性类型为int[],因此框架为每个字符串执行String-int的转换
  2. 第二种方式提交3个请求参数

转换为List时,与数组类似。如果没有使用泛型,则认为列表元素都是字符串类型的;如果使用了泛型,执行相应的类型转换。

Map映射
JSP
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:

struts-default.xml
XML
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)。实例化后的结果类型,就是结果,两者是类与实例的关系。

所有结果类都必须实现接口:

com.opensymphony.xwork2.Result
Java
1
2
3
4
5
6
public interface Result extends Serializable {
    // 继续处理Action调用过程,完成视图显示的服务器端逻辑:
    // 例如生成网页、生成电子邮件、发送JSM消息,等等
    public void execute(ActionInvocation invocation) throws Exception;
 
}
内置结果类型
dispatcher

该结果类型用于JSP、Servlet等类型的视图,dispatcher是struts-default包默认的结果类型。它的参数包括:

  1. location,默认参数,目标视图的位置
  2. parse,指定是否将location参数中的 ${expr} 作为OGNL表达式来解析

dispatcher依赖于javax.servlet.RequestDispatcher,后者能够将请求处理权从一个Servlet转向另外一个(通常此Servlet是JSP)。RequestDispatcher提供了两种方法来转移处理权:

  1. include():临时转移处理权,当前Servlet已经在输出响应内容,而需要另外一个Servlet的输出时,使用此方式
  2. forward():永久转移处理权,使用此方式时,当前Servlet不能已经输出任何响应

这两种方式都让第二个Servlet得到当前请求、响应对象,让它处理同一个用户请求。

RequestDispatcher与HTTP重定向不同,后者是向浏览器发送一个重定向响应,前者的全部处理都发生在服务器内部,浏览器不知情。

redirect

该结果用于将浏览器重定向到另外一个URL,与当前请求有关的全部信息将被丢弃。它的参数包括:

  1. location,默认参数,目标视图的位置
  2. parse,指定是否将location参数的 ${expr}作为OGNL表达式来解析

示例: <result type="redirect">http://gmem.cc/${postId}</result> 

redirectAction

与redirect类似,但是此结果类型能够识别Action的名称和名字空间,并且指定重定向时使用的请求参数:

XML
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

Velocity和FreeMarker是Struts2支持的、用于代替JSP的视图技术。与它们对应的结果类型已经内置在Struts2框架中。

使用此结果类型的Action配置示例如下:

struts.xml
XML
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

FreeMarker内置于Struts2框架中,不需要引入额外的JAR包。使用此结果类型的Action配置示例如下:

XML
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自身配置的结果,如果找不到匹配项,就会转而使用全局结果。

全局结果主要用于错误处理:

XML
1
2
3
<global-results>
    <result name="error">/error.jsp</result>
</global-results>
扩展结果类型:JSON

基于Ajax的Web应用通常使用JSON作为响应格式,这是默认的dispatcher无法满足的。我们可以自己实现一个JSON结果类型,满足Ajax应用的需要。

实现JSONResult
cc.gmem.study.struts2.JSONResult
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
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 ) );
    }
}
注册JSONResult

实现了结果类型后,还必须将其注册到Struts才能生效:

XML
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>
使用JSONResult 
XML
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元素可以简写为:

XML
1
<result name="success" type="json">user</result>

当客户端请求/usermgr/userinfo时Action的处理结果是success时,Struts会调用我们的JSONResult,并把值栈上的user属性编码为JSON文本,写到响应体中。  

标签库
Struts2标签简介

Struts2提供了大量不同类型的标签,这些标签有针对不同视图技术——JSP、Velocity、FreeMarker的语法变体。这些标签可以分为四个类别:数据标签、流程控制标签、UI标签、其它标签。

标签语法

Struts2标签API定义了一个独立于视图技术的抽象层, 该API指定了标签的公开参数和属性。针对三种视图技术的标签语法,差异并不大,并且有规律可循。

JSP语法

在JSP中使用标签库,必须声明:

JSP
1
<%@ taglib prefix="s" uri="/struts-tags" %>

标签用法示例:

JSP
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} 。

Velocity语法
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
FreeMarker语法

它的语法和JSP语法非常接近:

XHTML
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>
作为标签属性的OGNL

为Struts标签设置属性时,你需要搞清楚,目标属性期望的是一个字符串字面值还是ValueStack属性的OGNL表达式。

如果某个标签属性的类型是字符串,那么传递给它的值被作为字面值解析;否则,作为OGNL表达式解析。

如果要强制作为OGNL表达式解析,可以使用转义序 %{expr} :

JSP
1
2
3
4
<!-- 默认值是字面值: -->
<s:property value="nonExistingProperty" default="defaultValue"/>
<!-- 默认值是OGNL: -->
<s:property value="nonExistingProperty" default="%{defaultValue}"/>
数据标签
标签 说明
property

将ValueStack或者ActionContext上其它对象的属性值输出到HTML中,属性列表:

  1. value,指定被显示的值,类型Object,默认值栈顶
  2. default,值为空时的默认值
  3. escape,是否转义HTML字符,默认True
set

在某个作用域设置一个属性,属性列表:

  1. name,在指定作用域新增加的变量的名称
  2. scope,在哪里设置属性,application/session/request/page/action,默认action
  3. value,新属性的值,类型Object

示例:

JSP
1
2
3
4
<s:set name="username" value="user.name" />
 
<s:set name="username" scope="application" value="user.name" />
<s:property value="#application['username']" />
push 把一个对象压到值栈的顶端,这个对象的属性将获得最高优先级——优先代表虚拟对象的属性。示例:
JSP
1
2
3
<s:push value="alex">
    <s:property value="age" />
</s:push>

 在此标签的结束标签处,此对象从值栈弹出

bean

可以创建一个对象,然后将其放置到值栈上,或者设置为ActionContext的顶级字段。默认情况下,新对象被压到栈顶,并在结束标签处弹出。属性列表:

  1. name,新对象的全限定类名
  2. var,在结束标签之后,仍然能够通过var访问此对象,var为ActionContext的顶级字段

示例:

JSP
1
2
3
4
5
6
7
<s:bean name="org.apache.struts2.util.Counter" var="counter">
    <s:param name="last" value="10" />
</s:bean>
 
<s:iterator value="#counter">
    <s:property />
</s:iterator> 
action

可以从当前视图层调用其它的Action。属性列表:

  1. name,必须,目标Action名称
  2. namespace,目标Action的名字空间
  3. var,页面后续代码使用Action对象时,用此变量引用
  4. executeResult,是否排除动作的结果,默认false——包含Action结果到当前页面
  5. flush,是否在结束标签处flush缓冲
  6. ignoreContextParams,在执行Action时是否不包含请求参互诉,默认false
actionerror

和Action相关的错误

fielderror

和字段相关的错误

控制标签 
标签 说明
iterator

可以方便的遍历Collection、Map、Enumeration、Iterator或者数组。该标签支持在ActionContext中保存一个定义遍历状态的变量,你可以通过此变量获取当前循环状态的基本信息。属性列表:

  1. value,类型Object,被遍历的对象
  2. status,如果指定,那么IteratorStatus以此字段指定的名字,存放在ActionContext对象中
  3. var,如果指定,则当前遍历对象在栈上依据此变量存储

注意:在标签内部,当前正在迭代的元素,会被放到压入ValueStack顶部。示例:

JSP
1
2
3
4
5
6
7
8
9
10
<s:iterator value="users" var="user">    
    <tr>
        <!-- 注意status是放在ActionContext上的,因此需要前缀# -->
        <td><s:property value="#status.count"/></td>
        <td><s:property value="#user.id"/></td>
        <!-- #user.name和 name效果通常一样,除非User没有name属性(注意:不是说name为null),才会向栈底部追溯-->
        <td><s:property value="name"/></td>
        <td><s:property value="age"/></td>
    </tr>
</s:iterator>

你可以访问IteratorStatus的以下属性/方法:

  1. count,当前元素的计数,从1开始
  2. index,当前元素的索引,从0开始
  3. even,是否第偶数个元素
  4. odd,是否第奇数个元素
  5. modulus(int operand),执行取模操作
if-else

此标签用于实现分支逻辑,属性列表:

  1. test,布尔型的测试条件

示例:

JSP
1
2
3
4
5
6
7
8
9
<s:if test="user.age == 30">
    ${user.name} is 30 years old
</s:if>
<s:elseif test="user.age >30">
    ${user.name} is older then 30
</s:elseif>
<s:else>
    ${user.name} is younger than 30
</s:else>
其它标签
标签 说明
include

类似于JSP的 <jsp:include> 但是和Struts集成的更好,可以在当前页面包含其它Web资源的输出。属性列表:

  1. value,字符串,页面、Action、Servlet以及其它可以被引用的URL

你可以使用s:param子标签,向被包含的URL传递请求参数。示例:

JSP
1
2
3
<s:include value="%{includeURL}">
    <s:param name="username" value="alex" />
</s:include>
url

用于构建URL。属性列表:

  1. value,字符串,基础URL,默认为当前页面的URL
  2. action,字符串,生成的URL指向的Action名称,不需要.action后缀
  3. var,如果指定此参数,则把构建的URL存放在ActionContext中备用
  4. includeParams,包含哪些请求参数,可选值all、get、none
  5. includeContext,默认true,是否把当前Servlet上下文路径作为URL前缀
  6. encode,如果客户端不支持Cookie,是否把Session ID追加在生成的URL后面
  7. scheme,指定使用的HTTP协议

你可以使用s:param子标签,向生成的URL传递请求参数

text

在国际化应用中,该标签用于显示具体语言相关的文本。该标签从框架的资源束中查找消息。属性列表:

  1. name,到资源束中查找的关键字
  2. var,如果找到,具体语言相关的文本会存放在此属性指定的ActionContext变量中
param

作为特定标签的子标签,向其传递参数,属性列表:

  1. name,String,参数名
  2. value,Object,参数值
UI标签

使用UI组件,你可以:

  1. 生成HTML标记。Struts2可以根据你选择的主题,生成不同风格的HTML标记。
  2. 绑定HTML表单字段和Java对象属性
  3. 使用框架提供的类型转换机制
  4. 使用框架提供的验证机制
  5. 使用框架提供的国际化功能 

UI标签对应的HTML文本可能比你想象的更复杂,例如在XHTML主题下,UI标签:

JSP
1
<s:textfield name="username" label="User Name" />

生成的HTML标记是一行表格:

XHTML
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自带的主题包括

  1. simple:呈现基本的HTML元素
  2. xhtml:使用表格布局来呈现UI元素
  3. css_xhtml:使用纯CSS布局来呈现UI元素
  4. ajax:基于xhtml主题扩展,提供丰富的Ajax组件

如果想改变默认的主题,可以创建一个struts.properties文件,在其中添加:

struts.properties
1
struts.ui.theme=css_xhtml

你可以指定单个标签的theme属性,这样可以细粒度的控制主题。 

UI标签通用属性
属性 主题 说明
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标签都有了默认模板,但是可以覆盖
UI标签示例
JSP
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类组件:

validation-architecture

域数据

即被验证的数据,通常是Action属性,或者是ModelDriven的模型数据。

验证元数据

指定了验证规则,Struts2支持两种指定元数据的方式:

  1. ActionClassName-validator.xml,指定一个类的验证规则的XML
  2. 基于注解的验证配置 
验证器

实际负责执行验证的一系列组件。框架内建了很多验证器,你也可以自己扩展验证器。

工作流程

前文我们已经简单的介绍过验证框架,并做了一个简单的例子。

validation拦截器(AnnotationValidationInterceptor)是验证框架的入口点,它的工作流程(主要是父类ValidationInterceptor完成)是:

  1. validation获得域数据关联的验证元数据
  2. 根据元数据,找到一系列的验证器,执行验证
  3. 如果验证失败,则调用ValidationAware.addFieldError等方法,添加错误消息

validation拦截器必须配置在workflow前面,验证框架才能正常工作。validation处理完成后,workflow拦截器继续推进请求处理流程:

  1. workflow被触发时,首先检查动作是否实现了Validateable接口
  2. 如果是,则workflow调用validate()方法执行基本验证。如果使用验证框架,该方法一般是空的
  3. 当validate()返回时,workflow调用ValidationAware接口的hasErrors()方法
  4. 如果存在错误,workflow中止流程,返回一个input结果
  5. input结果一般导致页面转向表单提交时的页面
关联Action到验证框架
实现Validateable接口

这种方式之前我们已经尝试过,可以实现基本的、编程的验证。

声明XML元数据

你可以声明一个针对Action的验证配置文件,该文件必须和Action类在同一类路径目录下,该文件必须命名为ActionClassName-validator.xml:

/cc/gmem/study/ssm/action/UserAction-validations.xml
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>

 消息从以下资源束中获取:

/cc/gmem/study/ssm/action/UserAction.properties
1
age.outofrange=年龄必须在${min}和${max}之间
验证上下文

有时候,一个Action类中有几个方法,这几个方法都作为Action使用,如果这些方法需要不同的验证逻辑,该怎么办呢?

很简单,只需要分别为每个Action编写XML元数据文件就可以了,这些文件需要命名为:ActionClassName-aliasName-validator.xml。其中aliasName就是Action的名字。

关联模型类到验证框架

我们可以直接使用模型类上的验证元数据,这样更容易实现验证规则的重用。

声明XML元数据

与关联Action时完全相同,只是文件前缀改为实体类名字,并且和实体类放在一个类路径下。

关联到Action

配置在模型类上的XML元数据不会直接生效,你必须将其关联到一个Action,才能间接的关联到验证框架。

首先,模型类必须是Action的一个属性(可以基于ModelDriven),然后你需要在Action的XML元数据中添加:

XML
1
2
3
4
5
<field name="user">
   <!-- visitor 将验证逻辑转给字段对应模型类的验证XML元数据负责 -->
    <field-validator type="visitor">
    </field-validator>
</field>

注意,当使用ModelDriven时,模型属性直接暴露为值栈的顶级属性。例如访问用户名称,你只需要声明name,而不是user.name,此时Action的验证配置要做一点修改: 

XML
1
2
3
4
<field-validator type="visitor">
    <!-- 查找user字段(User类)的属性时,不需要user.前缀 -->
    <param name="appendPrefix">false</param>
</field-validator> 
验证上下文

利用visitor验证器使用模型XML元数据时,也可以指定上下文:

XML
1
2
3
<field-validator type="visitor">
    <param name="context">admin</param>
</field-validator>

上面这个配置,会自动定位并使用XML元数据文件User-admin-validation.xml。 

验证器分类

验证器可以分为:

  1. 字段验证器:仅仅针对一个字段/属性执行验证
  2. 非字段验证器:针对整个Action的验证器,某些验证规则不能简单的关联到一个字段

内置验证器中,只有expression是非字段验证器。

内建验证器
验证器 说明
required
@RequiredFieldValidator
验证值不为空
requiredstring
@RequiredStringValidator

验证值不为空,且不为空串。参数列表:

  1. trim,是否清楚前后空格,默认true
stringlength
@StringLengthFieldValidator

验证字符串长度在指定的范围内。参数列表:

  1. trim,是否清楚前后空格,默认true
  2. minLength,最小长度
  3. maxLength,最大长度
int
@IntRangeFieldValidator

验证整数在指定的范围内。参数列表:

  1. min,最小值
  2. max,最大值
double
@DoubleRangeFieldValidator

验证浮点数在指定的范围内

date
@DateRangeFieldValidator

验证日期在指定范围内。参数列表:

  1. min,最小日期,格式MM/DD/YYYY
  2. max,最大日期,格式MM/DD/YYYY
email
@EmailValidator
验证是否合法电子邮件地址
url
@UrlValidator
验证是否合法URL
fieldexpression
@FieldExpressionValidator
根据ValueStack解析OGNL表达式,如果结果为true则验证通过。参数expression
expression
@ExpressionValidator
与fieldexpression类似,但是用在动作级别。参数expression
regx
@RegexFieldValidator

根据正则式验证。参数列表:

  1. expression,正则式
  2. caseSensitive,是否大小写敏感
  3. trim,是否去除前后空格
visitor
@VisitorFieldValidator
将对象类型的属性的验证工作交给此对象的类自己的验证元数据
自定义验证器

要扩展自己的验证器,可以选择继承FieldValidatorSupport或者ValidatorSupport。例如我们这里实现一个密码强度验证器:

Java
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才能生效:

validators.xml
XML
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>
基于注解的验证

后面的章节有示例,需要注意一些事项:

  1. 可以配置validation拦截器的validateAnnotatedMethodOnly参数,仅仅去验证注解了Validations的Acion方法
  2. 可以为Action方法配置@SkipValidation,禁止对此方法进行验证
基于注解的配置

熟悉了基于XML的配置方式后,要转到注解方式很简单,仅仅是形式不同而已。本章主要用示例来说明如何进行注解配置。

包与Action配置
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
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();
    }
}
验证元数据
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
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;
    }
 
}
常用API
ServletActionContext

通过此工具类声明的静态方法,你可以获得当前HttpServletRequest、HttpServletResponse 、ServletContext、PageContext等对象的引用:

Java
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;
    }
}
ActionContext

调用此类的静态方法 getContext() ,可以得到当前的Action上下文,可以进一步得到值栈。

常见问题
零散问题
Action类添加@Transactional注解导致异常

整合Spring时,如果基于JDK动态代理实现基于AOP的事务控制,并且在Action类上添加事务注解,会报错:

java.lang.NoSuchMethodException: com.sun.proxy.$Proxy25.add()

解决办法:永远不要在控制器层加事务控制。

如何访问所有验证错误

让你的Action类继承ActionSupport或者实现ValidationAware,这样接口中暴露的getter就可以通过标签库直接访问了:

XML
1
<s:property value="fieldErrors"/> 
← SOA知识集锦
Mybatis学习笔记 →

针对该文章的评论功能已关闭

Related Posts

  • JDBC知识集锦
  • Maven依赖速查表
  • Aspject加载时织入示例
  • Eclipse 4.3.2开发环境搭建
  • 使用Oracle Java Mission Control监控JVM运行状态

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • Bazel学习笔记 38 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2