在实际项目中,拦截器的使用是非常普遍的,例如在购物网站中通过拦截器可以拦截未登录的用户,禁止其购买商品,或者使用它来验证已登录用户是否有相应的操作权限等。 在 Struts 2 框架中,拦截器是其重要的组成部分,而 Spring MVC 中也提供了拦截器功能,通过配置即可对请求进行拦截处理。 本章将针对 Spring MVC 中拦截器的使用进行详细讲解。
拦截器概述
Spring MVC 中的拦截器( Interceptor )类似于 Servlet 中的过滤器( Filter) ,它主要用于拦截用户请求并做相应的处理。 例如通过拦截器可以进行权限验证、记录请求信息的日志、判断用户是否登录等。
- 拦截器的定义
要使用 Spring MVC 中的拦截器,就需要对拦截器类进行定义和配置。 通常拦截器类可以通过两种方式来定义。 一种是通过实现 Handlerlnterceptor 接口,或继承 Handlerlnterceptor 接口的实现类(如 HandlerlnterceptorAdapter )来定义;另一种是通过实现 WebRequestlnterceptor 接口,或继承 WebRequestlnterceptor 接口的实现类来定义。
以实现 Handlerlnterceptor 接口的定义方式为例,自定义拦截器类的代码如下所示。public class Customlnterceptor implements HandlerInterceptor { @Override public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) throws Exception { } @Override public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3) throws Exception { } @Override public boolean preHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2) throws Exception { return false; } }
从上述代码可以看出,自定义的拦截器类实现了 Handlerlnterceptor 接口,并实现了接口中的三个方法。 关于这三个方法的具体描述如下。
- preHandler() 方法:该方法会在控制器方法前执行,其返回值表示是否中断后续操作。 当 其返回值为 true 时,表示继续向下执行;当其返回值为 false 时,会中断后续的所有操作(包括调用下一个拦截器和控制器类中的方法执行等) 。
- postHandle()方法:该方法会在控制器方法调用之后,且解析视图之前执行。 可以通过此 方法对请求域中的模型和视图做出进一步的修改。
- afterCompletion()方法: 该方法会在整个请求完成,即视图渲染结束之后执行。 可以通过此方法实现一些资源清理、记录曰志信息等工作。
- 拦截器的配置
要使自定义的拦截器类生效,还需要在 Spring MVC 的配置文件中进行配置,配置代码如下所示。
<!-- 配置拦截器 --> <mvc:interceptors> <!-- 使用bean直接定义<mvc:interceptors>下面的Interceptor将拦截所有请求 --> <bean class="com.neuedu.interceptor.CustomInterceptor"/> <!-- 拦截器1 --> <mvc:interceptor> <!-- 配置拦截器作用的路径 --> <mvc:mapping path="/**"/> <!-- 配置不需要拦截器作用的路径 --> <mvc:exclude-mapping path=""/> <!-- 定义在<mvc:interceptor>下面的,表示对匹配路径的请求才进行拦截 --> <bean class="com.neuedu.interceptor.Interceptor1" /> </mvc:interceptor> <!-- 拦截器2 --> <mvc:interceptor> <mvc:mapping path="/hello"/> <bean class="com.neuedu.interceptor.Interceptor2"/> </mvc:interceptor> ... </mvc:interceptors>
在上述代码中, <mvc:interceptors>元素用于配置一组拦截器,其子元素<bean> 中定义的是全局拦截器,它会拦截所有的请求;而<mvc:interceptor>元素中定义的是指定路径的拦截器, 它会对指定路径下的请求生效。 <mvc:interceptor>元素的子元素<mvc:mapping>用于配置拦截器作用的路径,该路径在其属性 path 中定义。 如上述代码中 path 的属性值"/**"表示拦截所有路径,"/hello" 表示拦截所有以 "/hello" 结尾的路径。 如果在请求路径中包含不需要拦截的内容,还可以通过<mvc:exclude-mapping>元素进行配置。
需要注意的是, <mvc:interceptor> 中的子元素必须按照上述代码的配置顺序进行编写, 即 <mvc:mapping ... /> ——> <mvc:exclude-mapping ... />——><bean ... />的顺序,否则文件会报错。
拦截器的执行流程
- 单个拦截嚣的执行流程
在运行程序时,拦截器的执行是有一定顺序的,该顺序与配置文件中所定义的拦截器的顺序相关。 如果在项目中只定义了一个拦截器,那么该拦截器在程序中的执行流程如图所示。
从图中可以看出,程序首先会执行拦截器类中的 preHandle() 方法,如果该方法的返回值为 true ,则程序会继续向下执行处理器中的方法,否则将不再向 下执行;在业务处理器 ( 即控制器 Controller 类)处理完请求后, 会执行 postHandle() 方法,然后会通过 DispatcherServlet 向客户端返回响应 ; 在 DispatcherServlet 处理完请求后,才会执行 afterCompletion() 方法。
为了验证上面所讲解的拦截器执行流程,下面通过一个案例来演示其使用,具体步骤如下。
( 1 ) 在 Eclipse 中,创建一个名为 springmvc05 的 Web 项目,将 Spring MVC 程序运行所需 JAR 包复制到项目的 lib 目录中,并发布到类路径下。
( 2 ) 在 web.xml 中,配置 Spring MVC 的前端过滤器和初始化加载配置文件等信息。
( 3 )在 src 目录下,创建一个 com.neuedu.controller 包,并在包中创建控制器类 HelloController,编辑后的代码文件如下所示。package com.neuedu.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller public class HelloController { /** * 页面跳转 */ @RequestMapping("/hello") public String Hello(){ System.out.println("Hello!"); return "success"; } }
( 4 )在 src 目录下 ,创建一个 com.neuedu.interceptor 包,并在包中创建拦截器类 Customlnterceptor。 该类需要实现 Handlerlnterceptor 接口,并且在实现方法中需要编写输出语句来输出信息,文件如下所示。
package com.neuedu.interceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; /** * 实现 了 HandlerInterceptor 接口的自定义拦截器类 */ public class Customlnterceptor implements HandlerInterceptor { @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object object, Exception exception) throws Exception { System.out.println("Customlnterceptor...afterCompletion"); } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object object, ModelAndView modelAndView) throws Exception { System.out.println("Customlnterceptor...postHandle"); } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception { System.out.println("Customlnterceptor...preHandle"); //对拦截的请求进行放行处理 return true; } }
( 5 ) 在 src 目录下 ,创建并配置 Spring MVC 的配置文件,文件如下所示。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:mvc="http://www.springframework.org/schema/mvc" 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-4.3.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd"> <!-- 定义组件扫描器,指定需要扫描的包 --> <context:component-scan base-package="com.neuedu.controller" /> <!-- 配置视图解析器 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <!-- 设置前缀 --> <property name="prefix" value="/WEB-INF/jsp/" /> <!-- 设置后缀 --> <property name="suffix" value=".jsp" /> </bean> <!-- 配置拦截器 --> <mvc:interceptors> <!-- 使用bean直接定义<mvc:interceptors>下面的Interceptor将拦截所有请求 --> <bean class="com.neuedu.interceptor.CustomInterceptor"/> </mvc:interceptors> </beans>
由于配置拦截器使用的是<mvc:interceptors>元素,所以需要配置 mvc 的 schema 信息。 本案例演示的是单个拦截器的执行顺序,所以这里只配置了一个全局的拦截器。
( 6 )在 WEB-INF 目录下,创建一个 jsp 文件夹,并在该文件夹中创建一个页面文件 success.jsp ,然后在页面文件的<body>元素内编写任意显示信息,如 "ok"。
( 7 ) 将项目发布到 Tomcat 服务器并启动,在浏览器中访问地址 http://localhost:8880/springmvc05/hello ,程序正确执行后,浏览器会跳转到 success.jsp 页面,此时控制台的输出结果如图所示。
从图中可以看出,程序先执行了拦截器类中的 preHandle() 方法,然后执行了控制器中 的 Hello() 方法,最后分别执行了拦截器类中的 postHandle() 方法和 afterCompletion() 方法。 这与上文所描述的单个拦截器的执行顺序是一致的。
- 多个拦截器的执行流程
在大型的企业级项目中,通常不会只有一个拦截器,开发人员可能会定义很多拦截器来实现不同的功能。 那么多个拦截器的执行顺序又是怎样的呢?下面通过一张图来描述多个拦截器的执行流程(假设有两个拦截器 Interceptor1 和 Interceptor2 ,并且在配置文件中, Interceptor1 拦截器配置在前),如图所示。
从图中可以看出,当有多个拦截器同时工作时,它们的 preHandle() 方法会按照配置文件中拦截器的配置顺序执行,而它们的 postHandle() 方法和 afterCompletion() 方法则会按照配置顺序的反序执行。
为了验证上述描述,下面通过修改前面小节的案例来演示多个拦截器的执行,具体步骤 如下。
( 1 )在 com.neuedu. interceptor 包中,创建两个拦截器类 Interceptor1 和 Interceptor2 ,这两个拦截器类均实现了 Handlerlnterceptor 接口,并重写其中的方法,文件如下所示。package com.neuedu.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 Interceptor1 implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception { System.out.println("Interceptor1...preHandle"); //对拦截的请求进行放行处理 return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object object, Exception exception) throws Exception { System.out.println("Interceptor1...afterCompletion"); } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object object, ModelAndView modelAndView) throws Exception { System.out.println("Interceptor1...postHandle"); } }
package com.neuedu.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 Interceptor2 implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception { System.out.println("Interceptor2...preHandle"); //对拦截的请求进行放行处理 return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object object, Exception exception) throws Exception { System.out.println("Interceptor2...afterCompletion"); } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object object, ModelAndView modelAndView) throws Exception { System.out.println("Interceptor2...postHandle"); } }
( 2 )在配置文件 springmvc-config.xml 中的<mvc:interceptors>元素内配置上面所定义的两个拦截器,配置代码如下所示。
<!-- 拦截器1 --> <mvc:interceptor> <!-- 配置拦截器作用的路径 --> <mvc:mapping path="/**"/> <!-- 配置不需要拦截器作用的路径 --> <mvc:exclude-mapping path=""/> <!-- 定义在<mvc:interceptor>下面的,表示对匹配路径的请求才进行拦截 --> <bean class="com.neuedu.interceptor.Interceptor1" /> </mvc:interceptor> <!-- 拦截器2 --> <mvc:interceptor> <mvc:mapping path="/hello"/> <bean class="com.neuedu.interceptor.Interceptor2"/> </mvc:interceptor>
在上述拦截器的配置代码中,第一个拦截器会作用于所有路径下的请求,而第二个拦截器会作用于以 "/hello" 结尾的请求。
小提示:为了不影响程序的输出结果,可将上一小节案例中所配置的 Customlnterceptor 的拦截器配置注释掉。
( 3 )发布项目到 Tomcat 服务器并启动,在浏览器中访问地址 http://localhost:8880/springmvc05/hello ,控制台中输出的信息如图所示。
从图中可以看出,程序先执行了前两个拦截器类中的 preHandle() 方法,这两个方法的执行顺序与配置文件中定义的顺序相同;然后执行了控制器类中的 Hello() 方法;最后执行了两个拦截器类中的 postHandle() 方法和 afterCompletion() 方法,且这两个方法的执行顺序与配置文件中所定义的拦截器顺序相反。
应用案例一一实现用户登录权限验证
本小节将通过拦截器来完成一个用户登录权限验证的案例。 本案例中,只有登录后的用户才能访问系统中的主页面,如果没有登录系统而直接访问主页面,则拦截器会将请求拦截,并转发到登录页面,同时在登录页面中给出提示信息。 如果用户名或密码错误,也会在登录页面给出相应的提示信息。 当已登录的用户在系统主页中单击"退出"链接时,系统同样会回到登录页面。 该案例的整个执行流程如图所示。
了解了案例的整个执行流程后,接下来讲解如何在项目中实现用户登录权限验证,具体步骤 如下。
( 1 ) 在 src 目录下, 创建一个 com.neuedu.po 包,并在包中创建 User 类。 在 User 类中,声明 id、 username 和 password 属性,并定义了各个属性的 getter/setter 方法,文件如下所示。package com.neuedu.po; /** * 用户 POJO 类 */ public class User { private Integer id;//id private String username;//用户名 private String password;//密码 public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } 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; } }
( 2 ) 在 com.neuedu.controller 包中, 创建控制器类 UserController,并在该类中定义向主页跳转、 向登录页面跳转、执行用户登录等操作的方法,文件如下所示。
package com.neuedu.controller; import javax.servlet.http.HttpSession; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import com.neuedu.po.User; @Controller public class UserController { /** * 向用户登录页面跳转 */ @RequestMapping(value="/login",method=RequestMethod.GET) public String toLogin(){ return "login"; } /** * 用户登录 */ @RequestMapping(value="/login",method=RequestMethod.POST) public String login(User user,Model model,HttpSession session){ //获取用户名和密码 String username = user.getUsername(); String password = user.getPassword(); //此处模拟从数据库中获取用户名和密码进行判断 if(username != null && username.equals("xiaoxue") && password != null && password.equals("123456")){ //将用户对象添加到 Session session.setAttribute("USER_SESSION", user); //重定向到主页面的跳转方法 return "redirect:main"; } model.addAttribute("msg", "用户名或密码错误,请重新登录!"); return "login"; } /** * 向用户主页面跳转 */ @RequestMapping(value="/main") public String toMain(){ return "main"; } /** * 退出登录 */ @RequestMapping(value = "/logout") public String logout(HttpSession session){ //清除session session.invalidate(); //重定向到登录页面的跳转方法 return "redirect:login"; } }
在文件中,向用户登录页面跳转和用户登录方法的@RequestMapping 注解的 value 属性值相同,但其 method 属性值不同,这是由于跳转到登录页面接收的是 GET 方式提交的方法,而用户登录接收的是 POST 方式提交的方法。 在用户登录方法中,先通过 User 类型的参数获取了用户名和密码,然后通过 if 语句来模拟从数据库中获取到用户名和密码后的判断。 如果存在此用户,就将用户信息保存到 Session 中,并重定向到主页,否则跳转到登录页面。
( 3 )在 com.neuedu.interceptor 包中,创建拦截器类 Loginlnterceptor,编辑后的代码文件如下所示。package com.neuedu.interceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import com.neuedu.po.User; public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object) throws Exception { //获取请求的 URL String url = request.getRequestURI(); //URL:除了login.jsp 是可以公开访问的,其他的URL都进行拦截控制 if(url.indexOf("login")>=0){ return true; } //获取 Session HttpSession session = request.getSession(); User user = (User)session.getAttribute("USER_SESSION"); //判断 Session 中是否有用户数据,如果有则返回true,继续向下执行 if(user != null){ return true; } // 不符合条件的给出提示信息,并转发到登录页面 request.setAttribute("msg", "您还没有登录,请先登录!"); request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request, response); return false; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object object, Exception exception) throws Exception { } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object object, ModelAndView modelAndView) throws Exception { } }
在文件中的 preHandle() 方法中,先获取了请求的 URL,然后通过 indexOf()方法判断 URL 中是否有 "/Iogin" 字符串。 如果有,则返回 true ,即直接放行;如果没有,则继续向下执行拦截处理。 接下来获取了 Session 中的用户信息,如果 Session 中包含用户信息,即表示用户己登录,也直接放行;否则会转发到登录页面,不再执行后续程序。
( 4 )在配置文件的<mvc:interceptors>元素中,配置自定义的登录拦截器信息,代码如下所示。<mvc:interceptor> <mvc:mapping path="/**"/> <bean class="com.neuedu.interceptor.LoginInterceptor" /> </mvc:interceptor>
( 5 )在 WEB-INF 目录下的 jsp 文件夹中,创建一个系统主页面 main.jsp。 在该页面中,使用 EL 表达式获取用户信息,并且通过一个超链接来实现"退出"功能,文件如下所示。
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <!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> 当前用户:${USER_SESSION.username} <a href="logout">退出</a> </body> </html>
( 6 )在 WEB-INF 目录下的 jsp 文件夹中,创建一个登录页面 login.jsp ,在页面中编写一个用于实现登录操作的 form 表单,文件如下所示。
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <!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> ${msg} <form action="login" method="post"> 用户名:<input type="text" name="username"><br> 密 码:<input type="password" name="password"><br> <input type="submit" value="登录"> </form> </body> </html>
( 7 )将项目发布到 Tomcat 服务器并启动,在浏览器中访问地址 http://localhost:8880/springmvc05/main ,其显示效果如图所示。
从图中可以看出,当用户未登录而直接访问主页面肘,访问请求会被登录拦截器拦截, 从而跳转到登录页面,并提示用户未登录信息。 如果在用户名输入框中输入 "jack" ,密码框中输入 "123456" ,当单击"登录"按钮后,浏览器的显示结果如图所示。
当输入正确的用户名气"xiaoxue" 和密码 "123456" ,并单击"登录"按钮后,浏览器会跳转到系统主页面,如图如图所示。
当单击图中的"退出"链接后,用户即可退出当前系统,系统会从主页面重定向到登录页面。
本章小结
本章主要对 Spring MVC 中的拦截器使用进行了详细讲解。 首先介绍了如何在 Spring MVC 项目中定义和配置拦截器,然后详细讲解了单个拦截器和多个拦截器的执行流程,最后通过一个用户登录权限验证的应用案例演示了拦截器的实际应用。 通过本章的学习,大家可以对 Spring MVC 中拦截器的定义和配置方式有一定的了解,能够熟悉拦截器的执行流程,并能够掌握拦截器的使用。