什么是Struts 2拦截器?
从软件构架上来说,拦截器是实现了面向方面编程的组件。它将影响了多个业务对象的公共行为封装到一个个可重用的模块,减少了系统的重复代码,实现功能的高度内聚,确保了业务对象的整洁和纯度。
从Java代码上来说,它就是一个普度的Java对象,它只需要实现一个名为Interceptor的接口。
为什么要使用拦截器?
拦截器消除了动作组件中的横切任务(cross-cutting task)。例如,日志记录是一个典型的横切关注。以前,我们可能需要为每个动作组件准备一些记录日志的代码。虽然这样看上去也没什么错误,但是一个不可以忽视的事实是——这些用于记录日志的代码并不是动作组件与模型交互的一部分。并且,日志记录是我们想为系统处理的每一个请求完成的管理性任务,它不是一个动作组件所特有的,而是贯穿了所有动作组件。Struts 2将这个功能放在了更高的层次上,我们可以能把日志记录的代码转移到动作组件之外,从而让动作组件更加简洁清晰。
拦截器组件提供了一个不错的场所,来将不同的横切关注点逻辑分成有层次的、可重用的部件。
拦截器的工作原理?
当框架接收到一个请求的时候,它首先必须决定这个URL映射到哪一个动作组件。这个动作组件的一个实例就会被加入到一个新创建的ActionInvocation实例中。接着框架咨询声明性架构(XML配置文件或Java注解),以发现哪一些拦截器应该被触发,以及按照什么样的顺序触发。将这些拦截器的引用添加到ActionInvocation中。除了这些核心元素,ActionInvocation也拥有其他重要的信息(Servlet请求对象和当前动作组件可用的结果组件列表)。
当ActionInvocation被创建完毕,并且填充了需要的所有对象和信息,就可以被使用了。ActionInvocation公开了一个invoke()方法,框架通过调用这个方法开始动作的执行。当框架调用了这个方法时,ActionInvocation通过执行拦截器栈中的第一个拦截器开始这个调用过程。需要注意的是,invoke()并不是总是指向拦截器栈的第一个拦截器,ActionInvocation负责跟踪执行过程达到的状态,并且把控制交给栈中合适的拦截器(通过调用拦截器的intercept()方法将控制权交给拦截器)。通过递归调用(递归过程?框架通过第一次调用ActionInvocation对象的invoke()方法开始了这个过程。ActionInvocation通过调用拦截器的intercept()方法把控制权交给第一个拦截器。重要的是,intercept()方法把ActionInvocation实例作为一个参数。在拦截器的处理过程中,他会调用ActionInvocation实例参数的invoke()方法来继续调用后续拦截器的递归过程。因此,在通常的执行中,调用过程向下通过所有拦截器,直到栈中再也没有拦截器时,触发动作。另外,ActionInvocation在内部管理处理状态,因此它总是直到自己现在处在栈的什么位置。)ActionInvocation的invoke()方法,拦截器栈中后续的拦截器继续执行,最终执行动作。这是因为每一次invoke()方法被调用时,ActionInvocation都会查询自身的状态,调用接下来的拦截器。在所有拦截器都被调用之后,invoke()方法会促使动作类执行。
下面看一个简单的例子,演示上面所说的理论:
TestAction
public class TestAction extends ActionSupport
{
private static final long serialVersionUID = 2753590609366162370L;
@Override
public String execute() throws Exception
{
System.out.println("execute");
return SUCCESS;
}
}
Interceptor1
public class Intercept1 implements Interceptor
{
private static final long serialVersionUID = 8596224826058233434L;
public void destroy()
{
}
public void init()
{
}
public String intercept(ActionInvocation invocation) throws Exception
{
System.out.println("interceptor1 begin");
String result = invocation.invoke();
System.out.println("interceptor1 end");
return result;
}
}
Interceptor2
public class Intercept2 implements Interceptor
{
private static final long serialVersionUID = -1580591331691823185L;
public void destroy()
{
}
public void init()
{
}
public String intercept(ActionInvocation invocation) throws Exception
{
System.out.println("interceptor2 begin");
String result = invocation.invoke();
System.out.println("interceptor2 end");
return result;
}
}
Interceptor3
public class Intercept3 implements Interceptor
{
private static final long serialVersionUID = 7081124564804422023L;
public void destroy()
{
}
public void init()
{
}
public String intercept(ActionInvocation invocation) throws Exception
{
System.out.println("interceptor3 begin");
String result = invocation.invoke();
System.out.println("interceptor3 end");
return result;
}
}
有三个自定以的拦截器,他们只是简单的输出几个字符串,拦截器开始将执行输出 "interceptor'x' begin";拦截器结束时输出" interceptor'x' end";
将他们设置在一个指定的动作上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 /index.jsp
16
17
提交表单,控制台的输出结果:
interceptor1 begin
interceptor2 begin
interceptor3 begin
execute
interceptor3 end
interceptor2 end
interceptor1 end
从控制台输出的结果可以看出,在提交数据到框架时,框架调用拦截器的过程,首先框架会根据URL请求创建指定的动作TestAction,将TestAction的实例和TestAction相关的拦截器引用myStack放入一个新的ActionInvocation对象中(还包含其他信息),然后框架调用ActionInvocation的invoke()方法,此时开始了拦截器栈调用过程,最开始调用拦截器栈的第一个拦截器也就是Intercept1,拦截器执行完预处理后,因为intercept()方法接收一个ActionInvocation对象作为参数,在Intercept1.intercept()方法中继续调用 ActionInvocation对象的invoke()方法将向下继续调用栈中余下的拦截器Intercept2...一直到栈中没有拦截器为止,最后执行动作组件。在结果被呈现之后,拦截器会按照相反的顺序再触发一遍,使他们可以进行后处理。
拦截器工作原理图:
拦截器触发时能够做些什么?
1. 做一些预处理。在这个阶段拦截器可以用来准备、过滤、改变或者操作任何可以访问的重要数据。这些数据包括所有与当前请求相关的关键对象和数据,也包括动作。
2. 通过调用invoke()方法将控制转移给后续的拦截器,直到动作。或者通过返回一个控制字符串中断执行。在这个阶段,如果拦截器决定请求不应该继续,他可以不调用ActionInvocation实例上的invoke()方法,而是直接返回一个控制字符串。通过这种方式可以停止后续的执行,并且决定哪个结果被呈现。
3. 做一些后加工。在这个阶段,任何一个返回的拦截器可以修改可以访问的对象的数据作为后加工,但是此时结果已经确定了。
怎么声明拦截器?
下面为Struts 2为我们提供的struts-default.xml文件部分:
...
dojo\..*,^struts\..*
input,back,cancel
...
在查看它的关于interceptor部分的dtd文件:
...
name CDATA #REQUIRED
class CDATA #REQUIRED>
name CDATA #REQUIRED>
name CDATA #REQUIRED>
...
查看dtd文档可以看到interceptors标签要定义在package标签下,我们对照struts-default.xml可以看到interceptors标签确实定义在package标签下,并且在dtd文档中package元素右边说明中interceptors标签右边带有一个"?"符号,表示该标签要么有且只有1个,要么没有。
再接着查看dtd文档interceptors元素右面有"(interceptor|interceptor-stack)*"的一串字符式子,表示interceptors标签下可以有0个或多个interceptor标签和interceptor-stack标签,并且不分顺序。此时再对比struts-default.xml文件可以看到interceptors标签下确实有许多interceptor标签和interceptor-stack标签。interceptor标签是用于声明一个独立的拦截器的,而interceptor-stack标签用于组织一系列拦截器的 。
在dtd文档中可以看到interceptor-stack的name属性为#REQUIRED表示这是必须的属性,你声明一个拦截器栈必须给栈取一个名字。而interceptor标签中有两个属性name和class它们都是#REQUIRED,所以你想定义一个拦截器就必须指明这两个属性的值,name为拦截器的名字,class为这个拦截器会使用哪一个类作为它的处理器。还有interceptor-stack标签可以包含许多interceptor-ref标签,这些标签是用于引用你使用interceptor标签声明的拦截器或另外的拦截器栈,它的name属性的值为interceptor标签声明的拦截器的name值或其他拦截器栈的name值,该属性也是#REQUIRED。可以再次对比struts-default.xml文档,可以看到这个两个标签的用法与我们的理解完全一致。若还有什么不清楚可以去下载struts的dtd文档那里有具体的说明。
最后一个package可以定义一组默认拦截器(有且只能有1个,dtd规定) ,例如在struts-default.xml文档中
定义了默认的拦截器组,这个默认的拦截器组会与这个包内没有显示声明自己的拦截器的所有动作相关联。
现在知道了这些,我们就可以定义拦截器了。
拦截器和拦截器栈的声明:
最后,一部分拦截器是可以接收参数,若一个拦截器接收参数,那么interceptor-ref元素是向它们传入参数的地方。比如,struts-default.xml文档中的workflow拦截器
input,back,cancel,browse
怎么构建自定义拦截器?
通过实现Interceptor接口
在编写一个拦截器时需要实现com.opensymphony.xwork2.interceptor.Interceptor接口
public interface Interceptor extends Serializable {
/*** Called to let an interceptor clean up any resources it has allocated.*/
void destroy();
/*** Called after an interceptor is created, but before any requests are processed using
* {@link#intercept(com.opensymphony.xwork2.ActionInvocation) intercept} , giving
* the Interceptor a chance to initialize any needed resources.*/
void init();
/*** Allows the Interceptor to do some processing on the request before and/or after the rest of the processing of the
* request by the {@linkActionInvocation} or to short-circuit the processing and just return a String return code.
*
*@paraminvocation the action invocation
*@returnthe return code, either returned from {@linkActionInvocation#invoke()}, or from the interceptor itself.
*@throwsException any system-level error, as defined in {@linkcom.opensymphony.xwork2.Action#execute()}.*/
String intercept(ActionInvocation invocation) throws Exception;
}
可以看到,这个简单的接口只定义了3个方法。前两个方法时典型的生命周期方法,让你初始化和清理资源。真正的业务逻辑发生在intercept()方法中。这个方法被ActionInvocation.invoke()方法递归调用。
下面演示一个简单的权限验证的例子:
它的工作原理很简单,当一个请求访问一个安全动作时,我们想检查请求是否是一个通过身份验证的用户发出的。
权限认证拦截器:
public class AuthenticationInterceptor implements Interceptor
{
private static final long serialVersionUID = -1500368808387165682L;
public void destroy()
{
}
public void init()
{
}
public String intercept(ActionInvocation invocation) throws Exception
{
Map session = invocation.getInvocationContext().getSession();
User user = (User) session.get("USER");
if (user == null)
{
return Action.INPUT;
}
else
{
Action action = (Action) invocation.getAction();
if (action instanceof UserAware)
{
((UserAware) action).setUser(user);
}
}
return invocation.invoke();
}
}
登陆动作组件:
public class LoginAction extends ActionSupport implements SessionAware
{
private static final long serialVersionUID = 3767975035031881006L;
private String username;
private String password;
private Map session;
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;
}
public void setSession(Map session)
{
this.session = session;
}
@Override
public String execute()
{
User user = null;
if (getUsername().equals(getPassword()))
{
user = new User();
user.setUsername(username);
user.setPassword(password);
}
if (user == null)
{
return INPUT;
}
else
{
session.put("USER" , user);
}
return SUCCESS;
}
}
安全动作组件,需要登陆后才可以进入:
public class TestAction extends ActionSupport implements UserAware
{
private static final long serialVersionUID = 2753590609366162370L;
private User user;
public User getUser()
{
return user;
}
@Override
public String execute() throws Exception
{
return SUCCESS;
}
public void setUser(User user)
{
this.user = user;
}
}
声明拦截器和为Action配置拦截器:
/test.jsp
/input.jsp
/index.jsp
/input.jsp
登录表单:
需要登录权限才可以提交的表单:
成功结果页面:
This is my JSP page.
测试:
打开服务器tomcat,打开浏览器输入http://localhost:服务器端口号/Web项目名称/test.jsp
点击submit按钮,得到以下结果
由于没有登陆,所以权限不够,不能够提交test.jsp的表单,这里我们设置的拦截器起到了作用,下面我们登录这里账号和密码逻辑很简单,只要一样就好了
输入账号密码之后,我们又回到这个页面了,这次再点击submit按钮
可以看到我们成功进入了TestAction
创建自定义的拦截器还可以扩展com.opensymphony.xwork2.interceptor.AbstractInterceptor类,它并没有什么高级的地方,它仅仅只是帮我们实现了Interceptor接口,帮我们默认实现了init()和destory()方法:
public abstract class AbstractInterceptor implements Interceptor {
/*** Does nothing*/
public void init() {
}
/*** Does nothing*/
public void destroy() {
}
/*** Override to handle interception*/
public abstract String intercept(ActionInvocation invocation) throws Exception;
}
最后,Struts 2内置了很多的日常Web开发都会用到的拦截器,所以一般不太需要自己开发一个拦截器,内置的拦截器几乎包含了所有日常需要的功能,要想真正运用好Struts 2必须要了解他们的工作原理,在Struts 2的官方网站上有详细的介绍,可以去哪里找到你需要的东西。