Spring Interceptor 拦截器主要用来拦截用户的请求,执行预定的业务逻辑,例如判断用户是否已经登录。在 SpringMVC 中,当用户请求发送到 Controller 时,在被 Controller 处理之前,它会经过Interceptor拦截器。Spring Interceptor是一个非常类似于Servlet Filter 的概念 。过滤器 Filter 是依赖于Servlet容器,属于Servlet规范的一部分。过滤器是在请求进入Web容器(Tomcat)后,但请求进入servlet之前进行预处理的。
接下来,我们创建一个“SpringBootInterceptorDemo”工程
然后我们修改编码格式以及Maven仓库地址,我们省略这个过程了。
接下来,我们修改 “pom.xml” 文件,添加SpringBoot和Web依赖,如下所示
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.demo</groupId>
<artifactId>SpringBootInterceptorDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.13</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.18</version>
</plugin>
</plugins>
</build>
</project>
接下来,我们创建 Appliaction 入口类文件
package com.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
接下来,我们创建 TestController 控制器
package com.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.Map;
@Controller
@RequestMapping("/test")
public class TestController {
@GetMapping("/hello")
public String hello(Map map){
map.put("message", "hello, world!");
return "test";
}
}
接下来,我们创建 “resources\static\index.html” 入口文件
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>index</title>
</head>
<body>
<a href="/test/hello">/test/hello</a>
<br />
</body>
</html>
接着,我们继续需要创建 “resources\templates\test.html” 文件
<!doctype html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>test</title>
</head>
<body>
<p th:text="${message}"></p>
</body>
</html>
我们运行查看一下效果
接下来,我们创建一个Spring拦截器,我们需要实现 org.springframework.web.servlet.HandlerInterceptor接口 或继承 org.springframework.web.servlet.handler.HandlerInterceptorAdapter类。
package com.demo.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 MyCustomerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
return false;
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
}
}
重写下面下面三个方法
第一,preHandler 方法在请求处理之前被调用,返回Boolean值决定是否执行后续操作。
第二,postHandler 方法在当前请求处理完成之后,但是视图还没有解析。
第三,afterCompletion 方法会在整个请求结束之后执行,可获取响应数据及异常信息。
在这三个方法中有两个重要的参数,HttpServletRequest request 和 HttpServletResponse response 两个对象。他们分别代表用户请求和响应,通过他们可以做很多操作,这里不做详细介绍了。
package com.demo.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 MyCustomerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
System.out.println("第一,拦截器preHandle方法执行");
return true; // 让 Controller 继续执行
}
@Override
public void postHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("第二,拦截器postHandle方法执行");
// 修改视图或数据
modelAndView.addObject("message", "hello, interceptor");
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) throws Exception {
System.out.println("第三,拦截器afterCompletion方法执行");
}
}
请注意,我们在第二个 “postHandle” 方法中对视图做了一个修改。
接下来,我们要注册这个 MyCustomerInterceptor 拦截器。
package com.demo.config;
import com.demo.interceptor.MyCustomerInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyCustomerInterceptor()).addPathPatterns("/test/**");
}
}
这里我们增加了一个Spring配置类,使其继承 WebMvcConfigurer 用于Web应用的定制,然后重写addInterceptors方法来添加我们刚刚创建的 MyCustomerInterceptor 拦截器。那么,这个拦截器到底拦截哪些请求呢?后面的 addPathPatterns() 方法指定要拦截哪些请求,也可以通过excludePathPatterns() 指定不拦截哪些请求。这里,我们拦截 “/test/***” 的请求,两个 “**” 星号代表我们可以匹配类似“/test/aaa/bbb”的请求路径。另外,addPathPatterns和excludePathPatterns可以接受多个请求连接参数。
接下来,我们重新运行测试一下吧
视图数据被我们修改了,我们去控制台查看日志
我们看到三个方法依次执行了。
我们总结一下拦截器执行过程,添加拦截器后,执行Controller的方法之前,请求会先被拦截器拦截住,执行 preHandle 方法。这个方法需要返回个布尔类型的值,如果返回true,就表示放行本次操作,继续访问controller中的方法。如果返回false,则不会放行(controller中的方法也不会执行)。如果返回true放行的话,controller当中的方法执行完毕后,再回过来执行 postHandle 这个方法,最后执行 afterCompletion 方法,执行完毕之后,最终给浏览器返回响应数据。
如果我们想要实现检查用户登录的操作的话,最快捷的办法就是在preHandle方法中抛出自定义异常,然后通过全局异常处理器来返回给客户端信息。我们首先自定义一个登陆异常 “LoginException” 类
package com.demo.exception;
public class LoginException extends RuntimeException {
protected final String message;
// 构造方法
public LoginException(String message) {
this.message = message;
}
@Override
public String getMessage() {
return message;
}
}
然后再定义一个 全局异常处理器 “GlobalExceptionHandler” 类。
package com.demo.exception;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.ModelAndView;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(LoginException.class)
public ModelAndView handler(LoginException e) {
e.printStackTrace();
ModelAndView mv = new ModelAndView();
mv.setViewName("exception");
mv.addObject("message", e.getMessage());
return mv;
}
}
接下来,我们需要创建 “resources\templates\exception.html” 文件
<!doctype html>
<html lang="zh-CN" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>exception</title>
</head>
<body>
<p th:text="${message}"></p>
</body>
</html>
关于异常的处理完毕了,我们创建一个 “LoginCheckInterceptor ” 拦截器。
package com.demo.interceptor;
import com.demo.exception.LoginException;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = request.getParameter("token");
if(token == null || token.length() <= 0){
throw new LoginException("没有登录token,无法访问该页面!");
}
return true;
}
}
这里,我们直接抛出了LoginException异常,不需要返回 false 了。
接下来,我们注册刚刚创建的拦截器
package com.demo.config;
import com.demo.interceptor.LoginCheckInterceptor;
import com.demo.interceptor.MyCustomerInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyCustomerInterceptor()).addPathPatterns("/test/**");
registry.addInterceptor(new LoginCheckInterceptor()).addPathPatterns("/test/**");
}
}
我们可以定义多个拦截器组成一个拦截器链。我们可以在适配器中注入多个拦截器。多个拦截器的话,先注册的先执行,也可以通过Order方法来设置执行的顺序(值越小越先执行)。
接下来,我们修改 “resources\static\index.html” 入口文件
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>index</title>
</head>
<body>
<a href="/test/hello">/test/hello</a>
<br />
<a href="/test/hello?token=abc">/test/hello?token=abc</a>
<br />
</body>
</html>
接下来,我们重新运行测试一下
没有token参数的话,就无法正常访问页面。
有token参数的话,就可以正常访问页面了。
如果需要返回 Json 格式数据的话,可以使用 HttpServletResponse response 对象和 com.alibaba.fastjson2 来完成。
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.52</version>
</dependency>
增加依赖库之后,修改 LoginCheckInterceptor 拦截器
package com.demo.interceptor;
import com.alibaba.fastjson2.JSON;
import com.demo.exception.LoginException;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.Writer;
import java.util.HashMap;
import java.util.Map;
public class LoginCheckInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String token = request.getParameter("token");
if(token == null || token.length() <= 0){
//throw new LoginException("没有登录token,无法访问该页面!");
response.setContentType("application/json;charset=UTF-8");
Writer writer = response.getWriter();
Map resultMap = new HashMap();
resultMap.put("code", "500");
resultMap.put("msg", "没有登录token,无法访问该页面!");
String result = JSON.toJSONString(resultMap);
writer.write(result);
writer.flush();
return false; // 停止 controller 的执行
}
return true;
}
}
我们再来测试一下啊
如果在拦截器中获取到用户请求参数并记录日志中,但是会发现在 controller 中的 @RequestBody 注解获取不到参数了(流是一次性的,数据会被消耗掉,无法重复读取)。Spring 中的 request.getInputStream()和 request.getReader()只能被读取一次,而 @RequestBody 注解底层也是通过流来请求数据,所以需要把拦截器中的数据流保存下来让 controller 层可以读取的数据。
本工程完整代码下载: https://download.youkuaiyun.com/download/richieandndsc/89953254