一、文件上传
1.测试
表单
<form method="post" action="/upload" enctype="multipart/form-data">
<input type="file" name="file"><br>
<input type="submit" value="提交">
</form>
接口
@ResponseBody
@PostMapping(value = "/upload")
public String upload(@RequestPart("file") MultipartFile file, // 接收单个文件上传
@RequestPart("images") MultipartFile[] files){ // 多个文件上传
System.out.println(file);
System.out.println(files.length);
return "成功";
}
2.springboot配置项
配置 | 描述 | 默认值 |
---|---|---|
spring.servlet.multipart.enabled | 是否启用对分段上传的支持 | true |
spring.servlet.multipart.file-size-threshold | 文件写入磁盘的阈值。 | 0B |
spring.servlet.multipart.max-file-size | 最大文件大小。 | 1MB |
spring.servlet.multipart.max-request-sizex | 最大请求大小。 | 10MB |
3.上传源码
doDispatch()
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
// 检查是否为上传请求,如果是文件上传的请求,那么会将请求包装一层
processedRequest = checkMultipart(request);
// 不相等 则代表被包装了一层 是上传请求s
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
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;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
请求检查
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
// 使用文件解析器判断是否为上传请求
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
if (DispatcherType.REQUEST.equals(request.getDispatcherType())) {
logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
}
}
else if (hasMultipartException(request)) {
logger.debug("Multipart resolution previously failed for current request - " +
"skipping re-resolution for undisturbed error rendering");
}
else {
try {
// 处理请求
return this.multipartResolver.resolveMultipart(request);
}
catch (MultipartException ex) {
if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
logger.debug("Multipart resolution failed for error dispatch", ex);
// Keep processing error dispatch with regular request handle below
}
else {
throw ex;
}
}
}
}
// If not returned before: return original request.
return request;
}
isMultipart() 判断是否为文件上传请求
判断请求是否为文件上传请求,判断方式也比较简单,就是判断请求的内容类型是否以multipart
开头,这也就对应了我们在上传文件时候的表单必须设置一个enctype="multipart/form-data"
,其实这是在后台有对应的判断的
@Override
public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(),
(this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/"));
}
resolveMultipart() 请求封装
创建一个新的对象MultipartHttpServletRequest
,将request
包装起来
@Override
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}
解析文件封装进Map
从请求中解析出文件,将其添加到MultiValueMap
中
// 上面的方法创建了一个此对象
public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)
throws MultipartException {
super(request);
if (!lazyParsing) {
parseRequest(request);
}
}
// 转换请求
private void parseRequest(HttpServletRequest request) {
try {
// 从请求获取到文件信息
Collection<Part> parts = request.getParts();
// 文件参数名集合
this.multipartParameterNames = new LinkedHashSet<>(parts.size());
MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
// 遍历处理文件
for (Part part : parts) {
// 获取请求头值 例如:form-data; name="file"; filename="converters.png"
String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
// 将上面获取的请求头字符串处理成对象
ContentDisposition disposition = ContentDisposition.parse(headerValue);
// 获取上传文件名
String filename = disposition.getFilename();
if (filename != null) {
if (filename.startsWith("=?") && filename.endsWith("?=")) {
filename = MimeDelegate.decode(filename);
}
// 将文件封添加到集合里面
files.add(part.getName(), new StandardMultipartFile(part, filename));
}
else {
this.multipartParameterNames.add(part.getName());
}
}
// 设置文件,该方法为父类的方法
// 如果请求为文件上传的请求,那么该请求会被封装为AbstractMultipartHttpServletRequest
// 同时将解析后的文件设置为其中的属性
setMultipartFiles(files);
}
catch (Throwable ex) {
handleParseFailure(ex);
}
}
参数解析
如果为文件上传请求,那么上面已经把文件解析好了,那么接一下看如何将参数赋值
@RequestPart
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestPart {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
boolean required() default true;
}
RequestPartMethodArgumentResolver
参数解析器,根据配置的注解信息,从上面设置的Map里面获取对应的结果
public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver {
// 判断是否可以应用此参数处理器
@Override
public boolean supportsParameter(MethodParameter parameter) {
if (parameter.hasParameterAnnotation(RequestPart.class)) {
return true;
}
else {
if (parameter.hasParameterAnnotation(RequestParam.class)) {
return false;
}
return MultipartResolutionDelegate.isMultipartArgument(parameter.nestedIfOptional());
}
}
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest request, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// 获取原生的请求
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");
// 获取注解信息
RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class);
// 是否必填
boolean isRequired = ((requestPart == null || requestPart.required()) && !parameter.isOptional());
// 参数名
String name = getPartName(parameter, requestPart);
parameter = parameter.nestedIfOptional();
Object arg = null;
// 解析上传参数
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
// 如果不相等 代表获取到了
if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
arg = mpArg;
}
else {
try {
HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(servletRequest, name);
arg = readWithMessageConverters(inputMessage, parameter, parameter.getNestedGenericParameterType());
if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(request, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
}
catch (MissingServletRequestPartException | MultipartException ex) {
if (isRequired) {
throw ex;
}
}
}
// 异常判断
if (arg == null && isRequired) {
if (!MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
throw new MultipartException("Current request is not a multipart request");
}
else {
throw new MissingServletRequestPartException(name);
}
}
// 判断是否包装成Optional
return adaptArgumentIfNecessary(arg, parameter);
}
}
resolveMultipartArgument()
该方法根据形参的类型,从Map里面获取结果,大体上就是一个if / else判断
参数可以为MultipartFile
、Part
类型,类型可以单个对象,集合,数组,这样 2 x 3 = 6,就对应下面的6个判断
当判断通过以后,就是从Map里面获取集合
@Nullable
public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request)
throws Exception {
MultipartHttpServletRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
// 再次判断是否为文件上传请求
boolean isMultipart = (multipartRequest != null || isMultipartContent(request));
// 形参类型为MultipartFile.class
if (MultipartFile.class == parameter.getNestedParameterType()) {
if (!isMultipart) {
return null;
}
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
// 这里底层是从上面创建的MultiValueMap里面获取
return multipartRequest.getFile(name);
}
else if (isMultipartFileCollection(parameter)) {// 判断是否为Multipart集合
if (!isMultipart) {
return null;
}
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
List<MultipartFile> files = multipartRequest.getFiles(name);
return (!files.isEmpty() ? files : null);
}
else if (isMultipartFileArray(parameter)) {// 判断为Multipart数组
if (!isMultipart) {
return null;
}
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
List<MultipartFile> files = multipartRequest.getFiles(name);
return (!files.isEmpty() ? files.toArray(new MultipartFile[0]) : null);
}
else if (Part.class == parameter.getNestedParameterType()) {// 判断为Part.class
if (!isMultipart) {
return null;
}
return request.getPart(name);
}
else if (isPartCollection(parameter)) {// 判断为Part集合
if (!isMultipart) {
return null;
}
List<Part> parts = resolvePartList(request, name);
return (!parts.isEmpty() ? parts : null);
}
else if (isPartArray(parameter)) {// 判断为Part数组
if (!isMultipart) {
return null;
}
List<Part> parts = resolvePartList(request, name);
return (!parts.isEmpty() ? parts.toArray(new Part[0]) : null);
}
else {
return UNRESOLVABLE;
}
}
4.总结
- 如果
request
为文件上传请求,那么会将该请求包装成StandardMultipartHttpServletRequest
- 在包装请求的时候,会把文件参数都解析好,然后添加到一个Map里面
- 接收请求可以通过
@RequestParam
、@RequestPart
标注参数 - 上面两个注解对应的参数解析器里面,底层其实都是从上面创建的Map里面获取,然后直接返回