【Spring Boot】拦截器、文件上传的功能实现与源码解析

一、拦截器

拦截器我们之前在springmvc已经做过介绍了
大家可以看下博主之前的博客【SpringMVC】自定义拦截器和过滤器

为什么在这里还要再讲一遍呢?
因为spring boot里面对它做了简化,大大节省了我们配置那些烦人的xml文件的时间

接下来,我们就通过一个小例子来了解一下拦截器在spring boot中的使用

1、创建一个拦截器

首先我们创建一个拦截器,实现HandlerInterceptor接口

package com.decade.interceptor;

import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

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

@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    // 在调用控制器接口方法之前进入,如果放回true就放行,进入下一个拦截器或者控制器,如果返回false就不继续往下走
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 获取当前请求路径
        final String requestURL = request.getRequestURI();
        log.info("拦截到的请求为:{}", requestURL);

        final HttpSession session = request.getSession();
        final Object userSession = session.getAttribute("loginUser");

        // 如果session中存在用户登录信息,那么就判定为用户已登录,放行
        if (null != userSession) {
            return true;
        } else {
            // model和request都会往请求域中塞信息,所以这里可以使用request传递我们需要返回给前端的信息
            request.setAttribute("msg", "请登录!");
            // 转发到登录页
            request.getRequestDispatcher("/").forward(request, response);
            return false;
        }
    }

    //调用前提:preHandle返回true
    //调用时间:Controller方法处理完之后,DispatcherServlet进行视图的渲染之前,也就是说在这个方法中你可以对ModelAndView进行操作
    //执行顺序:链式Interceptor情况下,Interceptor按照声明的顺序倒着执行。
    //备注:postHandle虽然post打头,但post、get方法都能处理
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle执行{}", modelAndView);
    }

    //调用前提:preHandle返回true
    //调用时间:DispatcherServlet进行视图的渲染之后
    //多用于清理资源
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("页面渲染完成后执行");
    }
}
2、配置拦截器

创建完之后,我们就需要将拦截器注册到容器中,并指定拦截规则

那么,我们创建一个配置类,实现WebMvcConfigurer接口,重写addInterceptors方法,将我们之前创建好的拦截器放入即可

值得注意的是,我们要放开对登录页以及静态资源的限制

package com.decade.config;

import com.decade.interceptor.LoginInterceptor;
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 MyConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // addPathPatterns:设置要拦截的请求,如果是/**,那么会拦截包括静态资源在内的所有请求
        // excludePathPatterns:设置不被拦截的请求,这里我们放行登录页请求和静态资源
        registry.addInterceptor(new LoginInterceptor())
            .addPathPatterns("/**")
            .excludePathPatterns("/", "/login", "/css/**", "/images/**", "/js/**", "/fonts/**");
    }
}

我们在未登录的状态下,对主页发起一个请求,可以发现,拦截器生效,而且拦截器中的方法所执行的顺序也符合预期
在这里插入图片描述
在这里插入图片描述

二、拦截器原理

我们还是使用debug模式,通过断点来进行分析

调用之前的主页面接口,可以发现断点还是走到了DispatcherServlet类下的doDispatch()
首先,他还是会返回给我们一个处理器执行链HandlerExecutionChain
这个里面除了包含我们的请求应该由哪个控制器类的哪个方法进行处理之外,还包含了拦截器链
在这里插入图片描述
然后在使用mv = ha.handle(processedRequest, response, mappedHandler.getHandler());执行目标方法之前,他会调用一个applyPreHandle()方法

如果这个方法返回false,那么就会直接返回,不再继续往下走
在这里插入图片描述

我们进入applyPreHandle()方法可以看到,这个方法里会遍历所有的拦截器,如果preHandle()方法返回结果为true,那就继续调用下一个拦截器的preHandle()方法

只要有一个拦截器的preHandle()方法返回false,那么就会从当前遍历到的拦截器开始,倒序执行afterCompletion()方法

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
    for(int i = 0; i < this.interceptorList.size(); this.interceptorIndex = i++) {
        HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
        // 如果拦截器的preHandle()返回false,那么就会调用下面的triggerAfterCompletion()
        if (!interceptor.preHandle(request, response, this.handler)) {
            this.triggerAfterCompletion(request, response, (Exception)null);
            return false;
        }
    }

    return true;
}

// 这个方法里面会从当前遍历到的拦截器开始,倒序执行afterCompletion()方法
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
    for(int i = this.interceptorIndex; i >= 0; --i) {
        HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);

        try {
            interceptor.afterCompletion(request, response, this.handler, ex);
        } catch (Throwable var7) {
            logger.error("HandlerInterceptor.afterCompletion threw exception", var7);
        }
    }
}

执行完目标方法之后,断点又走到mappedHandler.applyPostHandle(processedRequest, response, mv);
深入这个方法,我们可以发现,这里是倒序执行了所有拦截器的postHandle()方法

void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
    for(int i = this.interceptorList.size() - 1; i >= 0; --i) {
        HandlerInterceptor interceptor = (HandlerInterceptor)this.interceptorList.get(i);
        interceptor.postHandle(request, response, this.handler, mv);
    }

}

最后,页面渲染完成之后,他也会倒序执行所有拦截器的afterCompletion()方法
在这里插入图片描述

注意:只要在请求处理期间出现任何异常,它都会倒序执行所有拦截器的postHandle()方法
在这里插入图片描述

三、文件上传

之前博主也写过关于SpringMVC的文件上传和下载【SpringMVC】文件上传和下载

使用Spring Boot之后,我们节约了很多的配置
接下来,我们就通过一个例子,了解Spring Boot中的文件上传

首先,我们先创建一个页面,这里我们只贴核心代码

  • 默认情况下,enctype的值是application/x-www-form-urlencoded,不能用于文件上传,只有使用了multipart/form-data,才能完整的传递文件数据
  • multiple表示可接受多个值的文件上传字段
<div class="panel-body">
    <form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
        <div class="form-group">
            <label for="exampleInputEmail1">邮箱</label>
            <input type="email" name="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
        </div>
        <div class="form-group">
            <label for="exampleInputPassword1">名字</label>
            <input type="text" name="username" class="form-control" id="exampleInputPassword1" placeholder="Password">
        </div>
        <div class="form-group">
            <label for="exampleInputFile">头像</label>
            <input type="file" name="headerImg" id="exampleInputFile">
        </div>
        <div class="form-group">
            <label for="exampleInputFile">生活照</label>
            <input type="file" name="photos" multiple>
        </div>
        <div class="checkbox">
            <label>
                <input type="checkbox"> Check me out
            </label>
        </div>
        <button type="submit" class="btn btn-primary">提交</button>
    </form>

</div>

然后我们写一下后端的业务代码

package com.decade.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;

@Controller
@Slf4j
public class FileUploadController {

    /**
     * 页面跳转,跳转到文件上传页面
     * @return 跳转到文件上传页面
     */
    @GetMapping(value = "/form_layouts")
    public String uploadPage() {
        return "form/form_layouts";
    }

    /**
     * 文件上传请求
     * @param email 邮件
     * @param username 用户名
     * @param headerImg 头像文件
     * @param photos 生活照
     * @return 如果上传文件成功,跳转到首页
     */
    @PostMapping(value = "/upload")
    public String uploadFile(@RequestParam(name = "email") String email,
        @RequestParam(name = "username") String username, @RequestPart("headerImg") MultipartFile headerImg,
        @RequestPart("photos") MultipartFile[] photos) {
        log.info("请求参数email{}, username{}, 头像headerImg大小{}, 生活照photos张数{}",
            email, username, headerImg.getSize(), photos.length);
        try {
            // 判断头像文件是否为空,如果不是为空,那么就保存到本地
            if (!headerImg.isEmpty()) {
                final String filename = headerImg.getOriginalFilename();
                headerImg.transferTo(new File("D:\\test1\\" + filename));
            }
            // 判断生活照是否上传,循环保存到本地
            if (photos.length > 0) {
                for (MultipartFile photo : photos) {
                    final String originalFilename = photo.getOriginalFilename();
                    photo.transferTo(new File("D:\\test1\\" + originalFilename));
                }
            }
        } catch (IOException e) {
            log.error("上传文件出错!", e);
        }
        return "redirect:/main.html";
    }
}

如果报错信息如下,那么我们需要去Spring Boot的默认文件中添加如下配置
在这里插入图片描述

# 单个文件最大限制
spring.servlet.multipart.max-file-size=10MB
# 单次请求最大限制
spring.servlet.multipart.max-request-size=100MB

修改相关配置之后,文件上传成功
在这里插入图片描述

四、文件上传流程

文件上传相关配置类MultipartAutoConfiguration,相关配置类MultipartProperties

MultipartAutoConfiguration中我们自动配置好了文件上传解析器StandardServletMultipartResolver(它在容器中的beanName为multipartResolver)

然后我们跟着上面文件上传的例子进行一个debug,分析一下流程
首先,断点还是来到DispatcherServlet下面的doDispatch()方法

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    // 设置文件解析默认值为false
    boolean multipartRequestParsed = false;
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        try {
            ModelAndView mv = null;
            Object dispatchException = null;

            try {
            	// 检查当前请求是否涉及文件上传
                processedRequest = this.checkMultipart(request);
                // 将文件解析设置为true,表明当前请求涉及文件上传
                multipartRequestParsed = processedRequest != request;

这里的processedRequest = this.checkMultipart(request);
会调用StandardServletMultipartResolver类中的isMultipart()判断当前请求是否涉及文件上传
如果涉及那么就会对当前请求做一个处理,将原生的请求封装成一个StandardMultipartHttpServletRequest请求,把文件相关信息解析后放进Map中(具体可以看StandardMultipartHttpServletRequest类中的parseRequest方法)

protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
	// 如果文件上传解析器不为空,那么就调用StandardServletMultipartResolver类中的isMultipart()判断当前请求是否涉及文件上传
    if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
        if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
            if (DispatcherType.REQUEST.equals(request.getDispatcherType())) {
                this.logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
            }
        } else if (this.hasMultipartException(request)) {
            this.logger.debug("Multipart resolution previously failed for current request - skipping re-resolution for undisturbed error rendering");
        } else {
            try {
            	// 将原生的请求封装成一个StandardMultipartHttpServletRequest请求,把文件相关信息解析放进Map中
                return this.multipartResolver.resolveMultipart(request);

在这里插入图片描述
然后我们按照之前请求处理那篇博客里的路径,从mv = ha.handle(processedRequest, response, mappedHandler.getHandler())进入

一直走到InvocableHandlerMethod下面的getMethodArgumentValues()方法,深入断点
我们得知,使用@RequestParam注解的参数使用RequestParamMethodArgumentResolver这个解析器
而文件相关入参是使用@RequestPart注解的,它使用RequestPartMethodArgumentResolver来进行文件相关参数解析

在这个解析器中,他又会根据参数的名称去上面checkMultipart()方法所生成的Map中获取文件相关信息

@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest request, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    HttpServletRequest servletRequest = (HttpServletRequest)request.getNativeRequest(HttpServletRequest.class);
    Assert.state(servletRequest != null, "No HttpServletRequest");
    RequestPart requestPart = (RequestPart)parameter.getParameterAnnotation(RequestPart.class);
    boolean isRequired = (requestPart == null || requestPart.required()) && !parameter.isOptional();
    // 获取文件上传的参数名称
    String name = this.getPartName(parameter, requestPart);
    parameter = parameter.nestedIfOptional();
    Object arg = null;
    // 根据参数名称去获取前面map中的value,也就是MultipartFile对象
    Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);

后面的调用链为MultipartResolutionDelegate.resolveMultipartArgument()—>判断当前参数是否是文件上传,如果是,继续判断是多文件上传还是单文件上传—>然后进入AbstractMultipartHttpServletRequest中,单文件走getFile()从map中获取文件信息,多文件走getFiles()从map中获取文件信息

最后,在控制器的目标方法处使用MultipartFile类实现文件上传的相关功能

如有错误,欢迎指正!!!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot中,可以使用拦截器来对请求进行拦截和处理。拦截器是一种AOP(面向切面编程)的技术,在请求到达控制器之前或之后对请求进行处理。拦截器可以用于实现一些通用的功能,如日志记录、权限验证、参数校验等。 在Spring Boot中定义拦截器需要以下步骤: 1. 创建一个拦截器类,该类需要实现HandlerInterceptor接口。可以在拦截器中重写preHandle、postHandle和afterCompletion等方法来实现请求的前置处理、后置处理和完成处理。 2. 创建一个拦截器配置类,该类需要实现WebMvcConfigurer接口,并重写addInterceptors方法。在该方法中,可以注册拦截器类,并为其定义拦截规则。 在配置类中,可以通过使用addPathPatterns方法来定义需要拦截的请求路径,也可以使用excludePathPatterns方法来排除某些请求路径不被拦截。 拦截器过滤器在功能和使用上有一些区别: 1. 归属不同:过滤器属于Servlet技术,而拦截器属于SpringMVC技术。 2. 内容不同:过滤器对所有访问进行增强,而拦截器仅针对SpringMVC的访问进行增强。 通过上述步骤定义和配置拦截器,可以在Spring Boot应用中实现请求的拦截和处理。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Spring-Boot 拦截器](https://blog.csdn.net/PeterMrWang/article/details/123916751)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* [Springboot——拦截器](https://blog.csdn.net/weixin_51351637/article/details/128058053)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值