什么年代了,你还不知道 Servlet3.0 中的文件上传方式?

Posted _江南一点雨

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了什么年代了,你还不知道 Servlet3.0 中的文件上传方式?相关的知识,希望对你有一定的参考价值。

其实文件上传这块松哥之前和大家聊过很多次了,这次因为最近正在进行 SpringMVC 的源码分析,所以又再次把这个话题拉出来“鞭尸”,不过这次松哥想从源码角度来聊聊这个话题。

理解源码的前提是先会用,所以我们还是先来看看用法,然后再来分析源码。

1.两种文件解析方案

对于上传文件的请求,SpringMVC 中目前共有两种不同的解析方案:

  • StandardServletMultipartResolver
  • CommonsMultipartResolver

StandardServletMultipartResolver 支持 Servlet3.0 中标准的文件上传方案,使用非常简单;CommonsMultipartResolver 则需要结合 Apache Commons fileupload 组件一起使用,这种方式兼容低版本的 Servlet。

StandardServletMultipartResolver

先来回顾下 StandardServletMultipartResolver 的用法。

使用 StandardServletMultipartResolver,可以直接通过 HttpServletRequest 自带的 getPart 方法获取上传文件并保存,这是一种标准的操作方式,这种方式也不用添加任何额外的依赖,只需要确保 Servlet 的版本在 3.0 之上即可。

首先我们需要为 Servlet 配置 multipart-config,哪个 Servlet 负责处理上传文件,就为哪个 Servlet 配置 multipart-config。在 SpringMVC 中,我们的请求都是通过 DispatcherServlet 进行分发的,所以我们就为 DispatcherServlet 配置 multipart-config

配置方式如下:

<servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</serv
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-servlet.xml</param-value>
    </init-param>
    <multipart-config>
        <location>/tmp</location>
        <max-file-size>1024</max-file-size>
        <max-request-size>10240</max-request-size>
    </multipart-config>
</servlet>
<servlet-mapping>
    <servlet-name>springmvc</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

然后在 SpringMVC 的配置文件中提供一个 StandardServletMultipartResolver 实例,注意该实例的 id 必须为 multipartResolver(具体原因参见:SpringMVC 初始化流程分析一文)。

<bean class="org.springframework.web.multipart.support.StandardServletMultipartResolver" id="multipartResolver">
</bean>

配置完成后,我们就可以开发一个文件上传接口了,如下:

@RestController
public class FileUploadController 
    SimpleDateFormat sdf = new SimpleDateFormat("/yyyy/MM/dd/");

    @PostMapping("/upload")
    public String fileUpload(MultipartFile file, HttpServletRequest req) 
        String format = sdf.format(new Date());
        String realPath = req.getServletContext().getRealPath("/img") + format;
        File folder = new File(realPath);
        if (!folder.exists()) 
            folder.mkdirs();
        
        String oldName = file.getOriginalFilename();
        String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
        try 
            file.transferTo(new File(folder, newName));
            return req.getScheme() + "://" + req.getRemoteHost() + ":" + req.getServerPort() + "/img" + format + newName;
         catch (IOException e) 
            e.printStackTrace();
        
        return "error";
    

    @PostMapping("/upload2")
    public String fileUpload2(HttpServletRequest req) throws IOException, ServletException 
        StandardServletMultipartResolver resolver = new StandardServletMultipartResolver();
        MultipartFile file = resolver.resolveMultipart(req).getFile("file");
        String format = sdf.format(new Date());
        String realPath = req.getServletContext().getRealPath("/img") + format;
        File folder = new File(realPath);
        if (!folder.exists()) 
            folder.mkdirs();
        
        String oldName = file.getOriginalFilename();
        String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
        try 
            file.transferTo(new File(folder, newName));
            return req.getScheme() + "://" + req.getRemoteHost() + ":" + req.getServerPort() + "/img" + format + newName;
         catch (IOException e) 
            e.printStackTrace();
        
        return "error";
    

    @PostMapping("/upload3")
    public String fileUpload3(HttpServletRequest req) throws IOException, ServletException 
        String other_param = req.getParameter("other_param");
        System.out.println("other_param = " + other_param);
        String format = sdf.format(new Date());
        String realPath = req.getServletContext().getRealPath("/img") + format;
        File folder = new File(realPath);
        if (!folder.exists()) 
            folder.mkdirs();
        
        Part filePart = req.getPart("file");
        String oldName = filePart.getSubmittedFileName();
        String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
        try 
            filePart.write(realPath + newName);
            return req.getScheme() + "://" + req.getRemoteHost() + ":" + req.getServerPort() + "/img" + format + newName;
         catch (IOException e) 
            e.printStackTrace();
        
        return "error";
    

我这里一共提供了三个文件上传接口,其实最终都是通过 StandardServletMultipartResolver 进行处理的。

  1. 第一个接口是我们在 SpringMVC 框架中常见的一种文件上传处理方式,直接在参数中写上 MultipartFile,这个 MultipartFile 其实就是从当前请求中解析出来的,具体负责参数解析工作的就是 RequestParamMethodArgumentResolver。
  2. 第二个接口其实是一种古老的文件上传实现方案,参数就是普通的 HttpServletRequest,然后在参数里边,我们再手动利用 StandardServletMultipartResolver 实例进行解析(这种情况可以不用自己 new 一个 StandardServletMultipartResolver 实例,直接将 Spring 容器中的注入进来即可)。
  3. 第三个接口我们就利用了 Servlet3.0 的 API,调用 getPart 获取文件,然后再调用对象的 write 方法将文件写出去即可。

大致上一看,感觉办法还挺多,其实仔细看,万变不离其宗,一会我们看完源码,相信小伙伴们还能变化出更多写法。

CommonsMultipartResolver

CommonsMultipartResolver 估计很多人都比较熟悉,这个兼容性很好,就是有点过时了。使用 CommonsMultipartResolver 需要我们首先引入 commons-fileupload 依赖:

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

然后在 SpringMVC 的配置文件中提供 CommonsMultipartResolver 实例,如下:

<bean class="org.springframework.web.multipart.commons.CommonsMultipartResolver" id="multipartResolver">-->
</bean>

接下来开发文件上传接口就行了:

@PostMapping("/upload")
public String fileUpload(MultipartFile file, HttpServletRequest req) 
    String format = sdf.format(new Date());
    String realPath = req.getServletContext().getRealPath("/img") + format;
    File folder = new File(realPath);
    if (!folder.exists()) 
        folder.mkdirs();
    
    String oldName = file.getOriginalFilename();
    String newName = UUID.randomUUID().toString() + oldName.substring(oldName.lastIndexOf("."));
    try 
        file.transferTo(new File(folder, newName));
        return req.getScheme() + "://" + req.getRemoteHost() + ":" + req.getServerPort() + "/img" + format + newName;
     catch (IOException e) 
        e.printStackTrace();
    
    return "error";

这个就没啥好说,比较容易。

文件上传这块松哥之前在视频中也和大家分享过,公号后台回复 ssm 可以查看视频详情。

用法掌握了,接下来我们来看原理。

2.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/");
	
	@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()) 
			try 
				for (Part part : request.getParts()) 
					if (request.getFile(part.getName()) != null) 
						part.delete();
					
				
			
			catch (Throwable ex) 
			
		
	


这里满打满算就四个方法,其中一个还是 set 方法,我们来看另外三个功能性方法:

  1. isMultipart:这个方法主要是用来判断当前请求是不是文件上传请求,这里的判断思路很简单,就看请求的 content-type 是不是以 multipart/ 开头,如果是,则这就是一个文件上传请求,否则就不是文件上传请求。
  2. resolveMultipart:这个方法负责将当前请求封装一个 StandardMultipartHttpServletRequest 对象然后返回。
  3. cleanupMultipart:这个方法负责善后,主要完成了缓存的清理工作。

在这个过程中涉及到 StandardMultipartHttpServletRequest 对象,我们也来稍微说一下:

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

StandardMultipartHttpServletRequest 对象在构建的过程中,会自动进行请求解析,调用 getParts 方法获取所有的项,然后进行判断,将文件和普通参数分别保存下来备用。

这块的逻辑比较简单。

3.CommonsMultipartResolver

再来看 CommonsMultipartResolver。

先来看它的 isMultipart 方法:

@Override
public boolean isMultipart(HttpServletRequest request) 
	return ServletFileUpload.isMultipartContent(request);

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

ServletFileUpload.isMultipartContent 方法其实就在我们引入的 commons-fileupload 包中。它的判断逻辑分两步:首先检查是不是 POST 请求,然后检查 content-type 是不是以 multipart/ 开始。

再来看它的 resolveMultipart 方法:

@Override
public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException 
	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());
	

根据 resolveLazily 属性值,选择两种不同的策略将当前对象重新构建成一个 DefaultMultipartHttpServletRequest 对象。如果 resolveLazily 为 true,则在 initializeMultipart 方法中进行请求解析,否则先解析,再构建 DefaultMultipartHttpServletRequest 对象。

具体的解析方法如下:

protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException 
	String encoding = determineEncoding(request);
	FileUpload fileUpload = prepareFileUpload(encoding);
	try 
		List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
		return parseFileItems(fileItems, encoding);
	
	catch (FileUploadBase.SizeLimitExceededException ex) 
		以上是关于什么年代了,你还不知道 Servlet3.0 中的文件上传方式?的主要内容,如果未能解决你的问题,请参考以下文章

什么年代了,你还不知道 Servlet3.0 中的文件上传方式?

什么,你还不知道什么是JVM垃圾回收?!

你还不知道Nginx是什么?

iOS14:这些功能,可能你还不知道!

这些自动化测试框架知识你还不知道?

你还不知道为什么要学数据结构与算法?码起来!