其实关于文件上传在最早之前是使用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这个首部。
二、 服务端编程
了解客户端是为了我们在服务端解析客户端发过来的请求,那么如何判断发过来的请求中是否包含文件呢?基于以下几点可以进行判断:
在一个由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;
三、 其他问题
上面两个部分主要就是说明了客户端和服务端分别怎么做,但是根据具体的业务逻辑还有很多别的需求需要考虑,如下:
对文件的后缀名进行验证和约束;对文件的大小进行约束;文件的存储,是放在本地文件系统上还是数据库中;文件的编码问题,尤其是中文的编码问题;避免相同文件名的文件的重复上传导致覆盖问题;这些问题实现起来其实都比较简单,下面有一篇文章可以进行参考,主要是怕考虑不到这些问题,或者为了省事偷工减料。