Spring MVC使用篇(十三)—— 拦截器

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_REQUEST0代表只有在request中可以访问
SCOPE_SESSION1如果环境下允许它代表一个局部的隔离的session,否则就代表普通的session,并且在该session范围内可以访问
导SCOPE_GLOBAL_SESSION2如果环境允许,它代表一个全局共享的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 后在等路表单中输入如下信息:
在这里插入图片描述
  由于并没有输入密码,因此一定会提示登录失败,并跳转回登录页面,但是此时跳转回来后,用户之前输入的用户编号和用户名信息仍然存在,具体如下所示:
在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值