SpringBoot攔截器與文件上傳實現方法與源碼分析
一、攔截器
攔截器我們之前在springmvc已經做過介紹了
大家可以看下【SpringMVC】自定義攔截器和過濾器
為什么在這里還要再講一遍呢?
因為spring boot里面對它做了簡化,大大節(jié)省了我們配置那些煩人的xml文件的時間
接下來,我們就通過一個小例子來了解一下攔截器在spring boot中的使用
1、創(chuàng)建一個攔截器
首先我們創(chuàng)建一個攔截器,實現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就不繼續(xù)往下走
@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", "請登錄!");
// 轉發(fā)到登錄頁
request.getRequestDispatcher("/").forward(request, response);
return false;
}
}
//調用前提:preHandle返回true
//調用時間:Controller方法處理完之后,DispatcherServlet進行視圖的渲染之前,也就是說在這個方法中你可以對ModelAndView進行操作
//執(zhí)行順序:鏈式Interceptor情況下,Interceptor按照聲明的順序倒著執(zhí)行。
//備注:postHandle雖然post打頭,但post、get方法都能處理
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle執(zhí)行{}", modelAndView);
}
//調用前提:preHandle返回true
//調用時間:DispatcherServlet進行視圖的渲染之后
//多用于清理資源
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("頁面渲染完成后執(zhí)行");
}
}2、配置攔截器
創(chuàng)建完之后,我們就需要將攔截器注冊到容器中,并指定攔截規(guī)則
那么,我們創(chuàng)建一個配置類,實現WebMvcConfigurer接口,重寫addInterceptors方法,將我們之前創(chuàng)建好的攔截器放入即可
值得注意的是,我們要放開對登錄頁以及靜態(tài)資源的限制
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:設置要攔截的請求,如果是/**,那么會攔截包括靜態(tài)資源在內的所有請求
// excludePathPatterns:設置不被攔截的請求,這里我們放行登錄頁請求和靜態(tài)資源
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/", "/login", "/css/**", "/images/**", "/js/**", "/fonts/**");
}
}我們在未登錄的狀態(tài)下,對主頁發(fā)起一個請求,可以發(fā)現,攔截器生效,而且攔截器中的方法所執(zhí)行的順序也符合預期


二、攔截器原理
我們還是使用debug模式,通過斷點來進行分析
調用之前的主頁面接口,可以發(fā)現斷點還是走到了DispatcherServlet類下的doDispatch()
首先,他還是會返回給我們一個處理器執(zhí)行鏈HandlerExecutionChain
這個里面除了包含我們的請求應該由哪個控制器類的哪個方法進行處理之外,還包含了攔截器鏈

然后在使用mv = ha.handle(processedRequest, response, mappedHandler.getHandler());執(zhí)行目標方法之前,他會調用一個applyPreHandle()方法
如果這個方法返回false,那么就會直接返回,不再繼續(xù)往下走

我們進入applyPreHandle()方法可以看到,這個方法里會遍歷所有的攔截器,如果preHandle()方法返回結果為true,那就繼續(xù)調用下一個攔截器的preHandle()方法
只要有一個攔截器的preHandle()方法返回false,那么就會從當前遍歷到的攔截器開始,倒序執(zhí)行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;
}
// 這個方法里面會從當前遍歷到的攔截器開始,倒序執(zhí)行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);
}
}
}執(zhí)行完目標方法之后,斷點又走到mappedHandler.applyPostHandle(processedRequest, response, mv);
深入這個方法,我們可以發(fā)現,這里是倒序執(zhí)行了所有攔截器的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);
}
}最后,頁面渲染完成之后,他也會倒序執(zhí)行所有攔截器的afterCompletion()方法

注意:只要在請求處理期間出現任何異常,它都會倒序執(zhí)行所有攔截器的postHandle()方法

三、文件上傳
之前博主也寫過關于SpringMVC的文件上傳和下載
使用Spring Boot之后,我們節(jié)約了很多的配置
接下來,我們就通過一個例子,了解Spring Boot中的文件上傳
首先,我們先創(chuàng)建一個頁面,這里我們只貼核心代碼
- 默認情況下,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>然后我們寫一下后端的業(yè)務代碼
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));
}
// 判斷生活照是否上傳,循環(huán)保存到本地
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()—>判斷當前參數是否是文件上傳,如果是,繼續(xù)判斷是多文件上傳還是單文件上傳—>然后進入AbstractMultipartHttpServletRequest中,單文件走getFile()從map中獲取文件信息,多文件走getFiles()從map中獲取文件信息
最后,在控制器的目標方法處使用MultipartFile類實現文件上傳的相關功能
到此這篇關于SpringBoot攔截器與文件上傳實現方法與源碼分析的文章就介紹到這了,更多相關SpringBoot攔截器與文件上傳內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
SpringCloud:feign對象傳參和普通傳參及遇到的坑解決
這篇文章主要介紹了SpringCloud:feign對象傳參和普通傳參及遇到的坑解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03
詳解Maven settings.xml配置(指定本地倉庫、阿里云鏡像設置)
這篇文章主要介紹了詳解Maven settings.xml配置(指定本地倉庫、阿里云鏡像設置),小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-12-12
Spring?Data?JPA系列QueryByExampleExecutor使用詳解
這篇文章主要為大家介紹了Spring?Data?JPA系列QueryByExampleExecutor使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09

