当前位置: 首页 > article >正文

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中是否有登录⽤⼾的信息.如果有就可以放⾏,如果没有就进⾏拦截.

    ⽐如我们去银⾏办理业务,
    在办理业务前后,就可以加⼀些拦截操作
    办理业务之前,先取号,如果带⾝份证了就取号成功
    业务办理结束,给业务办理⼈员的服务进⾏评价. 这些就是"拦截器"做的⼯作.

下⾯我们先来学习下拦截器的基本使⽤.

拦截器的使⽤步骤分为两步:

  1. 定义拦截器
  2. 注册配置拦截器

⾃定义拦截器:实现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.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之前进⾏相应的业务处理,执⾏的流程如下图

  1. 添加拦截器后,执⾏Controller的⽅法之前,请求会先被拦截器拦截住.执⾏ preHandle() ⽅法,这个⽅法需要返回⼀个布尔类型的值.

    如果返回true,就表⽰放⾏本次操作,继续访问controller中的⽅法.

    如果返回false,则不会放⾏(controller中的⽅法也不会执⾏).

  2. 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 进行测试:

  1. 查看图书列表
    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中

设计模式的使⽤⾮常灵活,⼀个项⽬中通常会含有多种设计模式.

适配器模式应⽤场景

⼀般来说,适配器模式可以看作⼀种"补偿模式",⽤来补救设计上的缺陷.应⽤这种模式算是"⽆奈之 举",如果在设计初期,我们就能协调规避接⼝不兼容的问题,就不需要使⽤适配器模式了

所以适配器模式更多的应⽤场景主要是对正在运⾏的代码进⾏改造,并且希望可以复⽤原有代码实现新 的功能.⽐如版本升级等.


http://www.kler.cn/a/513633.html

相关文章:

  • logback日志自定义占位符
  • 云计算、AI与国产化浪潮下DBA职业之路风云变幻,如何谋破局启新途?
  • Kotlin语言的数据结构
  • 小程序获取微信运动步数
  • 玉米植物结构受乙烯生物合成基因 ZmACS7 的调控
  • 【C++】C++11
  • 专题三_穷举vs暴搜vs深搜vs回溯vs剪枝_全排列
  • 【王树森搜索引擎技术】概要04:搜索引擎的链路(查询词处理、召回、排序)
  • Linux的软件包管理器
  • 《Effective Java》学习笔记——第1部分 创建对象和销毁对象的最佳实践
  • Redis使用基础
  • TCP如何保证安全可靠?
  • 我国的金融组织体系,还有各大金融机构的分类,金融行业的组织
  • 【Excel】【VBA】Reaction超限点筛选与散点图可视化
  • 【线性代数】基础版本的高斯消元法
  • Keil自动生成Bin文件(2)
  • 2024年度个人成长与技术洞察总结
  • Data Filtering Network 论文阅读和理解
  • C++ 智能指针(八股总结)
  • 【组件库】使用Vue2+AntV X6+ElementUI 实现拖拽配置自定义vue节点
  • Springboot sse 示例
  • (done) 并行计算学习 (Day1: 两个简单的 OpenMP 例子)
  • JavaWeb开发(十五)实战-生鲜后台管理系统(二)注册、登录、记住密码
  • 【C++】揭秘类与对象的内在机制(核心卷之深浅拷贝与拷贝构造函数的奥秘)
  • 《从入门到精通:蓝桥杯编程大赛知识点全攻略》(五)-数的三次方根、机器人跳跃问题、四平方和
  • Python 进阶 - Excel 基本操作