我们在开发 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 包文件。
搭建好的项目工程整体目录比较简单,具体如下图所示:
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 拦截器的开发使用,已经介绍完毕。希望对大家有所帮助。