【springmvc】处理文件上传

本文介绍SpringMVC中文件上传的实现方式,包括通过CommonsFileUpload和Servlet3.0进行文件上传的方法,并分析了其核心组件MultipartResolver的工作原理。

Spring MVC提供MultipartResolver接口来实现对文件上传的支持,Spring MVC本身并未有此接口的实现,需借助第三方jar实现。

MultipartResolver的两种实现:

  • Commons FileUpload
  • Servlet 3.0

Commons FileUpload上传文件的使用

要使用Commons FileUpload首先需要引入如下依赖:

<dependency>
	<groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>

然后向spring web容器中注入一个名为multipartResolver的MultipartResolver对象(注意名字必须为multipartResolver,后面看源码就知道为什么?)

@Bean
public MultipartResolver multipartResolver() {
    return new CommonsMultipartResolver();
}

在上传文件的请求方法中加入MultipartFile参数,spring mvc会自动将文件封装成此参数:

package com.morris.spring.mvc.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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;

/**
 * 演示文件的上传
 */
@Slf4j
@Controller
@RequestMapping("file")
public class FileController {

	@GetMapping("index")
	public String index() {
		return "file";
	}

	/**
	 * 不需要MultipartResolver
	 * @param file
	 * @return
	 */
	@PostMapping("uploadFile")
	public String uploadFile(@RequestBody String file) {
		log.info("file content is {}", file);
		return "success";
	}

	/**
	 * 需要MultipartResolver,否则无法解析
	 * @param file
	 * @param desc
	 * @return
	 * @throws IOException
	 */
	@PostMapping("uploadFile2")
	public String uploadFile(MultipartFile file, String desc) throws IOException {
		file.transferTo(new File("d:\\" + file.getOriginalFilename()));
		log.info("file content is {}", file);
		log.info("file description is {}", desc);
		return "success";
	}

}

注意:@RequestParam("file")注解可以不加,但是参数名必须与页面表单的字段名保持一致,如MultipartFile file

在html页面上传时, enctype必须为multipart/form-data

<form action="/file/uploadFile" method="post" enctype="multipart/form-data">
    Choose File1: <input type="file" name="file">
    <br/>
    Description: <input type="text" name="desc">
    <br/>
    <input type="submit" value="Submit">
</form>

<br/><br/>
<form action="/file/uploadFile2" method="post" enctype="multipart/form-data">
    Choose File2: <input type="file" name="file">
    <br/>
    Description: <input type="text" name="desc">
    <br/>
    <input type="submit" value="Submit">
</form>

Commons FileUpload的源码分析

DispatchServlet注入MultipartResolver

Spring MVC容器在启动后会发送一个ContentRefresh消息,DispatchServlet监听到消息后会调用如下方法:

org.springframework.web.servlet.DispatcherServlet#initMultipartResolver

public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";

private void initMultipartResolver(ApplicationContext context) {
	...
		this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
	...
}

Spring MVC会在容器中寻找一个名为multipartResolver的MultipartResolver,所以上面注入的MultipartResolver的名字只能是multipartResolver

将请求包装为MultipartHttpServletRequest

当一个请求过来时,Spring MVC都会检查此请求中是否有文件,如果有就调用MultipartResolver(前提是已注入MultipartResolver,否则会直接返回)的resolveMultipart方法,这里会将请求包装成MultipartHttpServletRequest:

org.springframework.web.servlet.DispatcherServlet#checkMultipart

protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
	if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
		if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
			if (request.getDispatcherType().equals(DispatcherType.REQUEST)) {
				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;
}

怎么判断一个请求是否有文件?来自multipartResolver.isMultipart方法:

org.springframework.web.multipart.commons.CommonsMultipartResolver#isMultipart

public boolean isMultipart(HttpServletRequest request) {
	return ServletFileUpload.isMultipartContent(request);
}

org.apache.commons.fileupload.servlet.ServletFileUpload#isMultipartContent

public static final boolean isMultipartContent(
	HttpServletRequest request) {
	if (!POST_METHOD.equalsIgnoreCase(request.getMethod())) {
		return false;
	}
	return FileUploadBase.isMultipartContent(new ServletRequestContext(request));
}

org.apache.commons.fileupload.FileUploadBase#isMultipartContent(org.apache.commons.fileupload.RequestContext)

public static final boolean isMultipartContent(RequestContext ctx) {
	String contentType = ctx.getContentType();
	if (contentType == null) {
		return false;
	}
	if (contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART)) {
		return true;
	}
	return false;
}

当请求是一个POST请求并且ContentType以multipart/开头就会认为是一个文件请求。

Servlet3.0上传文件的使用

Servlet3.0中使用文件上传,首先需要开启Servlet容器的配置(不加MultipartConfigElement,文件无法上传),在无web.xml中可以如下配置:

@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
	MultipartConfigElement multipartConfigElement = new MultipartConfigElement("/tmp", -1, -1, 100*1024);
	registration.setMultipartConfig(multipartConfigElement);
}

在controller中与Commons FileUpload中的使用一致。

MultipartConfigElement可以限制上传文件的临时目录(这个临时目录在必须先创建好,servlet容器不会自动创建,不存在则会抛出异常),上传文件的大小,文件大小超过多少会生成临时文件(这个临时文件生成在临时目录中,请求完成后会自动删除),单位为byte。

注意:Servlet3.0的文件上传完全由Servlet容器完成,与spring mvc无关,MultipartConfigElement是servlet api。

Servlet3.0上传文件的源码分析

StandardServletMultipartResolver的自动注入

当一个文件上传时,Servlet容器会根据MultipartConfigElement来自动上传文件(根据配置是否上传至临时目录),如果没有配置MultipartConfigElement,文件将不会上传。

Spring MVC会根据目标方法中是否包含MultipartFile参数来确定是否使用StandardMultipartHttpServletRequest,代码如下:

org.springframework.web.multipart.support.MultipartResolutionDelegate#resolveMultipartArgument

public static MultipartRequest resolveMultipartRequest(NativeWebRequest webRequest) {
	MultipartRequest multipartRequest = webRequest.getNativeRequest(MultipartRequest.class);
	if (multipartRequest != null) {
		return multipartRequest;
	}
	HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
	if (servletRequest != null && isMultipartContent(servletRequest)) {
		// 包装为StandardMultipartHttpServletRequest
		return new StandardMultipartHttpServletRequest(servletRequest);
	}
	return null;
}

StandardMultipartHttpServletRequest的构造方法中会对servlet容器上传的文件进行解析:

public StandardMultipartHttpServletRequest(HttpServletRequest request) throws MultipartException {
	this(request, false);
}

public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)
	throws MultipartException {

	super(request);
	if (!lazyParsing) {
		parseRequest(request);
	}
}

其实,在调用Spring MVC的DispatchServlet之前,文件已经上传至临时目录,SpringMVC只是将文件的路径封装一下,供目标方法使用:

private void parseRequest(HttpServletRequest request) {
	try {
		// 从request直接拿上传好的文件进行再次封装
		Collection<Part> parts = request.getParts();
		this.multipartParameterNames = new LinkedHashSet<>(parts.size());
		MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
		for (Part part : parts) {
			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());
			}
		}
		setMultipartFiles(files);
	}
	catch (Throwable ex) {
		handleParseFailure(ex);
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

morris131

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值