10、拦截器
10.1、简介
SpringMVC 拦截器(HandlerInterceptor)是 Spring AOP 的应用。
- 可用于对处理器进行预处理和后置处理:相当于 AOP 环绕通知;
- 类似 Servlet 的过滤器(Filter)。
过滤器 | 拦截器 | |
---|---|---|
说明 | Servlet 规范中的一部分 | SpringMVC 框架内置 |
可用工程 | 任何 Java Web 工程 | 使用了 SpringMVC 框架的工程 |
配置 | 在 web.xml 的 url-pattern 中配置 | 在 Spring 配置文件的 mvc:interceptor 中配置 |
拦截 | 要访问的资源,包括静态资源 | Controller 请求方法,不包括静态资源 |
10.2、自定义拦截器
10.2.1、环境搭建
- 新建 module,添加 web 框架支持:在项目结构中手动添加 lib 目录;
-
web.xml
- DispatcherServlet
- 字符编码过滤器
-
applicationContext.xml
- 添加注解支持
- 静态资源过滤
- 添加注解驱动
- 视图解析器
web.xml
<?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">
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<filter>
<filter-name>characterEncoding</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncoding</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
applicationContext.xml
<?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"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<context:component-scan base-package="indi.jaywee.controller"/>
<mvc:default-servlet-handler/>
<mvc:annotation-driven/>
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
id="internalResourceViewResolver">
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>
</beans>
10.2.2、自定义拦截器
TestController
@RestController
@RequestMapping("/test")
public class TestController {
@RequestMapping("/t1")
public String test1() {
String msg = "TestController test() has been executed";
System.out.println(msg);
return msg;
}
}
MyInterceptor
实现 HandlerInterceptor 接口,重写方法
public class MyInterceptor1 implements HandlerInterceptor {
/**
* @return true表示放行
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("MyInterceptor1:预处理");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("MyInterceptor1:后置处理");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("MyInterceptor1:资源清理");
}
}
配置
在 Spring 配置文件中配置拦截器。
-
/
:拦截请求,不拦截页面; -
/*
:拦截文件夹,不包括子文件夹; -
/**
:拦截文件夹,包括子文件夹。
<mvc:interceptors>
<mvc:interceptor>
<!-- 要拦截的请求路径 -->
<mvc:mapping path="/test/**"/>
<!-- 拦截器 -->
<bean class="indi.jaywee.interceptor.MyInterceptor1"/>
</mvc:interceptor>
</mvc:interceptors>
10.3、HandlerInterceptor
- SpringMVC 中的 Interceptor 是链式调用的;
- 在一个应用或请求中可以同时存在多个拦截器,它们根据声明顺序(配置的先后顺序)依次执行。
preHandle ()
/**
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
...
}
- 返回值类型:Boolean
- true:放行。执行请求和 Interceptor 中的其它两个方法,调用下个 Interceptor;
- false :拦截请求。
-
运行时间:
- 在请求处理之前,即在 Controller 方法调用之前运行;
- 先声明的 Interceptor 的 preHandler 方法先执行。
-
作用
- 前置初始化操作或预处理请求,如获取 session 或 cookie 判断是否登录;
- 判断是否要执行请求,通过方法的返回值决定。
postHandle ()
/**
*
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
...
}
- 返回值类型:void
-
运行时间:
- 在请求处理之后,即在 Controller 方法调用之后运行;
- 在 DispatcherServlet 进行视图渲染之前;
- 后声明的 Interceptor 的 postHandle 方法先执行,与 preHandle 相反;
- 作用:使用 ModelAndView 对象,如设置 session 或 cookie。
afterCompletion ()
/**
*
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
...
}
- 返回值类型:void
-
运行时间:
- 在整个请求处理结束之后,DispatcherServlet 渲染了视图渲染之后;
- 后声明的 Interceptor 的 afterCompletion 方法先执行,与 postHandle 相同,与 preHandle 相反;
- 作用:资源清理。
10.4、拦截器执行顺序
先测试,再得出结论。
10.4.1、一个拦截器
对一组请求配置拦截器:A
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/test/**"/>
<bean class="indi.jaywee.interceptor.MyInterceptorA"/>
</mvc:interceptor>
</mvc:interceptors>
-
放行:预处理方法返回true
-
拦截:预处理方法返回 false
10.4.2、多个拦截器
对同一组请求配置3个拦截器,声明顺序为 A、B、C。
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/test/**"/>
<bean class="indi.jaywee.interceptor.MyInterceptorA"/>
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/test/**"/>
<bean class="indi.jaywee.interceptor.MyInterceptorB"/>
</mvc:interceptor>
<mvc:interceptor>
<mvc:mapping path="/test/**"/>
<bean class="indi.jaywee.interceptor.MyInterceptorC"/>
</mvc:interceptor>
</mvc:interceptors>
-
放行:三个预处理方法都返回 true
-
A拦截:A的预处理方法返回 false
-
A放行,B拦截:A的预处理方法返回 true,B的预处理方法返回 false
-
AB放行,C拦截:AB的预处理方法返回 true,C的预处理方法返回 false
10.4.3、结论
拦截器在不同情况的执行顺序如下:
一个拦截器
-
放行
- preHandle
- 请求处理
- postHandle
- afterCompletion
- 拦截:只执行 preHandle
多个拦截器
-
全部放行
-
按拦截器声明的顺序,执行每个拦截器的 preHandle
-
请求处理
-
按拦截器声明的相反顺序,执行每个拦截器的 postHandle
-
按拦截器声明的相反顺序,执行每个拦截器的 afterCompletion;
-
-
部分放行:假设执行到
拦截器demo
时拦截请求- 按拦截器声明的顺序,执行每个拦截器的 preHandle,直到被拦截(包括
demo
的 preHandle); - 按拦截器声明的相反顺序,执行每个拦截器的 afterCompletion(不包括
demo
的 afterCompletion); - 即:放行了的拦截器,会执行 preHandle 和 afterCompletion;而未放行的拦截器,只会执行 preHandle
- 按拦截器声明的顺序,执行每个拦截器的 preHandle,直到被拦截(包括
10.5、案例:登录拦截
10.5.1、环境搭建
Controller
UserController
- 用户登录成功,添加 session;
- 用户注销,移除 session。
@Controller
@RequestMapping("/user")
public class UserController {
/**
* 跳转到首页
*/
@RequestMapping("/toHome")
public String toHome() {
return "home";
}
/**
* 跳转到登录页
*/
@RequestMapping("/toLogin")
public String toLogin() {
return "login";
}
/**
* 处理用户登录
*/
@RequestMapping("/login")
public String login(String username, String password, HttpSession session) {
// 判断用户名密码是否合法,此处省略
// 登录成功,设置session
session.setAttribute("userInfo", username);
return "success";
}
/**
* 处理用户注销
*/
@RequestMapping("/logout")
public String logout(HttpSession session) {
String userInfoSession = "userInfo";
if (session.getAttribute(userInfoSession) != null) {
session.removeAttribute(userInfoSession);
}
return "login";
}
}
页面
index.jsp
- 由于页面放在 WEB-INF 下,无法直接通过地址栏访问;
- 因此通过超链接请求访问。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>$Title$</title>
</head>
<body>
<h3><a href="${pageContext.request.contextPath}/user/toHome">进入首页</a></h3>
<h3><a href="${pageContext.request.contextPath}/user/toLogin">进入登录页</a></h3>
</body>
</html>
home.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>用户首页</title>
</head>
<body>
<h1>用户首页</h1>
<span>这里是用户首页,用户登录才能访问</span>
</body>
</html>
login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>登录</title>
</head>
<body>
<form action="${pageContext.request.contextPath}/user/login" method="post">
<label> 用户名<input type="text" name="username" required> </label>
<br>
<label> 密码<input type="password" name="password" required> </label>
<br>
<input type="submit">
</form>
</body>
</html>
success.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>登录成功</title>
</head>
<body>
<h1>登录成功:${userInfo}</h1>
<h3><a href="${pageContext.request.contextPath}/user/toHome">进入首页</a></h3>
<h3><a href="${pageContext.request.contextPath}/user/logOut">注销</a></h3>
</body>
</html>
Controller 和基本的页面都写好了,此时所有的页面都可以直接访问。
- 我们想要的效果是:
- 用户登入成功,将用户信息存入 session,并跳转到成功页面;
- 如果用户未登入,就无法进入用户首页;
- 因此需要使用拦截器。
10.5.3、拦截器
LoginInterceptor
通过 session 判断用户是否已登录。
- 用户登录:放行;
- 用户未登录:重定向到登录页。
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String userInfo = (String) request.getSession().getAttribute("userInfo");
System.out.println("loginInfo = " + userInfo);
if (userInfo != null) {
System.out.println("用户已登录,放行");
return true;
} else {
response.sendRedirect(request.getContextPath() + "/user/toLogin");
}
return false;
}
}
配置
- 需要配置
不拦截地址
,否则 /user 下的所有请求,包括登录也会被拦截; - 如果登录请求也拦截了,此时还没有登入,就不会存入 session,则一定会被拦截。
<mvc:interceptor>
<mvc:mapping path="/user/**"/>
<!-- 不拦截地址 -->
<mvc:exclude-mapping path="/user/toLogin"/>
<mvc:exclude-mapping path="/user/login"/>
<bean class="indi.jaywee.interceptor.LoginInterceptor"/>
</mvc:interceptor>
再次测试
- 用户登入成功,将用户信息存入 session,并跳转到成功页面;
- 如果用户未登入,访问用户首页时会重定向到登录请求;
拦截器是 AOP 思想的一个体现,可以在不改变业务代码的情况下扩展业务。