SpringBoot攔截器與文件上傳實現(xiàn)方法與源碼分析
一、攔截器
攔截器我們之前在springmvc已經(jīng)做過介紹了
大家可以看下【SpringMVC】自定義攔截器和過濾器
為什么在這里還要再講一遍呢?
因為spring boot里面對它做了簡化,大大節(jié)省了我們配置那些煩人的xml文件的時間
接下來,我們就通過一個小例子來了解一下攔截器在spring boot中的使用
1、創(chuàng)建一個攔截器
首先我們創(chuàng)建一個攔截器,實現(xiàn)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 { // 在調(diào)用控制器接口方法之前進(jìn)入,如果放回true就放行,進(jìn)入下一個攔截器或者控制器,如果返回false就不繼續(xù)往下走 @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 獲取當(dāng)前請求路徑 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", "請登錄!"); // 轉(zhuǎn)發(fā)到登錄頁 request.getRequestDispatcher("/").forward(request, response); return false; } } //調(diào)用前提:preHandle返回true //調(diào)用時間:Controller方法處理完之后,DispatcherServlet進(jìn)行視圖的渲染之前,也就是說在這個方法中你可以對ModelAndView進(jìn)行操作 //執(zhí)行順序:鏈?zhǔn)絀nterceptor情況下,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); } //調(diào)用前提:preHandle返回true //調(diào)用時間:DispatcherServlet進(jìn)行視圖的渲染之后 //多用于清理資源 @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info("頁面渲染完成后執(zhí)行"); } }
2、配置攔截器
創(chuàng)建完之后,我們就需要將攔截器注冊到容器中,并指定攔截規(guī)則
那么,我們創(chuàng)建一個配置類,實現(xiàn)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:設(shè)置要攔截的請求,如果是/**,那么會攔截包括靜態(tài)資源在內(nèi)的所有請求 // excludePathPatterns:設(shè)置不被攔截的請求,這里我們放行登錄頁請求和靜態(tài)資源 registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/", "/login", "/css/**", "/images/**", "/js/**", "/fonts/**"); } }
我們在未登錄的狀態(tài)下,對主頁發(fā)起一個請求,可以發(fā)現(xiàn),攔截器生效,而且攔截器中的方法所執(zhí)行的順序也符合預(yù)期
二、攔截器原理
我們還是使用debug模式,通過斷點來進(jìn)行分析
調(diào)用之前的主頁面接口,可以發(fā)現(xiàn)斷點還是走到了DispatcherServlet類下的doDispatch()
首先,他還是會返回給我們一個處理器執(zhí)行鏈HandlerExecutionChain
這個里面除了包含我們的請求應(yīng)該由哪個控制器類的哪個方法進(jìn)行處理之外,還包含了攔截器鏈
然后在使用mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
執(zhí)行目標(biāo)方法之前,他會調(diào)用一個applyPreHandle()方法
如果這個方法返回false,那么就會直接返回,不再繼續(xù)往下走
我們進(jìn)入applyPreHandle()方法可以看到,這個方法里會遍歷所有的攔截器,如果preHandle()方法返回結(jié)果為true,那就繼續(xù)調(diào)用下一個攔截器的preHandle()方法
只要有一個攔截器的preHandle()方法返回false,那么就會從當(dāng)前遍歷到的攔截器開始,倒序執(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,那么就會調(diào)用下面的triggerAfterCompletion() if (!interceptor.preHandle(request, response, this.handler)) { this.triggerAfterCompletion(request, response, (Exception)null); return false; } } return true; } // 這個方法里面會從當(dāng)前遍歷到的攔截器開始,倒序執(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í)行完目標(biāo)方法之后,斷點又走到mappedHandler.applyPostHandle(processedRequest, response, mv);
深入這個方法,我們可以發(fā)現(xiàn),這里是倒序執(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()方法
注意:只要在請求處理期間出現(xiàn)任何異常,它都會倒序執(zhí)行所有攔截器的postHandle()方法
三、文件上傳
之前博主也寫過關(guān)于SpringMVC的文件上傳和下載
使用Spring Boot之后,我們節(jié)約了很多的配置
接下來,我們就通過一個例子,了解Spring Boot中的文件上傳
首先,我們先創(chuàng)建一個頁面,這里我們只貼核心代碼
- 默認(rèn)情況下,enctype的值是application/x-www-form-urlencoded,不能用于文件上傳,只有使用了multipart/form-data,才能完整的傳遞文件數(shù)據(jù)
- 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è)務(wù)代碼
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 { /** * 頁面跳轉(zhuǎn),跳轉(zhuǎn)到文件上傳頁面 * @return 跳轉(zhuǎn)到文件上傳頁面 */ @GetMapping(value = "/form_layouts") public String uploadPage() { return "form/form_layouts"; } /** * 文件上傳請求 * @param email 郵件 * @param username 用戶名 * @param headerImg 頭像文件 * @param photos 生活照 * @return 如果上傳文件成功,跳轉(zhuǎn)到首頁 */ @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("請求參數(shù)email{}, username{}, 頭像headerImg大小{}, 生活照photos張數(shù){}", 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的默認(rèn)文件中添加如下配置
# 單個文件最大限制 spring.servlet.multipart.max-file-size=10MB # 單次請求最大限制 spring.servlet.multipart.max-request-size=100MB
修改相關(guān)配置之后,文件上傳成功
四、文件上傳流程
文件上傳相關(guān)配置類MultipartAutoConfiguration
,相關(guān)配置類MultipartProperties
在MultipartAutoConfiguration
中我們自動配置好了文件上傳解析器StandardServletMultipartResolver
(它在容器中的beanName為multipartResolver)
然后我們跟著上面文件上傳的例子進(jìn)行一個debug,分析一下流程
首先,斷點還是來到DispatcherServlet下面的doDispatch()方法
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; // 設(shè)置文件解析默認(rèn)值為false boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { try { ModelAndView mv = null; Object dispatchException = null; try { // 檢查當(dāng)前請求是否涉及文件上傳 processedRequest = this.checkMultipart(request); // 將文件解析設(shè)置為true,表明當(dāng)前請求涉及文件上傳 multipartRequestParsed = processedRequest != request;
這里的processedRequest = this.checkMultipart(request);
會調(diào)用StandardServletMultipartResolver
類中的isMultipart()判斷當(dāng)前請求是否涉及文件上傳
如果涉及那么就會對當(dāng)前請求做一個處理,將原生的請求封裝成一個StandardMultipartHttpServletRequest
請求,把文件相關(guān)信息解析后放進(jìn)Map中(具體可以看StandardMultipartHttpServletRequest
類中的parseRequest方法)
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException { // 如果文件上傳解析器不為空,那么就調(diào)用StandardServletMultipartResolver類中的isMultipart()判斷當(dāng)前請求是否涉及文件上傳 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請求,把文件相關(guān)信息解析放進(jìn)Map中 return this.multipartResolver.resolveMultipart(request);
然后我們按照之前請求處理那篇博客里的路徑,從mv = ha.handle(processedRequest, response, mappedHandler.getHandler())
進(jìn)入
一直走到InvocableHandlerMethod
下面的getMethodArgumentValues()方法,深入斷點
我們得知,使用@RequestParam
注解的參數(shù)使用RequestParamMethodArgumentResolver
這個解析器
而文件相關(guān)入?yún)⑹鞘褂?code>@RequestPart注解的,它使用RequestPartMethodArgumentResolver
來進(jìn)行文件相關(guān)參數(shù)解析
在這個解析器中,他又會根據(jù)參數(shù)的名稱去上面checkMultipart()
方法所生成的Map中獲取文件相關(guān)信息
@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(); // 獲取文件上傳的參數(shù)名稱 String name = this.getPartName(parameter, requestPart); parameter = parameter.nestedIfOptional(); Object arg = null; // 根據(jù)參數(shù)名稱去獲取前面map中的value,也就是MultipartFile對象 Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
后面的調(diào)用鏈為MultipartResolutionDelegate.resolveMultipartArgument()
—>判斷當(dāng)前參數(shù)是否是文件上傳,如果是,繼續(xù)判斷是多文件上傳還是單文件上傳—>然后進(jìn)入AbstractMultipartHttpServletRequest
中,單文件走getFile()
從map中獲取文件信息,多文件走getFiles()
從map中獲取文件信息
最后,在控制器的目標(biāo)方法處使用MultipartFile類實現(xiàn)文件上傳的相關(guān)功能
到此這篇關(guān)于SpringBoot攔截器與文件上傳實現(xiàn)方法與源碼分析的文章就介紹到這了,更多相關(guān)SpringBoot攔截器與文件上傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringCloud:feign對象傳參和普通傳參及遇到的坑解決
這篇文章主要介紹了SpringCloud:feign對象傳參和普通傳參及遇到的坑解決,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03詳解Maven settings.xml配置(指定本地倉庫、阿里云鏡像設(shè)置)
這篇文章主要介紹了詳解Maven settings.xml配置(指定本地倉庫、阿里云鏡像設(shè)置),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-12-12Spring?Data?JPA系列QueryByExampleExecutor使用詳解
這篇文章主要為大家介紹了Spring?Data?JPA系列QueryByExampleExecutor使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09SpringBoot中的響應(yīng)式web應(yīng)用詳解
這篇文章主要介紹了SpringBoot中的響應(yīng)式web應(yīng)用詳解,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11Java的Hibernate框架中的繼承映射學(xué)習(xí)教程
Hibernate中的映射可以將類與表對應(yīng),并利用類的繼承特性,這里我們就來看一下Java的Hibernate框架中的繼承映射學(xué)習(xí)教程2016-07-07Java 高并發(fā)七:并發(fā)設(shè)計模型詳解
本文主要介紹Java高并發(fā) 并發(fā)設(shè)計模型的知識,這里主要講解 1. 什么是設(shè)計模式 2. 單例模式 3. 不變模式 4. Future模式 5. 生產(chǎn)者消費(fèi)者,有需要的小伙伴可以參考下2016-09-09