学完了SpringMVC的数据绑定,接下来学习SpringMVC的拦截器。本篇主要介绍SpringMVC的拦截器配置及应用,同时也会介绍拦截器和过滤器之间的区别,以及明白SpringMVC拦截器在项目开发过程中的重要性。

接下来通过这张图来回忆之前关于SpringMVC的工作流程:

里面有一个非常重要的组件–前端控制器(DispatchServlet),也称中央控制器,因为它负责接收和响应http请求并协调SpringMVC各个组件完成请求处理工作。和其他Servlet一样,用户必须在web.xml文件中必须配置DispatchServlet,也就是说DispatchServlet的配置是开发SpringMVC拦截器的首要工作。

环境搭建

使用Maven新建一个webapp项目,名称为springmvc_interceptor。毫无疑问第一步都是配置pom.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependencies>
<!--springmvc相关-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.3.9.RELEASE</version>
</dependency>

<!--Junit测试-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>

但是第二步配置DispatcherServlet有三种方式,之前使用的都是在web.xml中使用<init-param>参数并设置contextConfigLocation的形式,其实这只是一种,现在分别介绍这三种方式:

第一种:新建[servlet-name]-servlet.xml,比如springmvc-servlet.xml形式。在WEB-INF文件夹下面新建springmvc-servlet.xml文件,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

<!--配置自定义扫描包-->
<context:component-scan base-package="com.envy"></context:component-scan>

<!--映射物理路径或者说是配置视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<!--配置映射前缀-->
<property name="prefix" value="/WEB-INF/pages/"></property>
<!--配置映射后缀-->
<property name="suffix" value=".jsp"></property>
</bean>
</beans>

接着新建index.jsp,注意是在WEB-INF/pages文件夹下面:

index.jsp页面内的代码为:

1
2
3
4
5
6
7
8
9
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>SpringMVC测试</title>
</head>
<body>
<h1>SpringMVC测试</h1>
</body>
</html>

然后在controller包内(目录结构如上图所示)新建IndexController.java文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.envy.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class IndexController {

@RequestMapping(value = "/index")
public String index(){
return "index";
}
}

最后运行项目,在浏览器地址栏中输入http://localhost:8080/index,页面将会显示“SpringMVC测试”字眼。

第二种:改变命名空间namespace,其实这个易仅仅是在第一种的基础上添加了一个init-param参数:

1
2
3
4
5
6
7
8
9
<!--SpringMVC核心Servlet配置-->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>namespace</param-name>
<param-value>hello</param-value>
</init-param>
</servlet>

注意一旦设置了namespace属性,那么你在WEB-INF文件夹下面定义的xml配置文件就需要修改为hello.xml:

最后运行项目,在浏览器地址栏中输入http://localhost:8080/index,页面同样也会显示“SpringMVC测试”字眼。

不过前面两种方式还有一个条件就是配置xml文件必须放在WEB-INF文件夹下面,否则系统无法识别,这种方式太古板了,不建议大家使用。

第三种:也就是前面多次使用到的<init-param>参数并设置contextConfigLocation的形式:

1
2
3
4
5
6
7
8
9
<!--SpringMVC核心Servlet配置-->
<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:springmvc.xml</param-value>
</init-param>
</servlet>

通过这种方式只需要将springmvc.xml文件放在respurces文件夹下面,这样便于集中管理,个人推崇于使用这种方式。

拦截器与过滤器

拦截器(Inteceptor)

拦截器,依赖于web框架如SpringMVC,不依赖于servlet容器。实现上基于Java反射机制,用于通过统一拦截浏览器向服务器发送的请求并进行增强的机制,如字符编码,权限验证。一般有三个用途:⑴请求未到controller层时进行拦截;⑵请求通过controller层,但未到渲染时图层时进行拦截;⑶结束视图渲染,但是未到servlet层结束时进行拦截。拦截器在对其他的一些如直接访问静态资源的请求则无法进行拦截处理,但在请求权限校验时较为出色。

过滤器(Filter)

过滤器,依赖于servlet容器。在实现上基于函数回调,可以对几乎所有请求进行过滤,缺点是一个过滤器实例只能在容器初始化时调用一次。在请求进入容器后,还未进入Servlet之前进行预处理,并且请求结束返回给前端进行后期处理,只能在容器初始化时调用一次,可以自定义过滤器实现并在web.xml中注册。使用过滤器的目的是用来做一些过滤操作,以获取我们想要获取的数据,如在过滤器中修改字符编码,或者修改HttpServletRequest的一些参数,像过滤低俗文字、危险字符等。类似公路上的收费站。

案例分析

现在有这么一个场景:假设你是一个网站开发人员,正在开发一个后台管理系统,用户可以登陆,修改密码,修改签名,搜索这个四个功能。那么你对应的开发流程应该是怎样的?

第一步,新建User实体类:

1
2
3
private String name;
private String password;
//getter和setter方法

第二步,新建UserController类并定义三个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Controller
@RequestMapping(value = "/user")
public class UserController {

//搜索
@RequestMapping(value = "/search")
public String search(){
return "search";
}

//修改密码
@RequestMapping(value = "/updatePwd")
public String updatePwd(){
return "updatePwd";
}

//修改签名
@RequestMapping(value = "/updateSig")
public String updateSig(){
return "updateSig";
}
}

第三步,新建LoginController类并定义两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Controller
public class LoginController {

@RequestMapping(value = "/login")
public String login(){
return "login";
}


@RequestMapping(value = "/logined")
public String logined(@RequestParam("name")String name, @RequestParam("password")String password,
HttpSession session){
if("envy".equals(name)&&"1234".equals(password)){
User user = new User();
user.setName(name);
user.setPassword(password);
session.setAttribute("USER",user);
return "redirect:user/search";
}else{
return "redirect:login";
}
}
}

第四步,在WEB-INF/pages文件夹下面新建4个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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
*******************login.jsp**********************

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>登录页面</title>
</head>
<body>
<h1>用户登录页面</h1>
<div>
<form action="/logined" method="post">
<p>账号:<input type="text" name="name" placeholder="请输入用户账号"></p>
<p>密码:<input type="password" name="password" placeholder="请输入用户密码"></p>
<button type="submit">提交</button>
</form>
</div>
</body>
</html>

*******************search.jsp**********************

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>search</title>
</head>
<body>
<h1>this page is search!<p>当前登录用户为:${USER.name}</p></h1>
</body>
</html>

*******************updatePwd.jsp**********************

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>updatePwd</title>
</head>
<body>
<h1>this page is updatePwd!</h1>
</body>
</html>

*******************updateSig.jsp**********************

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>updateSig</title>
</head>
<body>
<h1>the page is updateSig!</h1>
</body>
</html>

然后运行项目,输入账号和密码,发现自动跳转到search页面,并正确显示当前登录用户为:envy,但是当你重启系统后没有登录,直接访问http://localhost:8080/user/search时确实可行的,不过仅仅只显示当前登录用户为:,这种结果是不行的,个人用户模块只有登录才能进行查看,因此我们需要对user下面的所有方法都进行登录校验。你可能会这样操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
@RequestMapping(value = "/user")
public class UserController {

@RequestMapping(value = "/search")
public String search(HttpSession session){
User user = (User)session.getAttribute("USER");
if(user==null){
return "redirect:../login";
}else{
return "search";
}
}
}

这样做确实可以,但是这样使得每一个方法里面都需要添加用户是否登录的判断逻辑,这和之前把Login单独列出来作为一个controller是相违背的,因此这种方式不是很明智。其实这时候就能使用到AOP了,面向切面编程,在SpringMVC中就是拦截器。

SpringMVC可以使用拦截器对请求(其实是方法)进行拦截,用户可以自定义拦截器来实现特定的功能,自定义的拦截器必须实现HandleInterceptor接口。

(1)、preHandle():这个方法在业务处理器处理请求之前被调用,在该方法中对用户请求request进行处理。如果程序员决定该拦截器对请求进行拦截处理后还要调用其他的拦截器,或者是业务处理器去进行处理,则返回true;如果程序员决定该不需要调用其他的组件去处理请求,则返回false。

(2)、postHandle():这个方法在业务处理器处理完请求后,但是DispatcherServlet向客户端返回响应前被调用,在该方法中对用户请求request进行处理。

(3)、afterCompletion():这个方法在DispatcherServlet完全处理完请求后被调用,可以在该方法中进行一些资源清理的操作。

新建handler包和LoginHandler类,注意这个类是拦截器,需要实现HandlerInterceptor接口,并重写其中的三个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LoginHandler implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
System.out.println("1、interceptor*****preHandle");
return true; //如果这个方法返回false,则表明后续操作不会进行
}

@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
System.out.println("3、interceptor*****postHandle");
}

@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
System.out.println("4、interceptor*****afterCompletion");
}
}

写好拦截器类以后,接下里就是在springmvc.xml配置文件中将刚才写的拦截器类进行注册:

1
2
3
4
5
6
7
8
9
<!--拦截器的注册-->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/user/search"/>
<mvc:mapping path="/user/updatePwd"/>
<mvc:mapping path="/user/updateSig"/>
<bean class="com.envy.handler.LoginHandler"></bean>
</mvc:interceptor>
</mvc:interceptors>

然后为了验证前面介绍的拦截类中三个方法的执行顺序,在UserControler类中按照图示进行配置:

接着打上断点,运行项目,在浏览器地址栏中输入http://localhost:8080/user/searchhttp://localhost:8080/user/updatePwdhttp://localhost:8080/user/updateSig发现控制台输出的顺序为:

1
2
3
4
5
6
7
8
9
10
11
12
1、interceptor*****preHandle
2、interceptor*****search
3、interceptor*****postHandle
4、interceptor*****afterCompletion
1、interceptor*****preHandle
interceptor*****updatePwd
3、interceptor*****postHandle
4、interceptor*****afterCompletion
1、interceptor*****preHandle
interceptor*****updateSig
3、interceptor*****postHandle
4、interceptor*****afterCompletion

其实在前面配置的时候,我就已经给其进行了编号,目的就是让大家有一个更清醒的认识。看到没有执行的顺序都为:preHandle-->业务方法-->postHandle-->afterCompletion(前提是preHandle方法返回结果为true,如果是false则仅仅只执行preHandle方法)。

既然这样接下来完善preHandle方法中的逻辑:

1
2
3
4
5
6
7
8
9
10
11
    @Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
User user = (User)httpServletRequest.getSession().getAttribute("USER");
if(user==null){
System.out.println("1、interceptor*****preHandle");
httpServletResponse.sendRedirect(httpServletRequest.getContextPath()+"/login");
// httpServletResponse.sendRedirect("../login"); //或者是下面这种方式
return false;
}
return true; //如果这个方法返回false,则表明后续操作不会进行
}

运行发现用户如果不登录直接访问http://localhost:8080/user/search则会跳转至登录页面。

在前面我们使用SpringMVC拦截器仅仅只是拦截了三个方法,现在加入需要对用户模块的所有方法进行拦截呢?此时可以借助于通配符进行设置:

1
2
3
4
5
6
7
<!--拦截器的注册-->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/user/*"/>
<bean class="com.envy.handler.LoginHandler"></bean>
</mvc:interceptor>
</mvc:interceptors>

但是那样还是有一个问题,就是它只能匹配下一级,无法匹配孙子级,如只能匹配http://localhost:8080/user/search,而无法匹配http://localhost:8080/user/search/center。其实很简单,只需要添加两个通配符即可:

1
2
3
4
5
6
7
<!--拦截器的注册-->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/user/**"/>
<bean class="com.envy.handler.LoginHandler"></bean>
</mvc:interceptor>
</mvc:interceptors>

因此建议大家一般在设置的时候尽量使用两个通配符的形式。接下来介绍另一个<mvc:exclude-mapping>,一般这个是结合通配符使用的,exclude-mapping在所有拦截中进行排除,一般在通配符会有意义,目的就是过虑掉通配符中一些不必要的链接。尝试将拦截器相关配置修改为:

1
2
3
4
5
6
7
8
<!--拦截器的注册-->
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/user/*"></mvc:mapping>
<mvc:exclude-mapping path="/user/updatePwd"></mvc:exclude-mapping>
<bean class="com.envy.handler.LoginHandler"></bean>
</mvc:interceptor>
</mvc:interceptors>

然后运行项目,在浏览器地址栏中输入http://localhost:8080/user/searchhttp://localhost:8080/user/updatePwdhttp://localhost:8080/user/updateSig会发现只有中间一个可以访问,其余都被跳转到登录页面。

当然你还可以设置多个拦截器,只是执行顺序如下图所示:(假设第一个拦截器在前面,第二个在下面)

这样更清楚一点:

假设现在存在两个拦截器,且同时都是对user/search方法进行拦截,那么四种情况对应的方法执行顺序如下:
第一种:第一个拦截器的preHandle方法返回为true,第二个拦截器的preHandle方法返回为true,则调用的方法顺序为:FirstInterceptor#preHandler—>SecondInterceptor#preHandler—>业务方法search—>SecondInterceptor#postHandler—>FirstInterceptor#postHandler—>SecondInterceptor#afterCompletion—>FirstInterceptor#afterCompletion,图中虚线所示:

第二种:第一个拦截器的preHandle方法返回为true,第二个拦截器的preHandle方法返回为false,则调用的方法顺序为:FirstInterceptor#preHandler--->SecondInterceptor#preHandler--->FirstInterceptor#afterCompletion,图中实线所示:

第三种:第一个拦截器的preHandle方法返回为false,第二个拦截器的preHandle方法返回为true,则调用的方法顺序为,仅仅只调用FirstInterceptor#preHandler方法。

第四种:第一个拦截器的preHandle方法返回为false,第二个拦截器的preHandle方法返回为false,大家想一下结果肯定和第三种是一样的,因为请求过不去。

总结

拦截器是使用JDK动态代理实现的,拦截的是调用的对应方法;过滤器是使用Filter实现的,拦截的是request对象。