当前位置: 首页 > article >正文

Spring MVC文件请求处理-MultipartResolver

Spring Boot中的MultipartResolver是一个用于解析multipart/form-data类型请求的策略接口,通常用于文件上传。
在这里插入图片描述
对应后端使用MultipartFile对象接收。

   @RequestMapping("/upload")
    public String uploadFile(MultipartFile file) throws IOException {
        String fileName = file.getOriginalFilename();
        String filePath = "D:\\";
        File file1 = new File(filePath+"\\"+fileName);
        file.transferTo(file1);
        return "ok";
    }

一、MultipartResolver接口

MultipartResolver是个接口,不做任何实现。

public interface MultipartResolver {  
	/**
	* 判断当前HttpServletRequest请求是否是文件请求
	*/
    boolean isMultipart(HttpServletRequest request);  
	/**
	*  将当前HttpServletRequest请求的数据(文件和普通参数)封装成MultipartHttpServletRequest对象
	*/
    MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;  
	/**
	*  清除文件上传产生的临时资源(如服务器本地临时文件)
	*/
    void cleanupMultipart(MultipartHttpServletRequest request);  
}

MultipartResolver在DispatcherServlet使用

我们都知道请求首先到达DispatcherServlet,它负责协调和组织不同组件完成请求处理并返回响应工作,所以DispatcherServlet中持有MultipartResolver成员变量,
在这里插入图片描述
在onRefresh中完成包括MultipartResolver在内的9个组件。
在这里插入图片描述

doDispatch中调用checkMultipart(request); 方法将请求转换为 multipart 请求,如该请求不是multipart 请求则返回原request

	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;
	}

DispatcherServlet处理文件请求会经过以下步骤:

1、判断当前HttpServletRequest请求是否是文件请求

  • 是:将当前HttpServletRequest请求的数据(文件和普通参数)封装成MultipartHttpServletRequest对象
  • 不是:不处理

2、DispatcherServlet对原始HttpServletRequest或MultipartHttpServletRequest对象进行业务处理
3、业务处理完成,清除文件上传产生的临时资源

二、MultipartResolver的实现类

Spring提供了两个MultipartResolver实现类:

  • StandardServletMultipartResolver:根据Servlet 3.0+ Part Api实现(默认使用)
  • CommonsMultipartResolver:根据Apache Commons FileUpload实现,需要引入相关的依赖

三、 StandardServletMultipartResolver

StandardServletMultipartResolver是根据Servlet 3.0+ Part Api实现,也是我们默认使用文件解析器。
在这里插入图片描述

public class StandardServletMultipartResolver implements MultipartResolver {

	private boolean resolveLazily = false;


	/**
	 * 设置是否延迟解析
	 * 可通过配置文件进行设置spring.servlet.multipart.resolve-lazily=true
	 * @since 3.2.9
	 */
	public void setResolveLazily(boolean resolveLazily) {
		this.resolveLazily = resolveLazily;
	}

	/**
	 * 判断是否是multipart请求
	 */
	@Override
	public boolean isMultipart(HttpServletRequest request) {
		return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
	}
	/**
	 * 返回StandardMultipartHttpServletRequest请求(主要部分)
	 */
	@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);
			}
		}
	}

}

3.1 StandardMultipartHttpServletRequest

构造方法

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

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

当resolveLazily为false时,在MultipartResolver#resolveMultipart()阶段并不会进行文件请求解析。也就是说,此时StandardMultipartHttpServletRequest对象的成员变量都是空值。那么,resolveLazily为false时文件请求解析是在什么时候完成的呢?
实际上,在调用StandardMultipartHttpServletRequest接口的getXxx()方法时,内部会判断是否已经完成文件请求解析。如果未解析,就会调用partRequest()方法进行解析,例如:
在这里插入图片描述

StandardMultipartHttpServletRequest#parseRequest

	private void parseRequest(HttpServletRequest request) {
		try {
			//调用getParts()方法从请求中获取所有的部分(parts),这些部分可能包括文件和表单数据
			Collection<Part> parts = request.getParts();
			//初始化集合存储所有非文件的参数名
			this.multipartParameterNames = new LinkedHashSet<>(parts.size());
			//存储每个文件的名称及其对应的MultipartFile对象。
			MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
			//对于每个部分,获取其Content-Disposition头,并解析出文件名。
			for (Part part : parts) {
				String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
				ContentDisposition disposition = ContentDisposition.parse(headerValue);
				String filename = disposition.getFilename();
				//如果文件名不为null,检测是否需要解码,添加到files中
				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);
		}
	}

3.2 CommonsMultipartResolver

CommonsMultipartResolver根据Apache Commons FileUpload实现,可以处理大文件、文件流等。功能比较强大
首先需要引入了commons-fileupload 依赖:

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

编写配置类

@Configuration
public class MultipartResolverConfig {

    @Bean
    public MultipartResolver multipartResolver() {
        CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
        // 文件请求解析配置:multipartResolver.setXxx()  
        multipartResolver.setResolveLazily(true);
        // 设置编码格式
        multipartResolver.setDefaultEncoding("UTF-8");
        // 设置写入内存的最大值(单位Byte),超过此值则写入硬盘临时文件
        multipartResolver.setMaxInMemorySize(1024 * 1024);
        // 设置上传文件最大值
        multipartResolver.setMaxUploadSize(1024 * 1024 * 1024);
        return multipartResolver;
    }

}

CommonsMultipartResolver解析器会根据请求方法和请求头来判断文件请求,源码如下:

	@Override
	public boolean isMultipart(HttpServletRequest request) {
		return ServletFileUpload.isMultipartContent(request);
	}
	
 public static final boolean isMultipartContent(HttpServletRequest request) {
        return !"POST".equalsIgnoreCase(request.getMethod()) ? false : FileUploadBase.isMultipartContent(new ServletRequestContext(request));
    }

3.2.1CommonsMultipartResolver#resolveMultipart#

CommonsMultipartResolver在解析文件请求时,会将原始请求封装成DefaultMultipartHttpServletRequest对象:

	@Override
	public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {
		Assert.notNull(request, "Request must not be null");
		// 判断是否是懒加载
		if (this.resolveLazily) {
			return new DefaultMultipartHttpServletRequest(request) {
				@Override
				protected void initializeMultipart() {
					MultipartParsingResult parsingResult = parseRequest(request);
					setMultipartFiles(parsingResult.getMultipartFiles());
					setMultipartParameters(parsingResult.getMultipartParameters());
					setMultipartParameterContentTypes(parsingResult.getMultipartParameterContentTypes());
				}
			};
		}
		else {
			MultipartParsingResult parsingResult = parseRequest(request);
			return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(),
					parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());
		}
	}

3.2.2 CommonsMultipartResolver#parseRequest

CommonsMultipartResolver使用流式处理文件,可以避免内存溢出
问题背景: 文件上传过程中,如果没有采用合适的策略,处理大文件时容易导致内存溢出(OutOfMemoryError)。这通常发生在将整个文件内容加载到内存中再进行处理时。
解决方法: CommonsMultipartResolver 使用了 流式处理,即它不会一次性将整个文件加载到内存中,而是通过流(stream)方式逐步读取文件的内容。这种方式确保了即使上传的是大文件,系统内存也不会被大量占用。这种流式读取方式能有效避免内存溢出问题。

public List<FileItem> parseRequest(RequestContext ctx)
            throws FileUploadException {
        List<FileItem> items = new ArrayList<FileItem>();
        boolean successful = false;
        try {
      	  // 获取文件迭代器,用于遍历 multipart/form-data 请求中的每个部分。每个部分可能是一个文件或者表单字段。
            FileItemIterator iter = getItemIterator(ctx);
            FileItemFactory fac = getFileItemFactory();
            if (fac == null) {
                throw new NullPointerException("No FileItemFactory has been set.");
            }
            while (iter.hasNext()) {
            /**
            * 使用 iter.hasNext() 遍历每个文件部分,item 代表当前处理的文件流。
			* fileName 从当前文件部分中获取文件名(这里没有使用 getName() 是为了避免异常)。
			* 调用 fac.createItem() 创建一个 FileItem,传入字段名、内容类型、是否是表单字段以及文件名,然后将 fileItem 添加到 items 列表中
			*/
                final FileItemStream item = iter.next();
                // Don't use getName() here to prevent an InvalidFileNameException.
                final String fileName = ((FileItemIteratorImpl.FileItemStreamImpl) item).name;
                FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(),
                                                   item.isFormField(), fileName);
                items.add(fileItem);
                try {
                // 将 item 的输入流(文件数据)通过 Streams.copy 复制到 fileItem 的输出流中,完成文件上传。
                    Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
                } catch (FileUploadIOException e) {
                    throw (FileUploadException) e.getCause();
                } catch (IOException e) {
                    throw new IOFileUploadException(format("Processing of %s request failed. %s",
                                                           MULTIPART_FORM_DATA, e.getMessage()), e);
                }
                final FileItemHeaders fih = item.getHeaders();
                fileItem.setHeaders(fih);
            }
            successful = true;
            return items;
        } catch (FileUploadIOException e) {
            throw (FileUploadException) e.getCause();
        } catch (IOException e) {
            throw new FileUploadException(e.getMessage(), e);
        } finally {
            if (!successful) {
                for (FileItem fileItem : items) {
                    try {
                        fileItem.delete();
                    } catch (Exception ignored) {
                        // ignored TODO perhaps add to tracker delete failure list somehow?
                    }
                }
            }
        }
    }

http://www.kler.cn/news/356958.html

相关文章:

  • 计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-10-19
  • 通过监控和警报驯服人工智能野兽
  • HL7协议简介及其在STM32上的解析实现
  • [计算机视觉]chapter1
  • Django 序列化serializers
  • 聊聊Go语言的异常处理机制
  • [H264]x264_encoder_headers函数
  • 并行编程实战——TBB框架的应用之三Supra的配置文件管理
  • Spring Boot 应用程序的 Controller 层中定义一个处理 HTTP DELETE 请求的方法
  • Python | Leetcode Python题解之第494题目标和
  • C++之const指针和const变量
  • 【Python】基础语法-输入输出
  • Mongodb基础用法【总结】
  • ‘perl‘ 不是内部或外部命令,也不是可运行的程序 或批处理文件。
  • JS异步编程进阶(二):rxjs与Vue、React、Angular框架集成及跨框架状态管理实现原理
  • 【React】事件绑定的方式
  • 【SSM详细教程】-03-Spring参数注入
  • 解锁A/B测试:如何用数据驱动的实验提升你的网站和应用
  • 过滤器Filter的介绍和使用
  • 聊聊 Facebook Audience Network 绑定收款账号的问题