Spring Boot文件上傳原理與實現(xiàn)詳解
1、文件上傳
文件上傳核心核心要點:
- 文件通過前端表單或者ajax提交,文件上傳應(yīng)該使用enctype="multipart/form-data"標簽。
- 前端文件上傳是面向多用戶的,多用戶之間可能存在上傳同一個名稱、類型的文件;為了避免文件沖突導(dǎo)致的覆蓋問題這些應(yīng)該在后臺進行解決!
- 對于文件名稱采用UUID、雪花算法、MD5等一些哈希手段確保不會重復(fù);
- 對于用戶上傳的文件不能讓用戶輕易的獲取到,應(yīng)該將上傳的文件放在一個相對隱秘的或者禁止的路徑中。
- 針對不同場景應(yīng)該限制用戶上傳文件的類型、大??;
- 后臺在處理文件上傳的時候應(yīng)該不應(yīng)該占用主線程,應(yīng)該使用異步的形式處理文件上傳;主線程繼續(xù)向下執(zhí)行代碼,異步的優(yōu)勢在于頁面不會白屏轉(zhuǎn)圈太久增強用戶體驗!
2、文件上傳簡單實現(xiàn)
2.1、編寫前端頁面
- 文件上傳請求類型必須是post請求
- 同時必須是enctype=“multipart/form-data”
- 可以通過accept設(shè)置上傳文件的類型
- 多文件可以使用ctrl多選,標簽中攜帶上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線程中不應(yīng)該直接對文件處理;而應(yīng)該將文件交由Service層進行異步處理,Controller線程繼續(xù)向下執(zhí)行處理未執(zhí)行完畢的代碼!
- @RequestPart注解用于標注文件上傳參數(shù)
- MultipartFile參數(shù)是一個封裝IO流的簡易文件處理接口,StandardMultipartFile實現(xiàn)類。
@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線程: =============== "+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層異步
- 針對用戶上傳的文件判斷文件是否存在、是否為空之類的東西。
- 由于需要對文件進行哈希避免沖突,因此需要將文件的類型從名稱中截取出來、然后另外使用哈希給文件生成一個隨機名稱并且拼接文件類型!
@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++){
// 獲取文件的類型
String type = file[i].getOriginalFilename().substring(file[i].getOriginalFilename().lastIndexOf("."));
System.out.println(type);
// UUID、雪花算法、MD5等一些哈希算法對文件名進行特殊處理,避免文件重名
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 # 單個文件大小
max-request-size: 100MB # 多文件總大小

3、文件上傳原理
首先文件上傳是通過請求發(fā)送出去的,那么肯定在中央調(diào)度DispatcherServlet中。
任何數(shù)據(jù)在網(wǎng)絡(luò)傳輸?shù)臅r候都是01比特串,因此只需要將文件上傳與普通參數(shù)一同看待即可!
```java
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) {
// 1. 保存一個額外請求processedRequest
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
// 這里檢查是否異步請求 暫時忽略
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
// 2. 檢查是不是文件上傳的請求
processedRequest = checkMultipart(request);
// 3. 判斷檢查前后請求是否一致
multipartRequestParsed = (processedRequest != request);
// 4. 拿到HandlerExecution執(zhí)行鏈
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// 查找適配器HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 請求方式解析
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)度
- 先將請求當做一個普通請求processedRequest,然后checkMultipart(request)檢查本次請求是否是文件上傳。
- 檢查的方式很簡單通過StandardServletMultipartResolver類判斷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 {
// 返回一個文件上傳請求的對象
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}
- 如果是文件上傳那么會將本次請求調(diào)用resolveMultipart進行解析一下并且封裝成一個新的請求。此時processRequest 一定不等于 request。
- 之后就是拿到HandlerExecution執(zhí)行鏈、查找HandlerAdapter適配器、請求方式method解析、調(diào)用preHandler前置攔截器做攔截。
3.2、設(shè)置與校驗
- 即上面執(zhí)行完畢后,來到ha.handle()方法;所有上面在執(zhí)行controller時沒做的東西都會在這里執(zhí)行(請求方式驗證、參數(shù)解析、反射調(diào)用controller…)
- 并且在這里會設(shè)置一堆的東西,例如:參數(shù)解析器(不同注解、類型的參數(shù)由不同的解析器)、數(shù)據(jù)綁定器(DataBinder),之后數(shù)據(jù)解析與綁定就是交由DataBinder做。
- 再一堆雜七雜八的設(shè)置之后來到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ù)解析大致流程
- 首先要避開一個彎,參數(shù)是在調(diào)用controller之前解析完畢的
- 不同參數(shù)使用不同的參數(shù)解析器,這里采用了策略模式,supportsParameter方法中是一個增強for循環(huán);匹配合適的直接丟入map中,在第4步的解析中直接從map中獲??!
- 整個方法核心就是不同參數(shù)是如何適配到解析器的、參數(shù)又是如何解析的。
public class InvocableHandlerMethod extends HandlerMethod {
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs) throws Exception {
// 1. 拿到前端上傳的所有參數(shù)名稱
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ù)會使用不同解析器
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ù)解析器的適配
這里只是適配每一個參數(shù)的解析器、并不會解析參數(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ù)的解析器的適配是通過RequestPartMethodArgumentResolver類判斷的。
- 這里直接判斷參數(shù)上的注解類型是否為@RequestPart,而參數(shù)的信息在之前執(zhí)行過程中就已經(jīng)全部拿到了。
- 判斷為true之后這個RequestPartMethodArgumentResolver解析器就會被扔到上面的緩存池中便于下次直接獲取
public boolean supportsParameter(MethodParameter parameter) {
// 直接判斷參數(shù)上的注解類型是否為@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ù)解析就變得非常簡單了。緩存拿到對應(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()方法判斷是單文件還是多文件上傳
- 找到對應(yīng)的HttpMessageConvert轉(zhuǎn)換器進行對應(yīng)參數(shù)數(shù)據(jù)到目標參數(shù)類型的解析
- 最后將轉(zhuǎn)換器交由DataBinder進行解析與數(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文件上傳原理與實現(xiàn)詳解的文章就介紹到這了,更多相關(guān)Spring Boot 文件上傳內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot項目中使用騰訊云發(fā)送短信的實現(xiàn)
本文主要介紹了SpringBoot項目中使用騰訊云發(fā)送短信的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04
關(guān)于Java父類沒有無參構(gòu)造方法子類處理方法
父類無參構(gòu)造方法,子類不寫,其實會默認調(diào)用父類的無參構(gòu)造方法也就是用super(),編譯運行后,會打印出"子類會調(diào)用Father的第一個構(gòu)造方法,這篇文章給大家介紹關(guān)于Java父類沒有無參構(gòu)造方法子類處理方法,感興趣的朋友一起看看吧2024-01-01
java使用Apache工具集實現(xiàn)ftp文件傳輸代碼詳解
這篇文章主要介紹了java使用Apache工具集實現(xiàn)ftp文件傳輸代碼詳解,分享了詳細連接ftp server和上傳文件,下載文件的代碼,以及結(jié)果展示,具有一定借鑒價值,需要的朋友可以參考下。2017-12-12
mybatis逆向工程與分頁在springboot中的應(yīng)用及遇到坑
最近在項目中應(yīng)用到springboot與mybatis,在進行整合過程中遇到一些坑,在此將其整理出來,分享到腳本之家平臺供大家參考下2018-09-09
Java對象轉(zhuǎn)json JsonFormat注解
這篇文章主要介紹了Java對象轉(zhuǎn)json JsonFormat注解,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2020-05-05

