SpringBoot拦截器
拦截器
- 1. 拦截器
- 1.1 拦截器快速入门
- 1.2 拦截器详解
- 1.2.1 拦截路径
- 1.2.2 拦截器执行流程
- 1.3 登陆校验
- 1.3.1 定义拦截器
- 1.3.2 注册配置拦截器
- 1.4 DispatcherServlet源码分析(了解)
- 1.4.1 处理请求 (核心)
- 1.4.2 适配器模式
1. 拦截器
上个章节我们完成了强制登录的功能,后端程序根据Session来判断⽤⼾是否登录,但是实现⽅法是⽐较 ⿇烦的
- 需要修改每个接⼝的处理逻辑
- 需要修改每个接⼝的返回结果
- 接⼝定义修改,前端代码也需要跟着修改
有没有更简单的办法, 统⼀拦截所有的请求,并进⾏Session校验呢,这⾥我们学习⼀种新的解决办法:拦截器
1.1 拦截器快速入门
什么是拦截器?
拦截器是Spring框架提供的核⼼功能之⼀,主要⽤来拦截⽤⼾的请求,在指定⽅法前后,根据业务需要执 ⾏预先设定的代码.
也就是说,允许开发⼈员提前预定义⼀些逻辑,在⽤⼾的请求响应前后执⾏.也可以在⽤⼾请求前阻⽌其执⾏.
在拦截器当中,开发⼈员可以在应⽤程序中做⼀些通⽤性的操作, ⽐如通过拦截器来拦截前端发来的请求,判断Session中是否有登录⽤⼾的信息.如果有就可以放⾏,如果没有就进⾏拦截.
⽐如我们去银⾏办理业务,
在办理业务前后,就可以加⼀些拦截操作
办理业务之前,先取号,如果带⾝份证了就取号成功
业务办理结束,给业务办理⼈员的服务进⾏评价. 这些就是"拦截器"做的⼯作.
下⾯我们先来学习下拦截器的基本使⽤.
拦截器的使⽤步骤分为两步:
- 定义拦截器
- 注册配置拦截器
⾃定义拦截器:实现HandlerInterceptor接⼝,并重写其所有⽅法
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("LoginInterceptor ⽬标⽅法执⾏前执⾏..");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("LoginInterceptor ⽬标⽅法执⾏后执⾏");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("LoginInterceptor 视图渲染完毕后执⾏,最后执⾏");
}
}
- preHandle()⽅法:⽬标⽅法执⾏前执⾏. 返回true:继续执⾏后续操作; 返回false:中断后续操作.
- postHandle()⽅法:⽬标⽅法执⾏后执⾏
- afterCompletion()⽅法:视图渲染完毕后执⾏,最后执⾏(后端开发现在⼏乎不涉及视图,暂不了 解)
注册配置拦截器:实现WebMvcConfigurer接⼝,并重写addInterceptors⽅法
@Configuration
public class WebConfig implements WebMvcConfigurer {
//⾃定义的拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册⾃定义拦截器对象
registry.addInterceptor(loginInterceptor).addPathPatterns("/**");//设置拦截器拦截的请求路径( /** 表⽰拦截所有请求)
}
}
启动服务,试试访问任意请求,观察后端⽇志
可以看到preHandle⽅法执⾏之后就放⾏了,开始执⾏⽬标⽅法,⽬标⽅法执⾏完成之后执⾏ postHandle和afterCompletion⽅法.
我们把拦截器中preHandle⽅法的返回值改为false,再观察运⾏结果
可以看到,拦截器拦截了请求,没有进⾏响应.
1.2 拦截器详解
拦截器的⼊⻔程序完成之后,接下来我们来介绍拦截器的使⽤细节。拦截器的使⽤细节我们主要介绍
两个部分:
- 拦截器的拦截路径配置
- 拦截器实现原理
1.2.1 拦截路径
拦截路径是指我们定义的这个拦截器,对哪些请求⽣效.
我们在注册配置拦截器的时候,通过 addPathPatterns() ⽅法指定要拦截哪些请求.也可以通过excludePathPatterns()
指定不拦截哪些请求.
上述代码中,我们配置的是
/**
,表⽰拦截所有的请求.
⽐如⽤⼾登录校验,我们希望可以对除了登录之外所有的路径⽣效.
import org.springframework.beans.factory.annotation.Autowired;
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 {
//⾃定义的拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册⾃定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/user/login");//设置拦截器拦截的请求路径 (/** 表⽰拦截所有请求)
}
}
在拦截器中除了可以设置 /**
拦截所有资源外,还有⼀些常⻅拦截路径设置:
以上拦截规则可以拦截此项⽬中的使⽤URL,包括静态⽂件(图⽚⽂件,JS和CSS等⽂件).
1.2.2 拦截器执行流程
正确的调用顺序
有了拦截器
之后,会在调⽤Controller之前
进⾏相应的业务处理,执⾏的流程如下图
-
添加拦截器后,执⾏Controller的⽅法之前,请求会先被拦截器拦截住.
执⾏ preHandle() ⽅法
,这个⽅法需要返回⼀个布尔类型的值.如果返回true,就表⽰放⾏本次操作,继续访问controller中的⽅法.
如果返回false,则不会放⾏(controller中的⽅法也不会执⾏).
-
controller当中的⽅法执⾏完毕后,再回过来执⾏
postHandle()
这个⽅法以及afterCompletion() ⽅法
,执⾏完毕之后,最终给浏览器响应数据.
1.3 登陆校验
学习拦截器的基本操作之后,接下来我们需要完成最后⼀步操作:
通过拦截器来完成图书管理系统中 的登录校验功能
1.3.1 定义拦截器
从session
中获取⽤⼾信息, 如果session中不存在,则返回false,并设置http状态码为401,否则返回true.
import jdk.nashorn.internal.runtime.regexp.joni.exception.SyntaxException;
import com.example.demo.constant.Constants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse
response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (session != null &&
session.getAttribute(Constants.SESSION_USER_KEY) != null) {
return true;
}
response.setStatus(401);
return false;
}
}
http状态码401:Unauthorized
Indicatesthatauthenticationisrequiredandwaseithernotprovidedorhasfailed.Ifthe requestalreadyincludedauthorizationcredentials,thenthe401statuscodeindicatesthat thosecredentialswerenotaccepted.
中⽂解释:未经过认证.指⽰⾝份验证是必需的,没有提供⾝份验证或⾝份验证失败.如果请求已经包 含授权凭据,那么401状态码表⽰不接受这些凭据。
1.3.2 注册配置拦截器
import org.springframework.beans.factory.annotation.Autowired;
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 {
//⾃定义的拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册⾃定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")//设置拦截器拦截的请求路径(/**表⽰拦截所有请 求)
.excludePathPatterns("/user/login")//设置拦截器排除拦截的路径
.excludePathPatterns("/**/*.js") //排除前端静态资源
.excludePathPatterns("/**/*.css")
.excludePathPatterns("/**/*.png")
.excludePathPatterns("/**/*.html");
}
}
也可以改成
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.List;
@Configuration
public class WebConfig implements WebMvcConfigurer {
//⾃定义的拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
private List<String> excludePaths = Arrays.asList(
"/user/login",
"/**/*.js",
"/**/*.css",
"/**/*.png",
"/**/*.html"
);
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册⾃定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")//设置拦截器拦截的请求路径(/** 表⽰拦截所有请 求)
.excludePathPatterns(excludePaths);//设置拦截器排除拦截的路径
}
}
删除之前的登录校验代码
@RequestMapping("/getListByPage")
public Result getListByPage(PageRequest pageRequest, HttpSession session) {
log.info("获取图书列表, pageRequest:{}", pageRequest);
// //判断⽤⼾是否登录
// if (session.getAttribute(Constants.SESSION_USER_KEY)==null){
// return Result.unlogin();
// }
// UserInfo userInfo = (UserInfo)session.getAttribute(Constants.SESSION_USER_KEY);
// if (userInfo==null || userInfo.getId()<0 ||"".equals(userInfo.getUserName())){
// return Result.unlogin();
// }
//⽤⼾登录, 返回图书列表
PageResult<BookInfo> pageResult =
bookService.getBookListByPage(pageRequest);
log.info("获取图书列表222, pageRequest:{}", pageResult);
return Result.success(pageResult);
}
运行程序, 通过 postman 进行测试:
- 查看图书列表
http://127.0.0.1:8080/book/getListByPage
观察返回结果:http状态码401
也可以通过Fiddler抓包观察
2. 登陆
http://127.0.0.1:8080/user/login?name=admin&password=admin
3. 再次查看图书列表
数据进行了返回
1.4 DispatcherServlet源码分析(了解)
观察我们的服务启动⽇志:
当Tomcat启动之后,有⼀个核⼼的类DispatcherServlet
,它来控制程序的执⾏顺序.
所有请求都会先进到DispatcherServlet,执⾏doDispatch调度⽅法.
如果有拦截器,会先执⾏拦截器preHandle() ⽅法的代码,
如果 preHandle() 返回 true
,继续访问controller中的⽅法.
controller当中的⽅法执⾏完毕后,再回过来执⾏ postHandle() 和afterCompletion() ,返回给 DispatcherServlet,最终给浏览器响应数据.
1.4.1 处理请求 (核心)
DispatcherServlet接收到请求后,执⾏doDispatch调度⽅法,再将请求转给Controller.
我们来看doDispatch⽅法的具体实现
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
//1. 获取执⾏链
//遍历所有的 HandlerMapping 找到与请求对应的Handler
mappedHandler = this.getHandler(processedRequest);
if (mappedHandler == null) {
this.noHandlerFound(processedRequest, response);
return;
}
//2. 获取适配器
//遍历所有的 HandlerAdapter,找到可以处理该 Handler 的
HandlerAdapter
HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler());
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) {
return;
}
}
//3. 执⾏拦截器preHandle⽅法
if (!mappedHandler.applyPreHandle(processedRequest, response))
{
return;
}
//4. 执⾏⽬标⽅法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
this.applyDefaultViewName(processedRequest, mv);
//5. 执⾏拦截器postHandle⽅法
mappedHandler.applyPostHandle(processedRequest, response, mv);
} catch (Exception var20) {
dispatchException = var20;
} catch (Throwable var21) {
dispatchException = new NestedServletException("Handler dispatch failed", var21);
}
//6. 处理视图, 处理之后执⾏拦截器afterCompletion⽅法
this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException);
} catch (Exception var22) {
//7. 执⾏拦截器afterCompletion⽅法
this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22);
} catch (Throwable var23) {
this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23));
}
} finally {
if (asyncManager.isConcurrentHandlingStarted()) {
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
} else if (multipartRequestParsed) {
this.cleanupMultipart(processedRequest);
}
}
}
HandlerAdapter
在SpringMVC中使⽤了适配器模式,下⾯详细再介绍
适配器模式
,也叫包装器模式.简单来说就是⽬标类不能直接使⽤,通过⼀个新类进⾏包装⼀下,适配 调⽤⽅使⽤.把两个不兼容的接⼝通过⼀定的⽅式使之兼容.
HandlerAdapter主要⽤于⽀持不同类型的处理器(如Controller、HttpRequestHandler或者 Servlet等),让它们能够适配统⼀的请求处理流程。这样,SpringMVC可以通过⼀个统⼀的接⼝ 来处理来⾃各种处理器的请求.
从上述源码可以看出在开始执⾏Controller之前,会先调⽤预处理⽅法applyPreHandle,
⽽ applyPreHandle⽅法的实现源码
如下:
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
// 获取项⽬中使⽤的拦截器 HandlerInterceptor
HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
this.triggerAfterCompletion(request, response, (Exception)null);
return false;
}
}
return true;
}
在applyPreHandle中会获取所有的拦截器HandlerInterceptor
,并执⾏拦截器中的 preHandle⽅法,这样就会咱们前⾯定义的拦截器对应上了,如下图所⽰:
如果拦截器返回true,整个发放就返回true,继续执⾏后续逻辑处理
如果拦截器返回fasle,则中断后续操作
1.4.2 适配器模式
HandlerAdapter在SpringMVC中使⽤了适配器模式
适配器模式定义
适配器模式,也叫包装器模式.将⼀个类的接⼝,转换成客⼾期望的另⼀个接⼝,适配器让原本接⼝不兼 容的类可以合作⽆间.
简单来说就是⽬标类不能直接使⽤,通过⼀个新类进⾏包装⼀下,适配调⽤⽅使⽤.
把两个不兼容的接⼝ 通过⼀定的⽅式使之兼容.
⽐如下⾯两个接⼝,本⾝是不兼容的(参数类型不⼀样,参数个数不⼀样等等)
可以通过适配器的⽅式,使之兼容
⽇常⽣活中,适配器模式也是⾮常常⻅的
⽐如转换插头,⽹络转接头等
出国旅⾏必备物品之⼀就是转换插头.不同国家的插头标准是不⼀样的,出国后我们⼿机/电脑充电器 可能就没办法使⽤了.⽐如美国电器110V,中国220V,就要有⼀个适配器将110V转化为220V.国 内也经常使⽤转换插头把两头转为三头,或者三头转两头
适配器模式角色
- Target:⽬标接⼝(可以是抽象类或接⼝),客⼾希望直接⽤的接⼝
- Adaptee:适配者,但是与Target不兼容
- Adapter:适配器类,此模式的核⼼.通过继承或者引⽤适配者的对象,把适配者转为⽬标接⼝
- client:需要使⽤适配器的对象
适配器模式的实现
**场景:**前⾯学习的slf4j就使⽤了适配器模式,slf4j提供了⼀系列打印⽇志的api,底层调⽤的是log4j或者 logback来打⽇志,我们作为调⽤者,只需要调⽤slf4j的api就⾏了.
/**
* slf4j接⼝
*/
interface Slf4jApi{
void log(String message);
}
/**
* log4j 接⼝
*/
class Log4j{
void log4jLog(String message){
System.out.println("Log4j打印:"+message);
}
}
/**
* slf4j和log4j适配器
*/
class Slf4jLog4JAdapter implements Slf4jApi{
private Log4j log4j;
public Slf4jLog4JAdapter(Log4j log4j) {
this.log4j = log4j;
}
@Override
public void log(String message) {
log4j.log4jLog(message);
}
}
/**
* 客⼾端调⽤
*/
public class Slf4jDemo {
public static void main(String[] args) {
Slf4jApi slf4jApi = new Slf4jLog4JAdapter(new Log4j());
slf4jApi.log("使⽤slf4j打印⽇志");
}
}
可以看出,我们不需要改变log4j的api,只需要通过适配器转换下,就可以更换⽇志框架,保障系统的平稳 运⾏.
适配器模式的实现并不在slf4j-core中(只定义了Logger),具体实现是在针对log4j的桥接器项⽬slf4jlog4j12中
设计模式的使⽤⾮常灵活,⼀个项⽬中通常会含有多种设计模式.
适配器模式应⽤场景
⼀般来说,适配器模式可以看作⼀种"补偿模式",⽤来补救设计上的缺陷.应⽤这种模式算是"⽆奈之 举",如果在设计初期,我们就能协调规避接⼝不兼容的问题,就不需要使⽤适配器模式了
所以适配器模式更多的应⽤场景主要是对正在运⾏的代码进⾏改造,并且希望可以复⽤原有代码实现新 的功能.⽐如版本升级等.