SpringMvc 拦截器开发和使用总结

我们在开发 SpringMvc 网站或接口时,肯定会遇到这样的情况:有些页面或者接口时需要登录后才能访问的,或者需要有权限才能访问的,在不改变原有 Controller 方法代码的情况下,使用 SpringMvc 拦截器是一个很不错的选择。

SpringMvc 的拦截器也是 Aop 切面编程思想的一种体现。SpringMvc 拦截器非常类似于 Servlet 的过滤器。两者之间的区别在于:过滤器依赖于 Servlet 容器,在实现上是基于函数的回调,只能在 Servlet 请求的前后起作用。拦截器是依赖于 SpringMvc 框架,在实现上是基于 Java 的反射机制,可以使用 Spring 的依赖注入的任何资源、事务管理、异常处理等相关技术,比 Servlet 的功能更加强大。因此在使用 SpringMvc 框架构建的应用程序中,优先使用拦截器。

本篇博客采用纯注解的方式编写简单的 Demo ,通过代码的方式展示 SpringMvc 拦截器的运行过程,以及演示如何采用拦截器解决实际开发中验证权限的处理方案。在本篇博客的最后会提供源代码的下载地址。


一、搭建工程

新建一个 maven 项目,导入相关 jar 包,我所导入的 jar 包都是最新的,内容如下:

有关具体的 jar 包地址,可以在 https://mvnrepository.com 上进行查询。

<dependencies>
    <!--导入 servlet 相关的 jar 包-->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>4.0.1</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>javax.servlet.jsp</groupId>
        <artifactId>jsp-api</artifactId>
        <version>2.2</version>
        <scope>provided</scope>
    </dependency>

    <!--导入 Spring 核心 jar 包-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.3.18</version>
    </dependency>
    <!--导入 SpringMvc 的 jar 包-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.3.18</version>
    </dependency>

    <!--导入 jackson 相关的 jar 包-->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.13.1</version>
    </dependency>
</dependencies>

配置好引用的 jar 包后,打开右侧的 Maven 窗口,刷新一下,这样 Maven 会自动下载所需的 jar 包文件。

搭建好的项目工程整体目录比较简单,具体如下图所示:

image

com.jobs.config 包下存储的是 SpringMvc 的配置文件和 Servlet 的初始化文件
com.jobs.controller 包下存储的是用于提供 api 接口的类
com.jobs.domain 包下存储的是 JavaBean 实体类

web 目录下放置的是网站文件,只有一个静态页面和一些 js 文件


二、SpringMvc 配置相关

com.jobs.config 下的 SpringMvcConfig 类是 SpringMvc 的配置类,具体内容如下:

package com.jobs.config;

import com.jobs.interceptor.MyInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.JstlView;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

@Configuration
@ComponentScan("com.jobs")
//启用 mvc 功能,配置了该注解之后,SpringMvc 拦截器放行相关资源的设置,才会生效
@EnableWebMvc
public class SpringMvcConfig implements WebMvcConfigurer {

    //配置 SpringMvc 连接器放行常用资源的格式(图片,js,css)
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    //配置响应数据格式所对应的数据处理转换器
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {

        //如果响应的是 application/json ,则使用 jackson 转换器进行自动处理
        MappingJackson2HttpMessageConverter jsonConverter =
                        new MappingJackson2HttpMessageConverter();
        jsonConverter.setDefaultCharset(Charset.forName("UTF-8"));
        List<MediaType> typelist1 = new ArrayList<>();
        typelist1.add(MediaType.APPLICATION_JSON);
        jsonConverter.setSupportedMediaTypes(typelist1);
        converters.add(jsonConverter);

        //如果响应的是 text/html 和 text/plain ,则使用字符串文本转换器自动处理
        StringHttpMessageConverter stringConverter = new StringHttpMessageConverter();
        stringConverter.setDefaultCharset(Charset.forName("UTF-8"));
        List<MediaType> typelist2 = new ArrayList<>();
        typelist2.add(MediaType.TEXT_HTML);
        typelist2.add(MediaType.TEXT_PLAIN);
        stringConverter.setSupportedMediaTypes(typelist2);
        converters.add(stringConverter);
    }

    //配置拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //添加拦截器(可以添加多个拦截器,拦截器的执行顺序,就是添加顺序)
        MyInterceptor myInterceptor = new MyInterceptor();
        //设置拦截器拦截的请求路径(此处配置拦截 test 下的所有请求)
        registry.addInterceptor(myInterceptor).addPathPatterns("/test/*");
        //设置拦截器排除的拦截路径
        //registry.addInterceptor(testInterceptor).excludePathPatterns("/");

        /*
        设置拦截器的拦截路径,支持 * 和 ** 通配
        配置值 /**         表示拦截所有映射
        配置值 /*          表示拦截所有 / 开头的映射
        配置值 /test/*     表示拦截所有 /test/ 开头的映射
        配置值 /test/get*  表示拦截所有 /test/ 开头,且具体映射名称以 get 开头的映射
        配置值 /test/*job  表示拦截所有 /test/ 开头,且具体映射名称以 job 结尾的映射
        */
    }

    //注解配置 SpringMvc 返回配置的字符串所表示的页面,从哪些去找
    //可以注释掉下面的方法,这样需要在 SpringMvc 方法返回时,指定全局路径的页面地址
    //这里配置的是:根据 SpringMvc 方法返回的字符串,到 /WEB-INF/pages/ 下找对应名称的 jsp 页面
    @Bean
    public InternalResourceViewResolver getViewResolver() {
        InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
        viewResolver.setPrefix("/WEB-INF/pages/");
        viewResolver.setSuffix(".jsp");
        //如果页面需要使用JSTL标签库的话
        //viewResolver.setViewClass(JstlView.class);
        return viewResolver;
    }
}

如上面的代码所示,在 SpringMvc 的配置类中通过重写 addInterceptors 方法来添加自定义的拦截器,设置拦截器生效的请求地址和忽略的请求地址。根据实际业务需要,可以添加多个拦截器,拦截器的执行顺序依赖于添加的顺序。

ServletInitConfig 类初始化 Servlet 容器,装载 SpringMvc 的配置,具体如下:

package com.jobs.config;

import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.support.AbstractDispatcherServletInitializer;

import javax.servlet.*;
import java.util.EnumSet;

public class ServletInitConfig extends AbstractDispatcherServletInitializer {

    //初始化 Servlet 容器,加载 SpringMvc 配置类
    //创建 web 专用的 Spring 容器对象:WebApplicationContext
    @Override
    protected WebApplicationContext createServletApplicationContext() {
        AnnotationConfigWebApplicationContext cwa = new AnnotationConfigWebApplicationContext();
        cwa.register(SpringMvcConfig.class);
        return cwa;
    }

    //注解配置 SpringMvc 的 DispatcherServlet 拦截地址,拦截所有请求
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

    @Override
    protected WebApplicationContext createRootApplicationContext() {
        return null;
    }

    //添加过滤器
    @Override
    protected Filter[] getServletFilters() {
        //采用 utf-8 作为统一请求的编码
        CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
        characterEncodingFilter.setEncoding("UTF-8");

        //该过滤器,能够让 web 页面通过 _method 参数将 Post 请求转换为 Put、Delete 等请求
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
        return new Filter[]{characterEncodingFilter, hiddenHttpMethodFilter};
    }
}

三、拦截器开发细节

首先列出 domain 包下的 MyResult 类的细节,主要用来定义返回的 Json 数据结构:

package com.jobs.domain;

public class MyResult {

    private Integer status;
    private String msg;

    public MyResult() {
    }

    public MyResult(Integer status, String msg) {
        this.status = status;
        this.msg = msg;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    @Override
    public String toString() {
        return "MyResult{" +
                "status=" + status +
                ", msg='" + msg + '\'' +
                '}';
    }
}

编写 TestController 类用来接收请求,验证拦截器的功能开发,内容如下:

package com.jobs.controller;

import com.jobs.domain.MyResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/test")
@RestController
public class TestController {

    //测试 SpringMvc 拦截器的执行流程

    //该请求可以成功运行,在控制台打印出拦截器执行的全过程
    @RequestMapping("/success")
    public MyResult testInterceptor1() {
        System.out.println("Controller 中的 success 请求执行...");
        MyResult result = new MyResult(0, "接口请求成功");
        //拦截器的 3 个方法 preHandle、postHandle、afterCompletion 都运行完毕后,
        //才会运行 return 语句,返回结果给调用者
        return result;
    }


    //该请求不能成功执行,因为会被 MyInterceptor 拦截器给拦截了,会跳转到 fail.jsp 页面
    @RequestMapping("/fail")
    public MyResult testInterceptor2() {
        //下面的代码,由于拦截器拦截了,所以不会执行。
        System.out.println("Controller 中的 fail 请求执行...");
        MyResult result = new MyResult(0, "接口请求成功");
        return result;
    }

    //模拟用户没有权限访问该资源
    //如果是 get 请求,将跳转到 fail.jsp 页面
    //如果是 post 或 其它请求方式,将返回 json 数据(内容为没有权限,不允许访问)
    @RequestMapping("/deny")
    public MyResult testInterceptor3()
    {
        //下面的代码,由于拦截器拦截了,所以不会执行。
        System.out.println("Controller 中的 deny 请求执行...");
        MyResult result = new MyResult(0, "接口请求成功");
        return result;
    }
}

下面列出拦截器 MyInterceptor 的开发细节,需要实现 HandlerInterceptor 接口即可,内容如下:

package com.jobs.interceptor;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.jobs.domain.MyResult;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

//该拦截器被配置为拦截 /test1 下的所有请求
public class MyInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        String uri = request.getRequestURI();
        if (uri.contains("success")) {
            System.out.println("拦截器前置执行...放行 /test1/success 请求");
            return true;
        } else if (uri.contains("fail")) {
            System.out.println("拦截器前置执行...拦截 /test1/fail 请求");
            request.setAttribute("data",
                        "我请求 /test1/fail,被 MyInterceptor 拦截器拦截,跳转到这里的");
            //跳转到指定页面
            request.getRequestDispatcher("/WEB-INF/pages/fail.jsp").forward(request, response);
            //此处返回 false 后,将不会再执行 postHandle 和 afterCompletion
            return false;
        } else {
            System.out.println("拦截器前置执行...拦截 "
                   + uri + " 请求,当前的请求方式为:" + request.getMethod());
            //获取用户的请求方式 get,post,put,delete 等等
            String method = request.getMethod().toLowerCase();
            if ("get".equals(method)) {
                //如果是 get 请求的话,则直接跳转到相关页面
                request.setAttribute("data", "我请求 "
                        + uri + ",被 MyInterceptor 拦截器拦截,跳转到这里的");
                //跳转到指定页面
                request.getRequestDispatcher("/WEB-INF/pages/fail.jsp").forward(request, response);
            } else {
                //如果是 post,put,delete 等相关请求,则直接返回 json 数据
                MyResult result = new MyResult(1, "你没有权限,不能访问");
                //返回 json数据
                ObjectMapper objectMapper = new ObjectMapper();
                String json = objectMapper.writeValueAsString(result);
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write(json);
            }

            //此处返回 false 后,将不会再执行 postHandle 和 afterCompletion
            return false;
        }
    }

    @Override
    public void postHandle(HttpServletRequest request,
                           HttpServletResponse response,
                           Object handler,
                           ModelAndView modelAndView) throws Exception {
        System.out.println("拦截器后置执行...");
    }

    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler,
                                Exception ex) throws Exception {
        System.out.println("拦截器执行完毕...");
    }
}

四、验证拦截器的页面细节

网站的主页 index.html 静态页面,以及所引用的 js 文件的内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>SpringMvc拦截器演示</title>
</head>
<body>
    <h1>该 Demo 演示 SpringMvc 拦截器的使用</h1>
    请求 /test/success 接口,从控制台可以看到拦截器执行的全过程:<br/>
    <a href="/test/success">请求test下的success接口</a><br/>
    <hr/>
    请求 /test/fail 接口,被拦截器拦截,会跳转到 fail.jsp 页面,从控制台能够看到拦截器的执行过程<br/>
    <a href="/test/fail">请求test下的fail接口</a><br/>
    <hr/>
    请求 /test/deny 模拟用户没有权限,不能访问的应用场景。<br>
    如果是 get 请求的话,会跳转到 fail.jsp 页面:<br/>
    <a href="/test/deny">get请求test下的deny接口</a><br/>
    如果是非 get 请求的化,会返回 json 数据,告诉用户没有访问权限:<br/>
    <a href="javascript:void(0);" id="test1">Post请求test下的deny接口</a><br/>
    <a href="javascript:void(0);" id="test2">Put请求test下的deny接口</a><br/>
    <a href="javascript:void(0);" id="test3">Delete请求test下的deny接口</a><br/>

    <script src="./js/jquery-3.6.0.min.js"></script>
    <script src="./js/apitest.js"></script>
</body>
</html>
$(function () {
    $('#test1').click(function () {
        $.ajax({
            type: "post",
            url: "/test/deny",
            //如果没有指定 dataType ,
            //则服务器会自动根据接口返回的 mime 类型,推断返回的数据类型
            success: function (data) {
                alert("post请求返回的数据:" + data.status + "," + data.msg);
            }
        });
    });

    $('#test2').click(function () {
        $.ajax({
            type: "post",
            url: "/test/deny",
            data: {_method: "put"},
            dataType: "json", //服务器接口返回的是 json 类型的数据
            success: function (data) {
                alert("put请求返回的数据:" + data.status + "," + data.msg);
            }
        });
    });

    $('#test3').click(function () {
        $.ajax({
            type: "post",
            url: "/test/deny",
            data: {_method: "delete"},
            dataType: "json", //服务器接口返回的是 json 类型的数据
            success: function (data) {
                alert("delete请求返回的数据:" + data.status + "," + data.msg);
            }
        });
    });
})

Get 请求被拦截后,跳转的页面 fail.jsp 内容为:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<h1>这里是 fail.jsp 页面,拦截器未放行后,会跳转到该页面</h1>
获取到的 data 值为:${data}
</body>
</html>

然后运行网站,在首页 index.html 静态页面中,即可进行拦截器的测试。

如果拦截器正常全部运行的话,执行的顺序为:

preHandle ---> controller 的方法 ---> postHandle -> afterCompletion ---> 返回结果给调用者

如果在拦截器的 preHandle 方法中返回 false ,则后面的方法都不会执行。

此时在 preHandle 方法中可以跳转页面或者直接返回数据给调用者。



到此为止,有关从 SpringMvc 拦截器的开发使用,已经介绍完毕。希望对大家有所帮助。

本篇博客的 Demo 源代码下载地址为:https://files.cnblogs.com/files/blogs/699532/SpringMvc_Interceptor.zip

posted @ 2022-04-09 15:26  乔京飞  阅读(9746)  评论(0编辑  收藏  举报