一、Spring MVC 拦截器介绍
Spring MVC 的拦截器(如无特殊说明,下文所说的拦截器即处理器拦截器)类似于过滤器 Filter,用于对处理器进行预处理和后处理。
二、拦截器常用应用场景
拦截器的应用场景也很多,主要有以下几个 方面:
-
日志记录:请求信息的日志记录,以对系统进行监控、信息统计等。
-
权限检查:如登录校验、权限拦截;
-
性能监控:可以通过拦截器记录请求开始时间和结束时间,从而得到该请求的处理时间;
-
通用行为:cookie 、Locale 、Theme 信息的存取;
-
资源管理:Session 管理、资源清理。
三、编写自定义拦截器
自定义拦截器,需要实现 HandlerInterceptor 接口:
package com.ssm.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
public class HandlerInterceptor1 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
System.out.println("HandlerInterceptor1 ... preHandle");
return true;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("HandlerInterceptor1 ... postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
System.out.println("HandlerInterceptor1 ... afterCompletion");
}
}
实现 HandlerInterceptor 接口的拦截器中有三个方法,在这之中有三个方法,每个方法的调用时间都不一样:
preHandle :Controller 执行前调用此方法,返回值为 true 表示继续流程(如调用下一个拦截器或处理器),为 false 表示该拦截器拦截了(如登录校验失败),不会继续调用其他两个方法,此时我们需要通过 response 来产生响应;
postHandle :Controller 执行后,并且未返回 ModelAndView 前调用此方法,此时我们可以通过形参 ModelAndView 对象对模型数据或视图进行再处理,ModelAndView 对象可能为 null。这个方法必须所以的拦截器的 preHandle 方法都返回 true 才执行。
afterCompletion :Controller 执行后且视图返回后调用此方法,此时可以进行性能监控和一些资源的清理。这个方法必须是 preHandle 返回 true 才执行。
注:如果想知道其原因,请看 org.springframework.web.servlet.DispatcherServlet
的 方法 doDispatch
,其中有详细说明。
四、拦截器配置
1. 针对某一个特定的 Mapping 配置拦截器:
<bean
class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="handlerInterceptor1"/>
<ref bean="handlerInterceptor2"/>
</list>
</property>
</bean>
<bean id="handlerInterceptor1" class="com.ssm.interceptor.HandlerInterceptor1"/>
<bean id="handlerInterceptor2" class="com.ssm.interceptor.HandlerInterceptor2"/>
2. 全局配置拦截器:
在 springmvc.xml 中配置,这里配置了两个全局拦截器 HandlerInterceptor1 和 HandlerInterceptor2。
<!-- 拦截器 -->
<mvc:interceptors>
<!-- 配置拦截器方式一 -->
<!-- <bean class="com.ssm.interceptor.HandlerInterceptor1"></bean> -->
<!-- 配置拦截器方式二 -->
<!--多个拦截器,顺序执行 -->
<!-- "/**" 表示所有的url路径包括子url路径
"/*" 表示所有的url路径
-->
<mvc:interceptor>
<!-- 拦截哪些URL -->
<mvc:mapping path="/**"/>
<!-- 哪些URL不拦截 -->
<mvc:exclude-mapping path="/toLogin.action"/>
<!-- 全局拦截器 -->
<bean class="com.ssm.interceptor.HandlerInterceptor1"></bean>
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/toLogin.action"/>
<bean class="com.ssm.interceptor.HandlerInterceptor2"></bean>
</mvc:interceptor>
</mvc:interceptors>
五、拦截器实现登陆验证
1. 编写登陆 Controller
package com.ssm.controller;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class LoginController {
// 登陆页面
@RequestMapping(value="/toLogin",method=RequestMethod.GET)
public String login() throws Exception {
//跳转登陆页面
return "login";
}
// 登陆
@RequestMapping(value="/login",method=RequestMethod.POST)
public String login(HttpSession session, String username, String password)
throws Exception {
// 调用service进行用户身份验证
// 在session中保存用户身份信息
session.setAttribute("username", username);
return "success";
}
// 退出
@RequestMapping("/logout")
public String logout(HttpSession session) throws Exception {
// 清除session
session.invalidate();
return "error";
}
}
2. 在 HandlerInterceptor1 拦截器中的 preHandle 方法中编写登陆校验,其中对于请求 login.action
不进行拦截。
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
System.out.println("HandlerInterceptor1 ... preHandle");
//获取请求的url
String url = request.getRequestURI();
//判断url是否是公开 地址(实际使用时将公开地址配置配置文件中)
if(url.indexOf("login.action") >= 0) {
//如果进行登陆提交,放行
return true;
}
//判断session
HttpSession session = request.getSession();
//从session中取出用户身份信息
String username = (String) session.getAttribute("username");
if(username != null && !"".equals(username)){
//身份存在,放行
return true;
}
//执行这里表示用户身份需要认证,跳转登陆页面
request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request, response);
return false;
}
3. 配置全局拦截器
在 springmvc.xml 中配置:
<!-- 拦截器 -->
<mvc:interceptors>
<mvc:interceptor>
<!-- 拦截哪些URL -->
<mvc:mapping path="/**"/>
<!-- 哪些URL不拦截 -->
<mvc:exclude-mapping path="/toLogin.action"/>
<!-- 全局拦截器 -->
<bean class="com.ssm.interceptor.HandlerInterceptor1"></bean>
</mvc:interceptor>
</mvc:interceptors>
使用上面的全局拦截器配置,其中对 toLogin.action
不进行拦截,因为这个进入登陆页面的请求 URL。
4. JSP 页面
login.jsp 页面在 WEB-INF 的 JSP 文件夹下,代码如下:
<form action="${pageContext.request.contextPath }/login.action" method="post">
用户名:<input type="text" name="username">
密码:<input type="password" name="password">
<input type="submit" value="submit">
</form>
<a href="${pageContext.request.contextPath }/logout.action">注销</a>
在 index.jsp 中添加链接 <a href="${pageContext.request.contextPath }/toLogin.action">登陆页面</a>
到登陆页面。
5. 总结
当没登陆时,在 index.jsp 页面点击链接到登陆页面时,拦截器不会拦截,因为在配置全局拦截器时将其设置为不拦截了,控制台打印:
DEBUG [http-bio-8080-exec-2] - DispatcherServlet with name 'SpringMVC' processing GET request for [/ssm/toLogin.action]
DEBUG [http-bio-8080-exec-2] - Looking up handler method for path /toLogin.action
DEBUG [http-bio-8080-exec-2] - Returning handler method [public java.lang.String com.ssm.controller.LoginController.login() throws java.lang.Exception]
DEBUG [http-bio-8080-exec-2] - Returning cached instance of singleton bean 'loginController'
DEBUG [http-bio-8080-exec-2] - Last-Modified value for [/ssm/toLogin.action] is: -1
DEBUG [http-bio-8080-exec-2] - Rendering view [org.springframework.web.servlet.view.JstlView: name 'login'; URL [/WEB-INF/jsp/login.jsp]] in DispatcherServlet with name 'SpringMVC'
DEBUG [http-bio-8080-exec-2] - Forwarding to resource [/WEB-INF/jsp/login.jsp] in InternalResourceView 'login'
DEBUG [http-bio-8080-exec-2] - Successfully completed request
而请求其他链接时,如注销 /logout.action
是会进行拦截的,然后跳转到登陆页面。控制台打印:
DEBUG [http-bio-8080-exec-8] - DispatcherServlet with name 'SpringMVC' processing GET request for [/ssm/logout.action]
DEBUG [http-bio-8080-exec-8] - Looking up handler method for path /logout.action
DEBUG [http-bio-8080-exec-8] - Returning handler method [public java.lang.String com.ssm.controller.LoginController.logout(javax.servlet.http.HttpSession) throws java.lang.Exception]
DEBUG [http-bio-8080-exec-8] - Returning cached instance of singleton bean 'loginController'
DEBUG [http-bio-8080-exec-8] - Last-Modified value for [/ssm/logout.action] is: -1
HandlerInterceptor1 ... preHandle
DEBUG [http-bio-8080-exec-8] - Successfully completed request
当登陆后,可正常访问 /logout.action
,是因为拦截器拦截到它后进行登陆校验,发现其已经登陆了,就对其放行,然后才能正常访问。
DEBUG [http-bio-8080-exec-9] - DispatcherServlet with name 'SpringMVC' processing POST request for [/ssm/login.action]
DEBUG [http-bio-8080-exec-9] - Looking up handler method for path /login.action
DEBUG [http-bio-8080-exec-9] - Returning handler method [public java.lang.String com.ssm.controller.LoginController.login(javax.servlet.http.HttpSession,java.lang.String,java.lang.String) throws java.lang.Exception]
DEBUG [http-bio-8080-exec-9] - Returning cached instance of singleton bean 'loginController'
HandlerInterceptor1 ... preHandle
DEBUG [http-bio-8080-exec-9] - Skip CORS processing, request is a same-origin one
HandlerInterceptor1 ... postHandle
DEBUG [http-bio-8080-exec-9] - Invoking afterPropertiesSet() on bean with name 'success'
DEBUG [http-bio-8080-exec-9] - Rendering view [org.springframework.web.servlet.view.JstlView: name 'success'; URL [/WEB-INF/jsp/success.jsp]] in DispatcherServlet with name 'SpringMVC'
DEBUG [http-bio-8080-exec-9] - Forwarding to resource [/WEB-INF/jsp/success.jsp] in InternalResourceView 'success'
HandlerInterceptor1 ... afterCompletion
DEBUG [http-bio-8080-exec-9] - Successfully completed request
六、研究拦截器执行顺序
以两个拦截器为例,研究拦截器的执行顺序。因此,我们编写一个拦截器 2 。然后进行全局的配置。
package com.ssm.interceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class HandlerInterceptor2 implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
System.out.println("HandlerInterceptor2 ... preHandle");
return true;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("HandlerInterceptor2 ... postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex)
throws Exception {
System.out.println("HandlerInterceptor2 ... afterCompletion");
}
}
增加拦截器 2 的配置:
<!-- 拦截器 -->
<mvc:interceptors>
<mvc:interceptor>
<!-- 拦截哪些URL -->
<mvc:mapping path="/**"/>
<!-- 哪些URL不拦截 -->
<mvc:exclude-mapping path="/toLogin.action"/>
<!-- 全局拦截器 -->
<bean class="com.ssm.interceptor.HandlerInterceptor1"></bean>
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/**"/>
<mvc:exclude-mapping path="/toLogin.action"/>
<bean class="com.ssm.interceptor.HandlerInterceptor2"></bean>
</mvc:interceptor>
</mvc:interceptors>
说明:这里的两个拦截器的顺序是拦截器1在前,拦截器2在后。
1. 拦截器1放行,拦截器2放行
控制台打印出:
HandlerInterceptor1 ... preHandle
HandlerInterceptor2 ... preHandle
HandlerInterceptor2 ... postHandle
HandlerInterceptor1 ... postHandle
HandlerInterceptor2 ... afterCompletion
HandlerInterceptor1 ... afterCompletion
结论:preHandle 按配置顺序正序执行;postHandle 按配置倒序执行;afterCompletion 按配置倒序执行。
2. 拦截器1不放行,拦截器2放行
控制台打印出:
HandlerInterceptor1 ... preHandle
结论:拦截器1执行 preHandle,拦截器2不执行 preHandle;postHandle 都不执行;afterCompletion 都不执行。
3. 拦截器1放行,拦截器2不放行
控制台打印出:
HandlerInterceptor1 ... preHandle
HandlerInterceptor2 ... preHandle
HandlerInterceptor1 ... afterCompletion
结论:preHandle 按配置顺序正序执行;postHandle 都不执行;拦截器1执行 afterCompletion,拦截器2不执行 afterCompletion。
4. 拦截器1不放行,拦截器2不放行
控制台打印出:
HandlerInterceptor1 ... preHandle
结论:拦截器1执行 preHandle ,拦截器2不执行 preHandle ;postHandle 都不执行;afterCompletion 都不执行。
综上所述,如果是通过拦截器进行日志记录的话,需要配置成第一个拦截器,登陆校验配置成第二个拦截器,等等。