SpringBoot拦截器与文件上传实现方法与源码分析

Java
475
0
0
2023-03-14
标签   SpringBoot
目录
  • 一、拦截器
  • 1、创建一个拦截器
  • 2、配置拦截器
  • 二、拦截器原理
  • 三、文件上传
  • 四、文件上传流程

一、拦截器

拦截器我们之前在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的文件上传和下载

使用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类实现文件上传的相关功能