1、综述
在系统中,经常需要在处理用户请求之前和之后执行一些行为,例如检测用户的权限,或者将请求的信息记录到日志中,即平时所说的“权限检测”及“日志记录”。当然不仅仅这些,所以需要一种机制,拦截用户的请求,在请求的前后添加处理逻辑。
Spring MVC提供了Interceptor拦截器机制,用于请求的预处理和后处理。在Spring MVC中定义一个拦截器有两种方法:第一种是实现HandlerInterceptor接口,或者继承实现了HandlerInterceptor接口的类(例如HandlerInterceptorAdapter);第二种方法是实现Spring的WebRequestInterceptor接口,或者继承实现了WebRequestInterceptor接口的类。 这些拦截器都是在Handler的执行周期内进行拦截操作的。
2、HandlerInterceptor接口
如果要实现HandlerInterceptor接口,就要实现其三个方法,分别是preHandle、postHandle及afterCompletion。
preHandle方法在执行Handler方法之前执行。 该方法返回值为Boolean类型,如果返回false,表示拦截请求,不会再向下执行。而如果返回true,表示放行,程序继续向下进行(如果后面没有其他Interceptor,就会直接执行Controller方法)。所以,此方法可以对请求进行判断,决定程序是否继续执行,或者进行一些前置初始化操作及对请求做预处理。
postHandle方法在执行Handler之后,返回ModelAndView之前执行。 由于该方法会在DispatcherServlet进行返回视图渲染之前被调用,所以此方法多被用于统一处理返回的视图,例如将公用的模板数据(例如导航栏菜单)添加到视图,或者根据其他情况指定公用的视图。
afterCompletion方法在执行完Handler之后执行。 由于是在Controller方法执行完毕后执行该方法,所以该方法适合进行统一的异常或者日志处理操作。
这里需要注意的是,由于preHandle方法决定了程序是否继续执行,所以postHandle及afterCompletion方法只能在当前Interceptor的preHandle方法的返回值为true时才会执行。
在实现了HandlerInterceptor接口之后,需要在Spring的类加载配置文件中配置拦截器实现类,才能使拦截器起到拦截的效果。HandlerInterceptor类加载配置有两种方式,分别是“针对HandlerMapping配置”和“全局配置”。
针对拦截器配置,需要在某个HandlerMapping配置中将拦截器作为其参数配置进去,此后通过该HandlerMapping映射成功的Handler就会使用配置好的拦截器,样例配置如下:
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="hInterceptor1" />
<ref bean="hInterceptor2" />
</list>
</property>
</bean>
<bean id="hInterceptor1" class="com.ccff.interceptor.HandlerInterceptorDemo1" />
<bean id="hInterceptor2" class="com.ccff.interceptor.HandlerInterceptorDemo2" />
上面的配置中为BeanNameUrlHandlerMapping处理器映射器配置了一个Interceptors拦截器链,该拦截器链中包含了两个拦截器,名称分别是“hInterceptor1”与“hInterceptor2”,具体的实现分别对应县id为“hInterceptor1”与“hInterceptor2”的bean配置。
此种配置的优点是针对具体的处理器映射器进行拦截操作,缺点是如果使用多个处理器映射器,就要在多处添加拦截器的噢诶之信息,比较繁琐。
针对全局配置,只需要在Spring的类加载配置文件中添加“<mvc:interceptor >”标签对,在该标签对中配置的拦截器,可以起到全局拦截器的作用,这是因为该配置会将拦截器注入每一个HandlerMapping处理器映射器中,样例配置如下:
<!--拦截器-->
<mvc:interceptors>
<!--多个拦截器顺序执行-->
<mvc:interceptor>
<!--/**表示所有url包括子url路径-->
<mvc:mapping path="/**"/>
<bean class="com.ccff.interceptor.HandlerInterceptorDemo1" />
</mvc:interceptor>
<mvc:interceptor>
<!--/**表示所有url包括子url路径-->
<mvc:mapping path="/**"/>
<bean class="com.ccff.interceptor.HandlerInterceptorDemo2" />
</mvc:interceptor>
</mvc:interceptors>
在上面的配置中,可以在“<mvc:interceptor >”标签下配置多个Interceptors拦截器,这些拦截器会顺序执行。在每个拦截器中,可以定义拦截器响应的url请求路径,可以是某一个子域下的请求,也可以是上述例子中的“/**”的形式,表示拦截所有url(包括子url路径)。通过bean标签配置拦截器的具体实现。
在日常开发中,可能会根据业务需求,配置多种拦截器来过滤不同信息。
3、WebRequestInterceptor接口
HandlerInterceptor主要进行请求前及请求后的拦截,而WebRequestInterceptor接口是针对请求的拦截器接口的,该接口方法参数中没有response,所以使用该接口只进行请求数据的准备和处理。
WebRequestInterceptor接口中也定义了三个方法,所以事先WebRequestInterceptor接口进行拦截的机制也是实现这三种方法。每个方法都含有WebRequest参数,WebRequest的方法定义与HttpServletRequest基本相同。在WebRequestInterceptor中对WebRequest进行的所有操作都将同步到HttpServletRequest中,然后在当前请求中一直传递。
在WebRequestInterceptor中,preHandler、postHandler及afterCompletion方法的使用与在HandlerInterceptor中略有不同。
preHandle方法也是在执行Handler方法之前执行。该方法返回值为void。由于没有返回值,使用该方法主要进行数据的前期准备。利用WebRequest的setAttribute(name,value,scope)方法,将需要准备的参数放到WebRequest的属性中。WebRequest的setAttribute方法的第三个参数scope的类型为Integer,在WebRequest的父层接口RequestAttributes中为它定义了三个常量,如下表所示:
常量名 | 真实值 | 释义 |
---|---|---|
SCOPE_REQUEST | 0 | 代表只有在request中可以访问 |
SCOPE_SESSION | 1 | 如果环境下允许它代表一个局部的隔离的session,否则就代表普通的session,并且在该session范围内可以访问 |
导SCOPE_GLOBAL_SESSION | 2 | 如果环境允许,它代表一个全局共享的session,否则就代表普通的session,并在在该session范围内可以访问 |
postHandle方法也是在执行Handler之后,返回ModelAndView之前执行。postHandler方法中有一个数据模型ModelMap,它是Controller处理之后返回的Model对象。可以通过改变ModelMap中的属性来改变Controller最终返回的Model模型。
afterCompletion方法也是在执行完Handler之后执行。如果为之前的preHandler方法中的WebRequest准备了一些参数,那么在afterCompletion方法中,可以将WebRequest参数中不需要的准备资源释放掉。
最后,WebRequestInterceptor拦截接口与HandlerInterceptor有以下两点区别:
第一,HandlerInterceptor接口的preHandle有一个Boolean类型的返回值,而WebRequestInterceptor的preHandle方法没有返回值。
第二,HandlerInterceptor是针对请求的整个过程的,接口的方法中都含有response参数。而WebRequestInterceptor是针对请求的,接口方法参数中没有response。
4、拦截器链
根据前面的学习我们知道,在一个Web工程中,甚至在一个HandlerMapping处理器适配器中都可以配置多个拦截器,每个拦截器都按照提前配置好的顺序执行。但是值得注意的是,它们内部的执行规律并不像多个普通Java类一样,它们的设计模式是基于“责任链”的模式。
拦截器的preHandler是有请求放行或拦截的规律的,所以拦截器链的执行首先从preHandler方法开始,逐步执行每一个拦截器的preHandler方法,若是某一个拦截器的preHandler返回false,则后面的所有拦截器的preHandler方法就不能够执行了。与此类似,postHandler与afterCompletion的执行也对责任链中的其他拦截器的执行有所影响,其实这些方法就是紧紧围绕这Controller的执行来根据不同的执行周期顺序执行的。
为了更加清楚的演示拦截器的执行模式,这里编写两个全局的拦截器,并在它们的每个方法中打印日志,然后观察它们的运行模式。
首先,在“com.ccff.interceptor”包下创建名为“HandlerInterceptorDemo1”和“HandlerInterceptorDemo2”的两个拦截器实现类,其中HandlerInterceptorDemo1的具体代码如下:
package com.ccff.interceptor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class HandlerInterceptorDemo1 implements HandlerInterceptor {
//创建该类的日志对象
Log log = LogFactory.getLog(this.getClass());
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
log.info("Demo1's preHandler method start");
return true; //放行
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
log.info("Demo1's postHandle method start");
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
log.info("Demo1's afterCompletion method start");
}
}
以上代码分别通过log对象打印了每个方法的info级别的日志信息,然后在preHandler方法中放行。HandlerInterceptorDemo2的方法与HandlerInterceptorDemo1 的几乎相同,唯一区别就是log对象打印“Demo2”相关信息,代码就不再赘述。
接着,在“com.ccff.controller”下创建名为“InterceptorController”的控制器类,具体代码如下所示:
package com.ccff.interceptor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/InterceptorTest")
public class InterceptorController {
@RequestMapping("/login")
public String login(){
return "user/login";
}
}
然后,在Spring MVC的类加载文件springmvc.xml中配置这两个全局拦截器,作为拦截器链,具体配置信息如下:
<!--拦截器-->
<mvc:interceptors>
<!--多个拦截器顺序执行-->
<mvc:interceptor>
<!--/**表示所有url包括子url路径-->
<mvc:mapping path="/**"/>
<bean class="com.ccff.interceptor.HandlerInterceptorDemo1" />
</mvc:interceptor>
<mvc:interceptor>
<!--/**表示所有url包括子url路径-->
<mvc:mapping path="/**"/>
<bean class="com.ccff.interceptor.HandlerInterceptorDemo2" />
</mvc:interceptor>
</mvc:interceptors>
最后,将项目部署到Tomcat上后,在浏览器内访问http://localhost:8080/demo/ExceptionTest/login.action 后查看Tomcat日志信息如下:
通过控制台信息可以观察到,两个拦截器的执行顺序并不是完全线性的,而是根据不同的方法功能穿插运行,这也是“责任链”设计模式的一个特点。
可以看到,拦截器Demo1首先执行了其preHandle方法,打印日志并返回true,从而放行请求。接下来请求被拦截器Demo2拦截,Demo2执行器preHandle方法,也打印了日志并返回true放行。
随后请求到达Controller的具体方法中,然后处理完方法的具体逻辑在返回ModelAndView或渲染视图之前,执行了Demo2的postHandle方法,打印了日志信息,原因是此时拦截器的流程正处于Demo2的周期中。然后当Demo2的postHandle方法执行结束后,紧接着回到Demo1的执行过程,去执行Demo1的postHandle方法,打印相关的日志。
接下来Controller方法执行完毕,在返回结果视图前执行了Demo2的afterCompletion方法,打印相关日志,紧接着执行了Demo1的afterCompletion方法,打印相关日志,完成整个请求过程的拦截。
注意: 多个拦截器是根据目前请求处理的状态进行分层校验的。
5、拦截器登录控制
在该演示案例中,实现如下逻辑:拦截用户的请求,判断用户是否已经登录,如果用户没有登录,则跳转到login界面,如果用户已经登录,则放行。
第一步,在“com.ccff.interceptor”下创建名为“LoginInterceptor”的登录拦截器,实现HandlerInterceptor接口,实现其三个方法。这里因为要判断用户的登录情况,因此主要以preHandle方法为主,具体代码如下所示:
package com.ccff.interceptor;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
String uri = httpServletRequest.getRequestURI();
//判断当前请求地址是否是登录地址
if (!(uri.contains("login") || uri.contains("Login"))){
//非登录请求
if (httpServletRequest.getSession().getAttribute("user") != null){
//说明用户已经登录过,放行
return true;
}else {
//说明用户之前没有登录过,跳转到登录页面
httpServletResponse.sendRedirect(httpServletRequest.getContextPath()+"/InterceptorTest/login.action");
}
}else {
//登录请求,直接放心
return true;
}
return false; //默认拦截
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
这里要说明的是,在用户登录成功后,会将用户信息封装在user对象中,并防止在全局的session会话对象中。在上面的代码中,在preHandle方法中编写了控制用户登录权限的逻辑。首先判断请求是否去往登录界面,如果是则直接返回true放行。如果不是,则检测用户的user信息是否在session中,如果不存在,则说明用户没有登录,跳转到login页面。如果session中包含了user对象,则说明用户已经登录,此时直接放行返回true。
第二步,在Spring MVC的核心配置文件中配置该全局拦截器,具体配置如下:
<!--拦截器-->
<mvc:interceptors>
<!--多个拦截器顺序执行-->
<mvc:interceptor>
<!--/**表示所有url包括子url路径-->
<mvc:mapping path="/**"/>
<bean class="com.ccff.interceptor.LoginInterceptor" />
</mvc:interceptor>
<mvc:interceptor>
<!--/**表示所有url包括子url路径-->
<mvc:mapping path="/**"/>
<bean class="com.ccff.interceptor.HandlerInterceptorDemo1" />
</mvc:interceptor>
<mvc:interceptor>
<!--/**表示所有url包括子url路径-->
<mvc:mapping path="/**"/>
<bean class="com.ccff.interceptor.HandlerInterceptorDemo2" />
</mvc:interceptor>
</mvc:interceptors>
第三步,修改“com.ccff.controller”包下的“InterceptorController”,具体代码如下:
package com.ccff.controller;
import com.ccff.model.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.servlet.http.HttpServletRequest;
@Controller
@RequestMapping("/InterceptorTest")
public class InterceptorController {
@RequestMapping("/index")
public String index(){
return "interceptor/index";
}
@RequestMapping("/login")
public String login(){
return "interceptor/login";
}
@RequestMapping("/doLogin")
public String doLogin(Model model, HttpServletRequest request, User user){
//检测账号密码,成功即登录成功
boolean flag = checkUser(user);
if (flag){
//登录成功,将用户信息放入session中
request.getSession().setAttribute("user",user);
}else {
//登录失败,返回登录界面,重新登录
model.addAttribute("errorMsg","账号或密码错误!");
return "interceptor/login";
}
return "interceptor/success";
}
@RequestMapping("/loginout")
public String loginout(Model model, HttpServletRequest request){
if (request.getSession().getAttribute("user") != null){
//将用户信息从session中清除
request.getSession().removeAttribute("user");
}else {
model.addAttribute("errorMsg","注销失败!用户已注销!");
}
return "interceptor/login";
}
private boolean checkUser(User user) {
if (1 == user.getUserId() && "张三".equals(user.getUsername()) && "123456789".equals(user.getPassword()))
return true;
else
return false;
}
}
第四步,在“WEB-INF/jsp”下创建名为“interceptor”的文件夹,在该文件夹下创建名为“login.jsp”、“success.jsp”和“index.jsp”三个JSP页面,具体代码为:
login.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!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>
<h3>用户登录</h3>
<%--显示错误信息--%>
<c:if test="${errorMsg != null}">
<font color="red">${errorMsg}</font>
</c:if>
<form action="doLogin.action" method="post">
<table width="300px;" border=1>
<tr>
<td>用户编号:</td>
<td><input type="text" name="userId" id="userId" /></td>
</tr>
<tr>
<td>用户名:</td>
<td><input type="text" name="username" id="username" /></td>
</tr>
<tr>
<td>密 码:</td>
<td><input type="password" name="password" id="password" /></td>
</tr>
</table>
<br/>
<input type="submit" id="login_button" value="用户登录" />
</form>
</body>
</html>
success.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>登录成功</title>
</head>
<body>
<script type="text/javascript">
onload=function(){
setInterval(go, 1000);
};
var x=10; //利用了全局变量来执行
function go(){
x--;
if(x>0){
document.getElementById("sp").innerHTML=x+"秒后跳转到首页"; //每次设置的x的值都不一样了。
}else{
location.href='${pageContext.request.contextPath}/InterceptorTest/index.action';
}
}
</script>
<h2>登录成功!</br></br>欢迎您【${user.username}】!</h2>
<br/>
<h3><span id="sp">10秒后跳转到首页</span> </h3>
<hr/>
<h3>使用user参数显示用户登录信息</h3>
<table width="300px;" border=1>
<tr>
<td>用户编号:</td>
<td>${user.userId}</td>
</tr>
<tr>
<td>用户名:</td>
<td>${user.username}</td>
</tr>
<tr>
<td>密 码:</td>
<td>${user.password}</td>
</tr>
</table>
</body>
</html>
index.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>首页</title>
</head>
<body>
<h2>这是一个只有33个字符的简单且不失华丽的首页!不信?你数数看!</h2>
<br/>
<hr/>
<br/>
<h4><a href="${pageContext.request.contextPath}/InterceptorTest/loginout.action">注销</a></h4>
</body>
</html>
最后,将项目部署到Tomcat上后,在浏览器内输入请求URL:http://localhost:8080/demo/InterceptorTest/index.action 后,由于拦截器的存在,判断到该用户还未登录,因此会自动跳转到登录页面(loginjsp),具体如下所示:
由于没有连接数据库,因此我们默认的登录信息为(用户编号:1、用户名:张三、密码:123456789)若第一次输入错误信息后,将跳转回登录页面并显示登录信息如下所示:
当输入正确登录信息后,系统会将用户信息保存到全局session中,并跳转到登录成功页面,显示登录信息,并在10秒后跳转到主页,具体如下所示:
若此时,在浏览器内再开一个新的标签页直接访问http://localhost:8080/demo/InterceptorTest/index.action 则可以直接显示该页面。
若在首页中点击注销按钮后,系统会将全局session中的用户信息删除,并跳转回登录页面。
通过以上测试结果,说明拦截器配置是成功且有效的。
6、数据回显
所谓数据回显就是当用户在登录页面中输入的登录信息有误后,再次跳转回登录页面的时候,用户之前输入的信息仍然存在显示,方便用户再次检查修改,同时也避免了用户需要再次将全部信息再次输入一遍,提高了用户体验。
而数据回显的原理就是通过model对象将表单原始数据直接转向页面中,在页面中通过EL表达式判断取出显示即可。
例如,首先修改InterceptorController中的doLogin方法如下所示:
@RequestMapping("/doLogin")
public String doLogin(Model model, HttpServletRequest request, User user){
//检测账号密码,成功即登录成功
boolean flag = checkUser(user);
if (flag){
//登录成功,将用户信息放入session中
request.getSession().setAttribute("user",user);
}else {
//登录失败,返回登录界面,重新登录
model.addAttribute("errorMsg","账号或密码错误!");
model.addAttribute("user",user);
return "interceptor/login";
}
return "interceptor/success";
}
然后,修改login.jsp页面如下所示:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!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>
<h3>用户登录</h3>
<%--显示错误信息--%>
<c:if test="${errorMsg != null}">
<font color="red">${errorMsg}</font>
</c:if>
<form action="doLogin.action" method="post">
<table width="300px;" border=1>
<c:choose>
<c:when test="${user != null}">
<tr>
<td>用户编号:</td>
<td><input type="text" name="userId" value="${user.userId}" /></td>
</tr>
<tr>
<td>用户名:</td>
<td><input type="text" name="username" value="${user.username}" /></td>
</tr>
<tr>
<td>密 码:</td>
<td><input type="password" name="password" value="${user.password}" /></td>
</tr>
</c:when>
<c:otherwise>
<tr>
<td>用户编号:</td>
<td><input type="text" name="userId" /></td>
</tr>
<tr>
<td>用户名:</td>
<td><input type="text" name="username" /></td>
</tr>
<tr>
<td>密 码:</td>
<td><input type="password" name="password" /></td>
</tr>
</c:otherwise>
</c:choose>
</table>
<br/>
<input type="submit" id="login_button" value="用户登录" />
</form>
</body>
</html>
l 最后,将项目重新部署到Tomcat上后,直接在浏览器内输入请求URL:http://localhost:8080/demo/InterceptorTest/login.action 后在等路表单中输入如下信息:
由于并没有输入密码,因此一定会提示登录失败,并跳转回登录页面,但是此时跳转回来后,用户之前输入的用户编号和用户名信息仍然存在,具体如下所示: