Spring Boot文件上傳原理與實(shí)現(xiàn)詳解
1、文件上傳
文件上傳核心核心要點(diǎn):
- 文件通過(guò)前端表單或者ajax提交,文件上傳應(yīng)該使用enctype="multipart/form-data"標(biāo)簽。
- 前端文件上傳是面向多用戶(hù)的,多用戶(hù)之間可能存在上傳同一個(gè)名稱(chēng)、類(lèi)型的文件;為了避免文件沖突導(dǎo)致的覆蓋問(wèn)題這些應(yīng)該在后臺(tái)進(jìn)行解決!
- 對(duì)于文件名稱(chēng)采用UUID、雪花算法、MD5等一些哈希手段確保不會(huì)重復(fù);
- 對(duì)于用戶(hù)上傳的文件不能讓用戶(hù)輕易的獲取到,應(yīng)該將上傳的文件放在一個(gè)相對(duì)隱秘的或者禁止的路徑中。
- 針對(duì)不同場(chǎng)景應(yīng)該限制用戶(hù)上傳文件的類(lèi)型、大小;
- 后臺(tái)在處理文件上傳的時(shí)候應(yīng)該不應(yīng)該占用主線(xiàn)程,應(yīng)該使用異步的形式處理文件上傳;主線(xiàn)程繼續(xù)向下執(zhí)行代碼,異步的優(yōu)勢(shì)在于頁(yè)面不會(huì)白屏轉(zhuǎn)圈太久增強(qiáng)用戶(hù)體驗(yàn)!
2、文件上傳簡(jiǎn)單實(shí)現(xiàn)
2.1、編寫(xiě)前端頁(yè)面
- 文件上傳請(qǐng)求類(lèi)型必須是post請(qǐng)求
- 同時(shí)必須是enctype=“multipart/form-data”
- 可以通過(guò)accept設(shè)置上傳文件的類(lèi)型
- 多文件可以使用ctrl多選,標(biāo)簽中攜帶上multiple
<!DOCTYPE html> <html lang="en" xml> <head> <meta charset="UTF-8"> <title>文件上傳</title> </head> <body> <form method="post" action="/upload" enctype="multipart/form-data"> 單文件: <input type="file" name="headimg"><br/> <hr/> 多文件: <input type="file" name="photos" multiple><br/> <input type="submit" value="上傳"> </form> </body> </html>
2.2、Controller層
- 依據(jù)上傳核心應(yīng)該使用異步的形式,因此Controller線(xiàn)程中不應(yīng)該直接對(duì)文件處理;而應(yīng)該將文件交由Service層進(jìn)行異步處理,Controller線(xiàn)程繼續(xù)向下執(zhí)行處理未執(zhí)行完畢的代碼!
- @RequestPart注解用于標(biāo)注文件上傳參數(shù)
- MultipartFile參數(shù)是一個(gè)封裝IO流的簡(jiǎn)易文件處理接口,StandardMultipartFile實(shí)現(xiàn)類(lèi)。
@Controller public class FileController { @Autowired FileUploadService service; @RequestMapping("/upload") @ResponseBody public String upload(@RequestPart MultipartFile headimg, @RequestPart MultipartFile[] photos) throws IOException { System.out.println(" Controller線(xiàn)程: =============== "+Thread.currentThread().getName()+" ==========="); System.out.println("頭像大小: " + headimg.getSize()); System.out.println("照片數(shù)量: " + photos.length); service.upload(new MultipartFile[]{headimg}); service.upload(photos); return "File Upload Success!"; } }
2.3、Service層異步
- 針對(duì)用戶(hù)上傳的文件判斷文件是否存在、是否為空之類(lèi)的東西。
- 由于需要對(duì)文件進(jìn)行哈希避免沖突,因此需要將文件的類(lèi)型從名稱(chēng)中截取出來(lái)、然后另外使用哈希給文件生成一個(gè)隨機(jī)名稱(chēng)并且拼接文件類(lèi)型!
@Service @EnableAsync public class FileUploadService { @Async public void upload(MultipartFile[] file) throws IOException { System.out.println(" =========================== "+Thread.currentThread().getName()+" ==========="); int length = file.length; if(length > 0){ for(int i = 0;i < length;i++){ // 獲取文件的類(lèi)型 String type = file[i].getOriginalFilename().substring(file[i].getOriginalFilename().lastIndexOf(".")); System.out.println(type); // UUID、雪花算法、MD5等一些哈希算法對(duì)文件名進(jìn)行特殊處理,避免文件重名 String name = UUID.randomUUID().toString(); file[i].transferTo(new File("C:\\Users\\Splay\\Desktop\\上傳的文件\\" + name + type)); } } System.out.println("上傳完畢!"); } }
2.4、參數(shù)配置
springboot可以支持自定義的參數(shù)配置,用于限制上傳文件的大小。
spring: servlet: multipart: enabled: true max-file-size: 10MB # 單個(gè)文件大小 max-request-size: 100MB # 多文件總大小
3、文件上傳原理
首先文件上傳是通過(guò)請(qǐng)求發(fā)送出去的,那么肯定在中央調(diào)度DispatcherServlet中。
任何數(shù)據(jù)在網(wǎng)絡(luò)傳輸?shù)臅r(shí)候都是01比特串,因此只需要將文件上傳與普通參數(shù)一同看待即可!
```java protected void doDispatch(HttpServletRequest request, HttpServletResponse response) { // 1. 保存一個(gè)額外請(qǐng)求processedRequest HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; // 這里檢查是否異步請(qǐng)求 暫時(shí)忽略 WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { // 2. 檢查是不是文件上傳的請(qǐng)求 processedRequest = checkMultipart(request); // 3. 判斷檢查前后請(qǐng)求是否一致 multipartRequestParsed = (processedRequest != request); // 4. 拿到HandlerExecution執(zhí)行鏈 mappedHandler = getHandler(processedRequest); if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } // 查找適配器HandlerAdapter HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 請(qǐng)求方式解析 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; } } // 前置攔截器調(diào)用 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // 5. 所有參數(shù)解析并且執(zhí)行 mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); ... ... // 善后處理 } } }
3.1、整體調(diào)度
- 先將請(qǐng)求當(dāng)做一個(gè)普通請(qǐng)求processedRequest,然后checkMultipart(request)檢查本次請(qǐng)求是否是文件上傳。
- 檢查的方式很簡(jiǎn)單通過(guò)StandardServletMultipartResolver類(lèi)判斷form表單中的contentType是否為enctype=“multipart/form-data”。
public class StandardServletMultipartResolver implements MultipartResolver { @Override public boolean isMultipart(HttpServletRequest request) { return StringUtils.startsWithIgnoreCase(request.getContentType(), (this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/")); } } @Override public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException { // 返回一個(gè)文件上傳請(qǐng)求的對(duì)象 return new StandardMultipartHttpServletRequest(request, this.resolveLazily); }
- 如果是文件上傳那么會(huì)將本次請(qǐng)求調(diào)用resolveMultipart進(jìn)行解析一下并且封裝成一個(gè)新的請(qǐng)求。此時(shí)processRequest 一定不等于 request。
- 之后就是拿到HandlerExecution執(zhí)行鏈、查找HandlerAdapter適配器、請(qǐng)求方式method解析、調(diào)用preHandler前置攔截器做攔截。
3.2、設(shè)置與校驗(yàn)
- 即上面執(zhí)行完畢后,來(lái)到ha.handle()方法;所有上面在執(zhí)行controller時(shí)沒(méi)做的東西都會(huì)在這里執(zhí)行(請(qǐng)求方式驗(yàn)證、參數(shù)解析、反射調(diào)用controller…)
- 并且在這里會(huì)設(shè)置一堆的東西,例如:參數(shù)解析器(不同注解、類(lèi)型的參數(shù)由不同的解析器)、數(shù)據(jù)綁定器(DataBinder),之后數(shù)據(jù)解析與綁定就是交由DataBinder做。
- 再一堆雜七雜八的設(shè)置之后來(lái)到invokeForRequest方法,拿到參數(shù)之后調(diào)用doInvoke()反射執(zhí)行controller。
public class InvocableHandlerMethod extends HandlerMethod { @Nullable public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { // 參數(shù)解析 Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); if (logger.isTraceEnabled()) { logger.trace("Arguments: " + Arrays.toString(args)); } return doInvoke(args); //執(zhí)行Controller } }
3.3、參數(shù)解析大致流程
- 首先要避開(kāi)一個(gè)彎,參數(shù)是在調(diào)用controller之前解析完畢的
- 不同參數(shù)使用不同的參數(shù)解析器,這里采用了策略模式,supportsParameter方法中是一個(gè)增強(qiáng)for循環(huán);匹配合適的直接丟入map中,在第4步的解析中直接從map中獲?。?/li>
- 整個(gè)方法核心就是不同參數(shù)是如何適配到解析器的、參數(shù)又是如何解析的。
public class InvocableHandlerMethod extends HandlerMethod { protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception { // 1. 拿到前端上傳的所有參數(shù)名稱(chēng) MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } // 2. 參數(shù)分配空間 Object[] args = new Object[parameters.length]; for (int i = 0; i < parameters.length; i++) { MethodParameter parameter = parameters[i]; parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); args[i] = findProvidedArgument(parameter, providedArgs); if (args[i] != null) { continue; } // 3. 參數(shù)解析器的適配,不同參數(shù)會(huì)使用不同解析器 if (!this.resolvers.supportsParameter(parameter)) { throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver")); } try { // 4. 參數(shù)解析 args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); } .... } return args; } }
3.4、參數(shù)解析器的適配
這里只是適配每一個(gè)參數(shù)的解析器、并不會(huì)解析參數(shù);因此緩存池是非常有必要的,下次解析參數(shù)就可以直接從緩存池中拿!
@Nullable private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { // 緩存池便于 HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); if (result == null) { for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { if (resolver.supportsParameter(parameter)) { result = resolver; this.argumentResolverCache.put(parameter, result); break; } } } return result; }
- 文件上傳參數(shù)的解析器的適配是通過(guò)RequestPartMethodArgumentResolver類(lèi)判斷的。
- 這里直接判斷參數(shù)上的注解類(lèi)型是否為@RequestPart,而參數(shù)的信息在之前執(zhí)行過(guò)程中就已經(jīng)全部拿到了。
- 判斷為true之后這個(gè)RequestPartMethodArgumentResolver解析器就會(huì)被扔到上面的緩存池中便于下次直接獲取
public boolean supportsParameter(MethodParameter parameter) { // 直接判斷參數(shù)上的注解類(lèi)型是否為@RequestPart if (parameter.hasParameterAnnotation(RequestPart.class)) { return true; } else { if (parameter.hasParameterAnnotation(RequestParam.class)) { return false; } return MultipartResolutionDelegate.isMultipartArgument(parameter.nestedIfOptional()); } }
3.5、參數(shù)解析
由于前面鋪墊太多東西,參數(shù)解析就變得非常簡(jiǎn)單了。緩存拿到對(duì)應(yīng)的解析器、然后解析
@Override @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) { // map緩存池拿解析器 HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); if (resolver == null) { throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first."); } // 解析文件 return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); }
- 這里整體的流程就是先拿到參數(shù)注解判斷注解中的屬性情況,是否required、是否為空…
- 然后resolveMultipartArgument()方法判斷是單文件還是多文件上傳
- 找到對(duì)應(yīng)的HttpMessageConvert轉(zhuǎn)換器進(jìn)行對(duì)應(yīng)參數(shù)數(shù)據(jù)到目標(biāo)參數(shù)類(lèi)型的解析
- 最后將轉(zhuǎn)換器交由DataBinder進(jìn)行解析與數(shù)據(jù)綁定。
@Override @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest request, @Nullable WebDataBinderFactory binderFactory) { HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); Assert.state(servletRequest != null, "No HttpServletRequest"); // 拿到參數(shù)注解 RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class); // 注解是否必須,且是否為空 boolean isRequired = ((requestPart == null || requestPart.required()) &&!parameter.isOptional()); // 參數(shù)名 String name = getPartName(parameter, requestPart); parameter = parameter.nestedIfOptional(); Object arg = null; // 這里判斷是否文件上傳、并且是單文件還是多文件上傳 Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { arg = mpArg; ... HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(servletRequest, name); // 拿到convert轉(zhuǎn)換器 arg = readWithMessageConverters(inputMessage, parameter, parameter.getNestedGenericParameterType()); if (binderFactory != null) { ... // dataBinder參數(shù)解析,這里結(jié)束文件就成型了! WebDataBinder binder = binderFactory.createBinder(request, arg, name); .... return adaptArgumentIfNecessary(arg, parameter); }
到此這篇關(guān)于Spring Boot文件上傳原理與實(shí)現(xiàn)詳解的文章就介紹到這了,更多相關(guān)Spring Boot 文件上傳內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot項(xiàng)目中使用騰訊云發(fā)送短信的實(shí)現(xiàn)
本文主要介紹了SpringBoot項(xiàng)目中使用騰訊云發(fā)送短信的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04關(guān)于Java父類(lèi)沒(méi)有無(wú)參構(gòu)造方法子類(lèi)處理方法
父類(lèi)無(wú)參構(gòu)造方法,子類(lèi)不寫(xiě),其實(shí)會(huì)默認(rèn)調(diào)用父類(lèi)的無(wú)參構(gòu)造方法也就是用super(),編譯運(yùn)行后,會(huì)打印出"子類(lèi)會(huì)調(diào)用Father的第一個(gè)構(gòu)造方法,這篇文章給大家介紹關(guān)于Java父類(lèi)沒(méi)有無(wú)參構(gòu)造方法子類(lèi)處理方法,感興趣的朋友一起看看吧2024-01-01JavaWeb分頁(yè)的實(shí)現(xiàn)代碼實(shí)例
這篇文章主要介紹了JavaWeb分頁(yè)的實(shí)現(xiàn)代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12Java中Stream流對(duì)多個(gè)字段進(jìn)行排序的方法
我們?cè)谔幚頂?shù)據(jù)的時(shí)候經(jīng)常會(huì)需要進(jìn)行排序后再返回給前端調(diào)用,比如按照時(shí)間升序排序,前端展示數(shù)據(jù)就是按時(shí)間先后進(jìn)行排序,下面這篇文章主要給大家介紹了關(guān)于Java中Stream流對(duì)多個(gè)字段進(jìn)行排序的相關(guān)資料,需要的朋友可以參考下2023-10-10java使用Apache工具集實(shí)現(xiàn)ftp文件傳輸代碼詳解
這篇文章主要介紹了java使用Apache工具集實(shí)現(xiàn)ftp文件傳輸代碼詳解,分享了詳細(xì)連接ftp server和上傳文件,下載文件的代碼,以及結(jié)果展示,具有一定借鑒價(jià)值,需要的朋友可以參考下。2017-12-12java操作ElasticSearch聚合查詢(xún)的示例代碼
這篇文章主要介紹了java操作ElasticSearch聚合查詢(xún)的示例代碼,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2024-08-08mybatis逆向工程與分頁(yè)在springboot中的應(yīng)用及遇到坑
最近在項(xiàng)目中應(yīng)用到springboot與mybatis,在進(jìn)行整合過(guò)程中遇到一些坑,在此將其整理出來(lái),分享到腳本之家平臺(tái)供大家參考下2018-09-09MyBatis-Plus分頁(yè)時(shí)排序的實(shí)現(xiàn)方法
這篇文章主要介紹了MyBatis-Plus分頁(yè)時(shí)的排序,分頁(yè)時(shí)排序的方法,后端OrderItems排序、Wrapper排序前端指定排序,文章結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-03-03idea中同一SpringBoot項(xiàng)目多端口啟動(dòng)
本文主要介紹了idea中同一SpringBoot項(xiàng)目多端口啟動(dòng),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04Java對(duì)象轉(zhuǎn)json JsonFormat注解
這篇文章主要介紹了Java對(duì)象轉(zhuǎn)json JsonFormat注解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-05-05