0x00 参考

本笔记直接参考或引自如下链接文章:

http://c.biancheng.net/struts2/

https://www.w3cschool.cn/struts_2/

0x01 Interceptor简介

Interceptor即拦截器。

在 Struts2 框架中,拦截器是其重要的组成部分,Struts2 的很多功能(数据校验、对象类型转换、文件上传等)都是构建在拦截器之上的。

通常情况下,开发者通过 Struts2 内建的拦截器可以完成大部分的操作,只有在内建拦截器不能满足需求时,才会自己扩展。可以这么说,Struts2 框架的简单易用,与拦截器的作用是分不开的。

拦截器(Interceptor)是 Struts2 框架的核心组成部分,它类似于 Servlet 中的过滤器,是一种可以在请求之前或之后执行的 Struts2 的组件,也可以将其理解为动态拦截 Action 调用的对象。

在早期的 MVC 框架中,通常会将一些通用的操作(如类型转换、数据校验、解析上传的文件等)强制写在控制器中,而这些常用操作又不是所有的请求都需要实现的,这就导致了框架的灵活性不足、可扩展性低等问题。

在 Struts2 框架中,这些通用的核心功能都放到了拦截器中实现,而不是集中放在核心控制器中实现。

由于框架中各个功能对应的拦截器是分开定义的,每个拦截器都可以完成单个功能,并且可以自由选择、灵活组合,而需要哪些拦截器时,只要在 struts.xml 配置文件中指定即可,所以 Struts2 框架的使用十分灵活。同时,由于在 Struts2 框架中支持自定义拦截器,所以其扩展性十分强大。

当多个拦截器组合在一起时就形成了拦截器链(Interceptor Chain)或拦截器栈(Interceptor Stack)。

拦截器链就是指对应各个功能的拦截器按照一定的顺序排列在一起形成的链,而拦截器链组成的集合就是拦截器栈。当有适配连接器栈的访问请求进来时,这些拦截器就会按照之前定义的顺序被调用。

在通常情况下,拦截器都是以代理方式调用的,它在一个 Action 执行前后进行拦截,围绕着 Action 和 Result 的执行而执行,其工作方式如图下所示。

从图下中可以看出,Struts2 拦截器的实现原理与 Servlet 过滤器的实现原理类似,它以链式执行,对真正要执行的方法(execute())进行拦截。

在执行 Action 的 execute() 方法之前会执行一次拦截,在 Action 和 Result 执行之后,拦截器会再次执行(与先前的调用顺序相反)。在此链式执行的过程中,每一个拦截器都可以直接返回,从而终止余下的拦截器、Action 及 Result 的执行。

0x02 Interceptor的配置和使用

拦截器

要使用拦截器,首先要对它进行配置。拦截器的配置是在 struts.xml 文件中完成的,它通常以 <interceptor> 标签开头,以 </interceptor> 标签结束。定义拦截器的语法格式如下所示:

1
2
3
<interceptor name="interceptorName" class="interceptorClass">
<param name="paramName">paramValue</param>
</interceptor>

上述语法格式中,<interceptor> 元素的 name 属性用于指定拦截器的名称,class 属性用于指定拦截器的实现类。有时,在定义拦截器时需要传入参数,这时需要使用 <param> 标签,其中 name 属性用于指定参数的名称,paramValue 表示参数的值。

拦截器栈

在实际的项目开发中,经常需要在 Action 执行之前执行多个拦截动作,如登录日志记录、权限管理等。

为了方便代码管理和程序的执行,开发者通常会将这些拦截器组成一个拦截器栈,在使用时,可以将栈内的多个拦截器当成一个整体引用。当拦截器栈被附加到一个 Action 上时,在执行 Action 之前必须先执行拦截器栈中的每一个拦截器。

定义拦截器栈使用 <interceptors> 元素和 <interceptor-stack> 子元素,当配置多个拦截器时,需要使用 <interceptor-ref> 元素指定多个拦截器,配置语法如下所示:

1
2
3
4
5
6
<interceptors>
<interceptor-stack name="interceptorStackName">
<interceptor-ref name="interceptorName"/>
...
</interceptor-stack>
</interceptors>

在上述语法中,interceptorStackName 值表示配置的拦截器栈的名称;interceptorName 值表示拦截器的名称。除此之外,在一个拦截器栈中还可以包含另一个拦截器栈,示例代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
<package name="default" namespace="/" extends="struts-default">
<!--声明拦截器-->
<interceptors>
<interceptor name="interceptor1" class="interceptorClass"/>
<interceptor name="interceptor2" class="interceptorClass"/>
<!--定义一个拦截器栈myStack,该拦截器栈中包含两个拦截器和一个拦截器栈-->
<interceptor-stack name="myStack">
<interceptor-ref name="defaultStack"/>
<interceptor-ref name="interceptor1"/>
<interceptor-ref name="interceptor2"/>
</interceptor-stack>
</interceptors>
</package>

在上述代码中,定义的拦截器栈的名称是 myStack,在 myStack 栈中,除了引用了两个自定义的拦截器 interceptor1 和 interceptor2 以外,还引用了一个内置拦截器栈 defaultStack,这个拦截器是必须要引入的。

默认拦截器

如果想对一个包下的 Action 使用相同的拦截器,则需要为该包中的每个 Action 都重复指定同一个拦截器,这样写显然过于繁琐。为了解决此问题,Struts2 中支持使用默认拦截器,它可以对其指定的包中的所有 Action 都起到拦截作用。

一旦为某一个包指定了默认拦截器,并且该包中的 Action 未显示指定拦截器,则会使用默认拦截器。反之,若此包中的 Action 显示的指定了某个拦截器,则该默认拦截器将会被屏蔽。此时,如果还想使用默认拦截器,则需要用户手动配置该默认拦截器的引用。

配置默认拦截器需要使用 <default-interceptor-ref> 元素,此元素为 <package> 元素的子元素。其语法格式如下所示:

1
<default-interceptor-ref name="拦截器(栈)的名称"/>

在上述语法格式中,name 属性的值必须是已经存在的拦截器或拦截器栈的名称。下面用该语法格式配置一个默认拦截器,示例代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<package name="default" namespace="/" extends="struts-default">
<!--声明拦截器-->
<interceptors>
<interceptor name="interceptor1" class="interceptorClass"/>
<interceptor name="interceptor2" class="interceptorClass"/>
<!--定义一个拦截器栈myStack,该拦截器栈中包含两个拦截器和一个拦截器栈-->
<interceptor-stack name="myStack">
<interceptor-ref name="defaultStack"/>
<interceptor-ref name="interceptor1"/>
<interceptor-ref name="interceptor2"/>
</interceptor-stack>
</interceptors>
<!--配置包下的默认拦截器,既可以是拦截器,也可以是拦截器栈-->
<default-interceptor-ref name="myStack"/>
<action name="login" class="com.mi1k7ea.LoginAction">
<result name="input">/login.jsp</result>
</action>
</package>

在上述代码中,指定了包下面的默认拦截器为一个拦截器栈,该拦截器栈将会作用于包下所有的 Action。

注意:每一个包下只能定义一个默认拦截器,如果需要多个拦截器作为默认拦截器,则可以将这些拦截器定义为一个拦截器栈,再将这个拦截器栈作为默认拦截器即可。

0x03 内建拦截器

Struts2 框架中内置了许多拦截器,这些拦截器以 name-class 对的形式配置在 struts-default.xml 文件中,其中,name 是拦截器的名称,也就是引用的名字;class 指定了该拦截器所对应的实现。

只要自定义的包继承了 Struts2 的 struts-default 包,就可以使用包中定义的内建拦截器,否则需要自行定义拦截器。

内建拦截器的介绍

在 struts-default.xml 中,每一个拦截器都具有不同的意义,如下表:

名 称 说 明
alias 在不同请求之间将请求参数在不同名称间转换,请求内容不变
autowiring 用于实现 Action 的自动装配
chain 让前一个 Action 的属性可以被后一个 Action 访问,现在和 chain 类型的 result() 结合使用
conversionError 将错误从 ActionContext 中添加到 Action 的属性字段中
cookies 使用配置的 Name 和 Value 指定 Cookies
cookieProvider 该类是一个 Cookie 工具,方便开发者向客户端写 Cookie
clearSession 用于清除一个 HttpSession 实例
createSession 自动创建 HttpSession,用于为需要使用 HttpSession 的拦截器服务
debugging 提供不同的调试用的页面展现内部的数据状况
execAndWait 在后台执行 Action,同时将用户带到一个中间的等待页面
exception 将异常定位到一个画面
fileUpload 提供文件上传功能
il8n 记录用户选择的 locale
logger 输出 Action 的名称
model-driven 如果一个类实现了 Model Driven,将 get Model 得到的结果放在 Value Slack 中
scoped-model-driven 如果一个 Action 实现了 ScopedModelDriven,则这个拦截器会从相应的 Scope 中取 出 model 调用 Action 的 setModel 方法,将其放入 Action 内部
params 将请求中的参数设置到 Action 中
actionMappingParams 用于负责在 Action 配置中传递参数
prepare 如果 Action 实现了 Preparable,则该拦截器调用 Action 类的 prepare 方法
staticParams 将 struts.xml 文件中 标签的参数内容设置到对应的 Action 中
scope 将 Action 状态存入 session 和 application 范围
servletConfig 提供访问 HttpServletRequest 和 HttpServletResponse 方法,以 Map 方式访问
timer 输岀 Action 执行的时间
token 通过 Token 避免双击
tokenSession 和 Token Interceptor 一样,不过双击时把请求的数据存储在 Session 中
validation 使用 action-validation.xml 文件中定义的内容校验提交的数据
workflow 调用 Action 的 validate 方法,一旦有错谋返回,则重新定位到 INPUT 画面
store 存储或者访问实现 ValidalionAware 接口的 Action 类出现的消息、错误和字段错误等
checkbox 添加了 checkbox 自动处理代码,将没有选中的 checkbox 的内容设定为 false,而 html 在默认情况下不提交没有选中的 checkbox
datetime 日期拦截器
profiling 通过参数激活 profile
roles 确定用户是否具有 JAAS 指定的 Role,否则不予执行
annotationWorkflow 利用注解代替 XML 配置,使用 annotationWorkflow 拦截器可以使用注解,执行流程为 before-execute-feforeResult-after
multiselect 检测是否有像
deprecation 当日志级别设置为调试模式(debug)并且没有特殊参数时,在 devMode 模式中,会检查应用程序使用过时或未知的常量,并且显示警告

Struts2 框架除了提供这些有用的拦截器以外,还定义了一些拦截器栈,在开发 Web 应用时,可以直接引用这些拦截器栈,而无须自定义拦截器。

注意:随着 Struts2 版本的发展,内建拦截器的数量也在相应地增多,不同版本的 Struts2 拦截器的数量有一些差异,此版本的 Struts2 内建拦截器共有 35 个。这些内建拦截器读者不需要记忆,只需要了解即可。

内建拦截器的配置

在 struts-core-2.3.24.jar 包中的根目录下找到 struts-default.xml 文件,打开后找到 <interceptors> 元素下的内建拦截器和拦截器栈,其部分代码如下所示:

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
<package name="struts-default" abstract="true">
...
<interceptors>
<!--系统内建拦截器部分,上一部分介绍的内容-->
<interceptor name="alias"
class="com.opensymphony.xwork2.interceptor.AliasInterceptor"/>
<interceptor name="autowiring"
class="com.opensymphony.xwork2.spring.interceptor.ActionAutowiringInterceptor"/>
<interceptor name="chain"
class="com.opensymphony.xwork2.interceptor.ChainingInterceptor"/>
...
<!-- 定义Basic stack拦截器栈 -->
<interceptor-stack name="basicStack">
<!--引用系统定义的exception拦截器-->
<interceptor-ref name="exception"/>
...
</interceptor-stack>
...
<!-- 定义Sample model -driven stack -->
<interceptor-stack name="modelDrivenStack">
<!--引用系统定义的modelDriven拦截器-->
<interceptor-ref name="modelDriven"/>
<!--引用系统定义的basicStack拦截器栈-->
<interceptor-ref name="basicStack"/>
</interceptor-stack>
...
<!--定义defaultStack拦截器栈-->
<interceptor-stack name="defaultStack">
<interceptor-ref name="exception"/>
<interceptor-ref name="alias"/>
<interceptor-ref name="il8n"/>
...
<interceptor-ref name="validation">
<param name="excludeMethods">input,back,cancel,browse</param>
</interceptor-ref>
...
</interceptor-stack>
</interceptors>
<!--将defaulrStack拦截器栈配置为系统默认拦截器栈-->
<default-interceptor-ref name="defaultStack"/>
<!--默认action类是ActionSupport-->
<default-class-ref class="com.opensymphony.xwork2.ActionSupport" />
</package>

在上述内建拦截器的配置代码中,defaultStack 拦截器组合了多个拦截器,这些拦截器可以满足大部分 Web 应用程序的需求。使用时,只要在 struts.xml 定义包的过程中继承 struts-default 包,那么 defaultStack 拦截器栈就是默认拦截器的引用。

由于本节篇幅有限,这里没有列出所有的内建拦截器和拦截器栈,读者需要时可以自行查阅 struts-default.xml 文件。

0x04 自定义拦截器

在实际的项目开发中,虽然 Struts2 的内建拦截器可以完成大部分的拦截任务,但是,一些与系统逻辑相关的通用功能(如权限的控制和用户登录控制等),则需要通过自定义拦截器实现。

实现Interceptor接口类

在 Struts2 框架中,通常开发人员所编写的自定义拦截器类都会直接或间接地实现 com.opensymphony.xwork2.interceptor.Interceptor 接口。Interceptor 接口中的主要代码如下所示:

1
2
3
4
5
public interface Interceptor extends Serializable{
void init();
void destroy();
String intercept(ActionInvocation invocation) throws Exception;
}

从上述代码中可以看出,该接口共提供了以下三个方法。

  • void init():该方法在拦截器被创建后会立即被调用,它在拦截器的生命周期内只被调用一次。可以在该方法中对相关资源进行必要的初始化。
  • void destroy():该方法与 init() 方法相对应,在拦截器实例被销毁之前,将调用该方法释放和拦截器相关的资源,它在拦截器的生命周期内,也只被调用一次。
  • String intercept(ActionInvocation invocation)throws Exception:该方法是拦截器的核心方法,用于添加真正执行拦截工作的代码,实现具体的拦截操作,它返回一个字符串作为逻辑视图,系统根据返回的字符串跳转到对应的视图资源。每拦截一个动作请求,该方法就会被调用一次。该方法的 ActionInvocation 参数包含了被拦截的 Action 的引用,可以通过该参数的 invoke() 方法,将控制权转给下一个拦截器或者转给 Action 的 execute() 方法。

继承AbstractIntercepter抽象拦截器类

除了实现 Interceptor 接口可以自定义拦截器以外,在实际开发过程中,更常用的一种方式是继承抽象拦截器类 AbstractIntercepter。

AbstractIntercepter 类实现了 Interceptor 接口,并且提供了 init() 方法和 destroy() 方法的空实现。使用时,可以直接继承该抽象类,而不用实现那些不必要的方法。AbstractInterceptor 类中定义的方法如下所示:

1
2
3
4
5
public abstract class AbstractInterceptor implements Interceptor{
public void init(){}
public void destroy(){}
public abstract String intercept (ActionInvocation invocation) throws Exception;
}

从上述代码中可以看出,AbstractInterceptor 类已经实现了 Interceptor 接口的所有方法,一般情况下,只需继承 AbstractInterceptor 类,实现 interceptor() 方法就可以创建自定义拦截器。

需要注意的是,只有当自定义的拦截器需要打开系统资源时,才需要覆盖 AbstractInterceptor 类的 init() 方法和 destroy() 方法。与实现 Interceptor 接口相比,继承 AbstractInterceptor 类的方法更为简单。

Demo

自定义拦截器实现权限控制实例。

新建一个st2test项目。

配置web.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<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>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 首页 -->
<welcome-file-list>
<welcome-file>main.jsp</welcome-file>
</welcome-file-list>
</web-app>

新建User类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.mi1k7ea.domain;

public class User {
private String username; // 用户名
private String password; // 密码
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}

新建LoginAction类,用于处理登录逻辑:

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
package com.mi1k7ea.action;

import com.mi1k7ea.domain.User;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.ModelDriven;

public class LoginAction extends ActionSupport implements ModelDriven<User> {
private static final long serialVersionUID = -8493698886438630994L;
private User user = new User();
@Override
public User getModel() {
return user;
}
public String execute() throws Exception {
// 获取ActionContext
ActionContext actionContext = ActionContext.getContext();
if ("admin".equals(user.getUsername())
&& "123456".equals(user.getPassword())) {
// 将用户存储在session中
actionContext.getSession().put("user", user);
return SUCCESS;
} else {
actionContext.put("msg", "用户名或密码错误,请重新登录!");
return INPUT;
}
}
}

新建BookAction类,商品处理类:

1
2
3
4
5
6
7
8
9
10
11
package com.mi1k7ea.action;

import com.opensymphony.xwork2.ActionSupport;

public class BookAction extends ActionSupport {
private static final long serialVersionUID = 5640989517690867879L;
// 购买图书
public String buy() {
return SUCCESS;
}
}

新建拦截器PrivilegeInterceptor类,继承AbstractInterceptor类,在intercept()函数中通过获取session中是否含有user来判断是否已登录再决定是否放行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.mi1k7ea.interceptor;

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

public class PrivilegeInterceptor extends AbstractInterceptor {
private static final long serialVersionUID = 193664972753450682L;
@Override
public String intercept(ActionInvocation invocation) throws Exception {
// 得到 ActionContext
ActionContext actionContext = invocation.getInvocationContext();
// 获取User对象
Object user = actionContext.getSession().get("user");
if (user != null) {
return invocation.invoke(); // 继续向下执行
} else {
actionContext.put("msg", "您还未登录,请先登录!");
return Action.LOGIN; // 如果用户不存在,则返回login值
}
}
}

配置struts.xml文件,此文件用于声明自定义拦截器、拦截器栈以及对book操作的Action:

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
<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE struts PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
"http://struts.apache.org/dtds/struts-2.3.dtd">

<struts>
<package name="struts2" namespace="/" extends="struts-default">
<!-- 声明拦截器 -->
<interceptors>
<interceptor name="privilege" class="com.mi1k7ea.interceptor.PrivilegeInterceptor" />
<interceptor-stack name="myStack">
<interceptor-ref name="defaultStack" />
<interceptor-ref name="privilege" />
</interceptor-stack>
</interceptors>
<!-- 用户登录操作 -->
<action name="login" class="com.mi1k7ea.action.LoginAction">
<result>/main.jsp</result>
<result name="input">/login.jsp</result>
</action>
<!-- 关于book操作 -->
<action name="book_*" class="com.mi1k7ea.action.BookAction" method="{1}">
<result>/success.jsp</result>
<result name="login">/login.jsp</result>
<!-- 在action中使用自定义拦截器 -->
<interceptor-ref name="myStack" />
</action>
</package>
</struts>

新建main.jsp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>主页</title>
</head>
<body>
<table border="0">
<tr>
<td>《SSH框架整合实战教程》</td>
<td><a href="/st2test/book_buy">购买</a></td>
</tr>
</table>
</body>
</html>

新建login.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 language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>登录</title>
<style type="text/css">
input[type=text],input[type=password]{width:150px}
</style>
</head>
<body>
<div align="center">
<form action="/st2test/login.action" method="post">
<table>
<tr>
<td><label style="text-align:right;">用戶名:</label></td>
<td><input type="text" name="username"></td>
<td><span style="color:#F00">${requestScope.msg }</span></td>
</tr>
<tr>
<td><label style="text-align:right;">密&nbsp;&nbsp;&nbsp;&nbsp;码:</label></td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td align="right" colspan="2"><input type="submit" value="登录"></td>
</tr>
</table>
</form>
</div>
</body>
</html>

新建success.jsp:

1
2
3
4
5
6
7
8
9
10
11
<%@ page language="java" contentType="text/html; charset=utf-8"
pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title>成功页面</title>
</head>
<body>
尊敬的会员${user.username },您已成功购买商品,祝您购物愉快!
</body>
</html>

效果: