Java Web基础知识之文件上传:文件上传一窥究竟

其实文件上传的文章已经写得很多了,但是好多文章都是都是说明了怎么实现,没有说这个过程到底发生了什么(会不会引来仇恨。。),其实实现文件上传并不复杂,也没有多少代码,但是要是清楚的明白其中的原理还是费点功夫的,这里就还原文件上传的整个过程。

其实关于文件上传在最早之前是使用Apache的Commons FileUpload组件,但是自从servlet提出了自己的解决办法之后,就不再使用这个组件了,有了正规军谁还使用民兵啊,不对,也不一定,之前Apache的HttpClient就比JDK自己的HttpUrlConnection流行,不说废话了,直接进入!

一、 客户端编程

下面是我们的页面FileUpload.jsp
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>File upload</title>
</head>
<body>
	<form action="/JavaServlet/fileUploadServlet" method="post" enctype="multipart/form-data">
		select a file :<input type="file" name="file" multiple>
		<input type="text" value="upload file" name="identifier" />
		<input type="submit" value="upload" />
	</form>
</body>
</html>
关于这个页面有几点值得注意:
  • 首先是action是URI,注意它和URL的区别,不要省掉contextPath,这样会找不到该资源的;
  • enctype的值一定是multipart/form-data,这个属性是指在发送放到服务器之前如何对表单数据进行编码,这种方式是将表单数据组装成一条消息,并用分隔符将表单的每个部分分隔开;默认值是application/x-www-form-urlencoded,也意味着所有的值都会进行编码,这种方式是用键值对来进行编码;
  • 如果想要上传多个文件,可以使用multiple属性,注意这个属性是在HTML5中提出的,这样就不用我们使用多个input来上传多个文件了;
下面我上传三个文件,可以看到Http请求和响应如下,我使用的是chrome浏览器:

这里边最重要的就是Content-type,这个类型和表单的enctype类型相同,最重要的就是增加了boundary属性,该属性的值就是用来分割表单中各个部分的。
下面是post表单时发出的request payload,如下:

从图中可以看出整个被上传的表单数据是被分隔符包裹起来,并且通过 使用"分隔符--"的方式来标明数据结束,这个分隔符在开头和结尾必须有又来说明数据的开始和结束,只有在表单中有多个元素或者上传多个文件时才会在中间出现,每一个被分隔符分隔的部分里面都包含Content-disposition首部,里面包含表单元素中的一些属性,有name,filename;但是content-type首部是可选的,而且 对于表单中非文件的部分是没有content-type的,只有文件域才会有content-type这个首部
值得注意的是,当没有文件上传的时候仍然会有文件部分存在,只不过filename属性为空,如下:

二、 服务端编程

了解客户端是为了我们在服务端解析客户端发过来的请求,那么如何判断发过来的请求中是否包含文件呢?基于以下几点可以进行判断:
  • 在一个由multipart/form-data组成的请求中,每一个部分包括非文件部分都会转换成一个Part对象,在服务器端我们主要是针对该Part对象进行处理;
  • 通过查看Part中是否存在content-type首部来判断一个Part是属于普通的非文件部分,还是属于文件部分;
  • 如果存在content-type,则说明文件部分存在,之后查看上传的文件名称是否为空,文件名为空说明有客户端没有选择要上传的文件;
  • 如果文件存在,就使用Part的write方法来将他写入服务器端的文件系统;
在服务器上处理文件上传的servlet如下:
@WebServlet(name="fileUploadServlet", urlPatterns={"/fileUploadServlet"})
//@MultipartConfig(location="/")
@MultipartConfig
public class FileUploadServlet extends HttpServlet {

	private static final long serialVersionUID = 1920423365061691218L;
	
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		Collection<Part> parts = req.getParts();
		
		for(Part part: parts){
			if(part.getContentType() != null){
				String filename = getFileName(part);
				if(filename != null && !filename.isEmpty()){
					part.write(filename);
				}
			} 
		}
	}
	
	
	String getFileName(Part part){
		Objects.requireNonNull(part, "part can not be null");
		
		String disposition = part.getHeader("content-disposition");
		String[] disParts = disposition.split(";");
		
		String filenamePart = disParts[disParts.length - 1];
		String filename = filenamePart.substring(filenamePart.indexOf("=")+1).trim().replace("\"", "");
		
		return filename;
	}
}
关于上述的处理其实主要围绕@MultipartConfig注解和Part接口来进行,关于这两个的使用其实很简单,可以查看一下JavaDoc即可,但是有两个我要着重说一下,因为我自己就掉坑里了:
  • 一个就是@MultipartConfig中的location属性,这个绝对是一个坑,当这个值是一个绝对路径时,调用Part的write()方法将该文件写到对应的路径是没有问题的,但是当是相对路径的时候,比如如我上边写的"/",这个相对路径是相对于tomcat路径下的C:\Program Files\tomcat7\work\Catalina\localhost\JavaServlet路径的,这是一个文件上传临时保存的位置,这个路径值主要是为了在文件超过预设大小时写入硬盘,为@MultiSizeThreshold准备,所以最好还是不要使用这个属性为好;
  • 还有一个就是Part中的getName()方法并不是用来获取文件名的,而是用来获取表单元素中的name属性的;文件名需要我们自己来解析出来;
  • 在使用Part的write()方法时,如果提供的是相对路径,那么相对路径的根路径都是C:\Program Files\tomcat7\work\Catalina\localhost\JavaServlet;

三、 其他问题

上面两个部分主要就是说明了客户端和服务端分别怎么做,但是根据具体的业务逻辑还有很多别的需求需要考虑,如下:
  • 对文件的后缀名进行验证和约束;
  • 对文件的大小进行约束;
  • 文件的存储,是放在本地文件系统上还是数据库中;
  • 文件的编码问题,尤其是中文的编码问题;
  • 避免相同文件名的文件的重复上传导致覆盖问题;
这些问题实现起来其实都比较简单,下面有一篇文章可以进行参考,主要是怕考虑不到这些问题,或者为了省事偷工减料。

猜你喜欢

转载自blog.csdn.net/super_wu1992/article/details/77532517