声明

源码基于Spring Boot 2.3.12.RELEASE、Spring Framework 5.2.15.RELEASE

Servlet3.0 文件上传

Servlet 3.0对于HttpServletRequest接口增加了getParts方法,从而不用再借助apache commons-fileupload组件来获取文件相关信息。

/**
* 获取所有参数
*/
Collection<Part> getParts(); /**
* 根据参数名获取
*/
Part getPart(String name) throws IOException, ServletException;

对于文件上传提交,即content-type=multipart/form-data,还需要使用以下注解搭配才能使用上面的方法获取到信息。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultipartConfig { /**
* 文件上传时的临时路径
*/
String location() default ""; /**
* 单个文件最大值
* 默认-1,不限制
*/
long maxFileSize() default -1L; /**
* 整个请求的数据最大值
* 默认-1,不限制
*/
long maxRequestSize() default -1L; /**
* 每次写入磁盘的阈值
*/
int fileSizeThreshold() default 0;
}

下面看下使用例子

假设前台文件上传请求如下

key value
file1 in.txt
file2 in.txt
count 2
@MultipartConfig
@WebServlet(urlPatterns = "/uploadFile")
public class UploadServlet extends HttpServlet {
private static final long serialVersionUID = 318064779855484536L; /**
* @MultipartConfig注解必须,否则下面方法获取不到任何数据
* 即使是count参数
*/
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// {count=[2]}
Map<String, String[]> parameterMap = req.getParameterMap();
System.out.println(parameterMap);
// 2
System.out.println(req.getParameter("count"));
// 集合数目为3条,普通参数也会包含,只不过普通参数只能获取到name属性,其它的都是null
Collection<Part> parts = req.getParts();
for (Part part : parts) {
// 参数名字
System.out.println(part.getName());
// 文件类型, 如文本(text/plain), 图片(image/png)
System.out.println(part.getContentType());
// 文件名
System.out.println(part.getSubmittedFileName());
// 文件流
if (part.getName().equals("file1") || part.getName().equals("file2")) {
System.out.println(IOUtils.toString(part.getInputStream(), StandardCharsets.UTF_8));
}
System.out.println("=======================");
}
}
}

MultipartConfig代码配置

// 注册Servlet以及初始化配置
ServletRegistration.Dynamic registration = servletContext.addServlet(
"uploadServlet",
new UploadServlet()
);
registration.addMapping("/uploadFile");
// 全部使用默认值
registration.setMultipartConfig(new MultipartConfigElement(""));

SpringMVC文件上传

使用SpringMVC进行文件上传,我们需要给DispatcherServlet赋值一个multipartResolver。只需要往容器中注入一个id为multipartResolver的bean即可,DispatcherServlet初始化时会自动从容器中搜索id为multipartResolver进行赋值。

目前SpringMVC中给MultipartResolver组件提供了2种实现,如下所示

  • StandardServletMultipartResolver,依赖于Servlet 3.0 API,上面章节所述,Spring Boot项目默认方式。
  • CommonsMultipartResolver,依赖于apache commons-fileupload

只需往容器中注入即可(两者使用其一)

@Configuration
public class WebMvcConfig { /**
* 使用该方式时不要忘记给DispatcherServlet设置MultipartConfig
* 参考MultipartConfig代码配置
*/
@Bean
public MultipartResolver multipartResolver() {
return new StandardServletMultipartResolver();
} @Bean
public MultipartResolver multipartResolver() {
final long _1M = 1024 * 1024;
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
// 一次请求数据最大值
multipartResolver.setMaxUploadSize(_1M * 20);
// 编码
multipartResolver.setDefaultEncoding("UTF-8");
// 单个文件最大值
multipartResolver.setMaxUploadSizePerFile(_1M * 10);
return multipartResolver;
}
}

Controller

@RestController
public class UploadController { @PostMapping("/upload")
public ResponseEntity<String> upload(MultipartFile file1, MultipartFile file2, Integer count) throws IOException {
// 文件名
System.out.println(file1.getOriginalFilename());
// 文件类型 如文本(text/plain), 图片(image/png)
System.out.println(file1.getContentType());
String string1 = IOUtils.toString(file1.getInputStream(), StandardCharsets.UTF_8);
System.out.println(string1);
String string2 = IOUtils.toString(file2.getInputStream(), StandardCharsets.UTF_8);
System.out.println(string2);
System.out.println(count);
return ResponseEntity.of(Optional.of("success"));
}
}

SpringMVC 文件上传原理

SpringMVC将所有请求交给DispatcherServlet处理,对于文件上传请求,会使用MultipartResolver组件包装请求。

  1. 先看MultipartResolver组件初始逻辑

DispatcherServlet.java

@Override
protected void onRefresh(ApplicationContext context) {
// 初始各大组件
initStrategies(context);
} protected void initStrategies(ApplicationContext context) {
// 初始MultipartResolver组件
initMultipartResolver(context);
....
} private void initMultipartResolver(ApplicationContext context) {
try {
// 从容器中获取id为multipartResolver的bean
this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("Detected " + this.multipartResolver);
}
else if (logger.isDebugEnabled()) {
logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName());
}
}
catch (NoSuchBeanDefinitionException ex) {
// Default is no multipart resolver.
this.multipartResolver = null;
if (logger.isTraceEnabled()) {
logger.trace("No MultipartResolver '" + MULTIPART_RESOLVER_BEAN_NAME + "' declared");
}
}
}
  1. 借助MultipartResolver包装Request

DispatcherServlet.java

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);
...
}
}
} protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
if (this.multipartResolver != null &&
this.multipartResolver.isMultipart(request)) {
// 包装请求
return this.multipartResolver.resolveMultipart(request);
}
return request;
}
  1. StandardServletMultipartResolver
public class StandardServletMultipartResolver implements MultipartResolver {

	private boolean resolveLazily = false;

	public void setResolveLazily(boolean resolveLazily) {
this.resolveLazily = resolveLazily;
} /**
* 判断是否文件上传
*/
@Override
public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
} /**
* 包装请求为MultipartHttpServletRequest
* 该接口存在MultipartFile对象的方法
*/
@Override
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
} /**
* 清理操作
*/
@Override
public void cleanupMultipart(MultipartHttpServletRequest request) {
if (!(request instanceof AbstractMultipartHttpServletRequest) ||
((AbstractMultipartHttpServletRequest) request).isResolved()) {
// To be on the safe side: explicitly delete the parts,
// but only actual file parts (for Resin compatibility)
try {
for (Part part : request.getParts()) {
if (request.getFile(part.getName()) != null) {
part.delete();
}
}
}
catch (Throwable ex) {
LogFactory.getLog(getClass()).warn("Failed to perform cleanup of multipart items", ex);
}
}
} }

主要逻辑还是在StandardMultipartHttpServletRequest类中

  1. StandardMultipartHttpServletRequest
public StandardMultipartHttpServletRequest(HttpServletRequest request,
boolean lazyParsing) throws MultipartException {
super(request);
if (!lazyParsing) {
// 解析请求
parseRequest(request);
}
} private void parseRequest(HttpServletRequest request) {
try {
// 使用Servlet 3.0API
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);
}
} /**
* 根据参数名获取MultipartFile
*/
@Override
public MultipartFile getFile(String name) {
return getMultipartFiles().getFirst(name);
} /**
* 根据参数名获取MultipartFile列表
*/
@Override
public List<MultipartFile> getFiles(String name) {
List<MultipartFile> multipartFiles = getMultipartFiles().get(name);
if (multipartFiles != null) {
return multipartFiles;
}
else {
return Collections.emptyList();
}
}
  1. SpringMVC如何给Controller中方法的MultipartFile对象赋值呢?

SpringMVC给Controller方法中的参数赋值是借助HandlerMethodArgumentResolver组件实现的

public interface HandlerMethodArgumentResolver {

	/**
* 是否支持该参数
*/
boolean supportsParameter(MethodParameter parameter); /**
* 解析参数值
*/
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; }

而负责给MultipartFile对象赋值的接口实现为RequestParamMethodArgumentResolver

@Override
public boolean supportsParameter(MethodParameter parameter) {
// 忽略该方法其它逻辑
if (MultipartResolutionDelegate.isMultipartArgument(parameter)) {
return true;
}
} /**
* 如果参数类型是MultipartFile或者MultipartFile[]
* 或者List<MultipartFile>、Collection<MultipartFile>
*/
public static boolean isMultipartArgument(MethodParameter parameter) {
Class<?> paramType = parameter.getNestedParameterType();
return (MultipartFile.class == paramType ||
isMultipartFileCollection(parameter) || isMultipartFileArray(parameter) ||
(Part.class == paramType || isPartCollection(parameter) || isPartArray(parameter)));
}

再看解析参数值逻辑

/**
* 该方法在父类AbstractNamedValueMethodArgumentResolver的resolveArgument方法调用
*/
@Override
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); if (servletRequest != null) {
// 从请求参数中获取值
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
return mpArg;
}
} Object arg = null;
// 如果还没获取到,再次尝试获取
MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
if (multipartRequest != null) {
List<MultipartFile> files = multipartRequest.getFiles(name);
if (!files.isEmpty()) {
arg = (files.size() == 1 ? files.get(0) : files);
}
}
if (arg == null) {
String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
}
}
return arg;
}
/**
* 从封装好的MultipartHttpServletRequest对象中获取参数
* 该方法还会有一个兜底的处理逻辑,即便容器中没有配置MultipartResolver组件,也能成功获取到参数
*/
@Nullable
public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request)
throws Exception {
// 获取经过MultipartResolver包装好的MultipartHttpServletRequest请求对象
MultipartHttpServletRequest multipartRequest =
WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);
/*
* 判断是否是文件上传
* 如果是multipartRequest == null,则没有经过MultipartResolver处理
* isMultipartContent方法通过content-type是否以multipart/开头
*/
boolean isMultipart = (multipartRequest != null || isMultipartContent(request)); if (MultipartFile.class == parameter.getNestedParameterType()) {
// 参数类型是MultipartFile
if (multipartRequest == null && isMultipart) {
// 如果是文件上传请求且multipartRequest == null,手动构造
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
return (multipartRequest != null ? multipartRequest.getFile(name) : null);
}
else if (isMultipartFileCollection(parameter)) {
// 参数类型是Collection<MultipartFile>或者List<MultipartFile>
if (multipartRequest == null && isMultipart) {
// 如果是文件上传请求且multipartRequest == null,手动构造
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
return (multipartRequest != null ? multipartRequest.getFiles(name) : null);
}
else if (isMultipartFileArray(parameter)) {
// 参数类型是MultipartFile[]
if (multipartRequest == null && isMultipart) {
// 如果是文件上传请求且multipartRequest == null,手动构造
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
if (multipartRequest != null) {
List<MultipartFile> multipartFiles = multipartRequest.getFiles(name);
return multipartFiles.toArray(new MultipartFile[0]);
}
else {
return null;
}
}
else if (Part.class == parameter.getNestedParameterType()) {
return (isMultipart ? request.getPart(name): null);
}
else if (isPartCollection(parameter)) {
return (isMultipart ? resolvePartList(request, name) : null);
}
else if (isPartArray(parameter)) {
return (isMultipart ? resolvePartList(request, name).toArray(new Part[0]) : null);
}
else {
return UNRESOLVABLE;
}
}

通过该方法可知,即便没有给DispatcherServlet配置MultipartResolver组件,也能正确获取值。

Spring Boot文件上传默认配置

理解了SpringMVC中文件上传的原理后,在Spring Boot中那便很简单了,因为Spring Boot也只不过是做了一些自动配置而已。Spring Boot对于文件上传的自动配置类为MultipartAutoConfiguration

/**
* 该配置在Servlet 3.0以上环境生效,因为MultipartConfigElement类是3.0版本才有
* 可以通过spring.servlet.multipart.enabled=false关闭
*
* 该配置类非常简单,就是注册了Servlet3.0方式所需的两个类
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(MultipartProperties.class)
public class MultipartAutoConfiguration { private final MultipartProperties multipartProperties; public MultipartAutoConfiguration(MultipartProperties multipartProperties) {
this.multipartProperties = multipartProperties;
} /**
* 如果容器中没有MultipartConfigElement类型的bean
* 且没有CommonsMultipartResolver类型的bean(使用apache file upload)
* 就不需要MultipartConfigElement了
*/
@Bean
@ConditionalOnMissingBean({ MultipartConfigElement.class, CommonsMultipartResolver.class })
public MultipartConfigElement multipartConfigElement() {
return this.multipartProperties.createMultipartConfig();
} /**
* 如果容器中没有MultipartResolver类型的bean
*/
@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
@ConditionalOnMissingBean(MultipartResolver.class)
public StandardServletMultipartResolver multipartResolver() {
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
return multipartResolver;
} }
/**
* 属性配置
*/
@ConfigurationProperties(prefix = "spring.servlet.multipart", ignoreUnknownFields = false)
public class MultipartProperties { /**
* 是否启用自动配置
*/
private boolean enabled = true; /**
* 临时路径,为null,则使用tomcat默认路径
*/
private String location; /**
* 默认1M
*/
private DataSize maxFileSize = DataSize.ofMegabytes(1); /**
* 默认10M
*/
private DataSize maxRequestSize = DataSize.ofMegabytes(10); /**
* 默认值,与MultipartConfigElement fileSizeThreshold默认值也是0
*/
private DataSize fileSizeThreshold = DataSize.ofBytes(0); public MultipartConfigElement createMultipartConfig() {
MultipartConfigFactory factory = new MultipartConfigFactory();
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
map.from(this.fileSizeThreshold).to(factory::setFileSizeThreshold);
map.from(this.location).whenHasText().to(factory::setLocation);
map.from(this.maxRequestSize).to(factory::setMaxRequestSize);
map.from(this.maxFileSize).to(factory::setMaxFileSize);
return factory.createMultipartConfig();
} }

SpringMVC文件上传详解的更多相关文章

  1. Multipart/form-data POST文件上传详解

    Multipart/form-data POST文件上传详解 理论 简单的HTTP POST 大家通过HTTP向服务器发送POST请求提交数据,都是通过form表单提交的,代码如下: <form ...

  2. Multipart/form-data POST文件上传详解(转)

    Multipart/form-data POST文件上传详解 理论 简单的HTTP POST 大家通过HTTP向服务器发送POST请求提交数据,都是通过form表单提交的,代码如下: <form ...

  3. 【转】JSch - Java实现的SFTP(文件上传详解篇)

    JSch是Java Secure Channel的缩写.JSch是一个SSH2的纯Java实现.它允许你连接到一个SSH服务器,并且可以使用端口转发,X11转发,文件传输等,当然你也可以集成它的功能到 ...

  4. JSch - Java实现的SFTP(文件上传详解篇)

    JSch是Java Secure Channel的缩写.JSch是一个SSH2的纯Java实现.它允许你连接到一个SSH服务器,并且可以使用端口转发,X11转发,文件传输等,当然你也可以集成它的功能到 ...

  5. JSch - Java实现的SFTP(文件上传详解篇) [转载]

    文章来源:http://www.cnblogs.com/longyg/archive/2012/06/25/2556576.html JSch是Java Secure Channel的缩写.JSch是 ...

  6. 摘抄--使用cos实现多个文件上传详解

    在开发中常常需要上传文件,上传文件的方式有很多种,这里有一个cos实现的例子. 首先是要拷贝cos.jar包拷贝到WEB-INF/lib目录下,然后才进行编码. 创建一个可以进行自动重命名的Java文 ...

  7. JSch - Java实现的SFTP(文件上传详解篇)(转)

    JSch是Java Secure Channel的缩写.JSch是一个SSH2的纯Java实现.它允许你连接到一个SSH服务器,并且可以使用端口转发,X11转发,文件传输等,当然你也可以集成它的功能到 ...

  8. SWFUpload文件上传详解

    SWFUpload是一个flash和js相结合而成的文件上传插件,其功能非常强大. SWFUpload的特点: 1.用flash进行上传,页面无刷新,且可自定义Flash按钮的样式; 2.可以在浏览器 ...

  9. 文件上传详解 (HTML FILE)

    FileUpload 对象 在 HTML 文档中 <input type="file"> 标签每出现一次,一个 FileUpload 对象就会被创建. 该元素包含一个文 ...

  10. Java大文件上传详解及实例代码

    1,项目调研 因为需要研究下断点上传的问题.找了很久终于找到一个比较好的项目. 在GoogleCode上面,代码弄下来超级不方便,还是配置hosts才好,把代码重新上传到了github上面. http ...

随机推荐

  1. JZOJ 1082. 【GDOI2005】选址

    \(\text{Problem}\) 很久以前,在世界的某处有一个形状为凸多边形的小岛,岛上的居民们决定建一个祭坛,居民们任务祭坛的位置离岛的顶点处越远越好. 你的任务是求凸多边形内一点,使其与各顶点 ...

  2. NOIP2021游记总结

    \(\text{Day-1}\) 惨遭遣返······ 这真是伟大的啊!! \(\text{Day1}\) \(day\) 几好像没有意义,反正只有一天 \(\text{T1}\) 极致 \(H_2O ...

  3. 三天吃透MySQL八股文(2023最新整理)

    本文已经收录到Github仓库,该仓库包含计算机基础.Java基础.多线程.JVM.数据库.Redis.Spring.Mybatis.SpringMVC.SpringBoot.分布式.微服务.设计模式 ...

  4. webform项目 aspx页面顶部提示运行时错误(.Net Framwork已下载还是报错)

    找到项目属性页 选择对应的.Net 框架,点击保存,重启一下就好了

  5. PostGIS之空间连接

    1. 概述 PostGIS 是PostgreSQL数据库一个空间数据库扩展,它添加了对地理对象的支持,允许在 SQL 中运行空间查询 PostGIS官网:About PostGIS | PostGIS ...

  6. PostgreSQL Repmgr集群

    一.概述 repmgr是一套开源工具,用于管理PostgreSQL服务器群集内的复制和故障转移.它支持并增强了PostgreSQL的内置流复制,该复制流提供了一个读/写主服务器以及一个或多个只读备用数 ...

  7. Qt5 CMake项目简单模板

    cmake_minimum_required(VERSION 3.5) project(test VERSION 0.1 LANGUAGES CXX) set(CMAKE_INCLUDE_CURREN ...

  8. jsgrammer

    jsgrammer 计算机编程基础 能够说出什么是编程语言 能够区分编程语言和标记语言的不同 能够说出常见的数据存储单位及其换算关系 能够说出内存的主要作用以及特点 关键词:编程语言 计算机基础 编程 ...

  9. TypeError: 'int' object is not subscriptable 报错

    Python中报错TypeError: 'int' object is not subscriptable 原因:整形数据中加了下标索引 例如 #python utf-8 a = 10 b = a[0 ...

  10. datax缺少clickhouse reader插件

    背景:想要把click house的数据源同步到clickhouse,发现Datax没有clickhousereader组件. 1.把clickhousewriter/libs下的所有jar包复制到r ...