文件的上传和下载无时不在,文件是如何上传至服务器又是如何下载至服务器,文件在服务器的保存形式又是什么,浏览器又怎么知道这是个文件,这就是需要了解的地方。
目录
文件上传
上传的前提
在学习如何实现上传之前,需要先知道上传所需的要求:
1.必须通过表单,并且表单的请求方式为POST
2.表单的属性enctype的值必须是multipart/form-data
3.表单中需要附有file表单字段
第二点需要说明一下,设置这个属性的值为multipart/form-data是为了设置表单传递到服务器端的不是key-value形式而是传递的字节码形式。
上传的演示
手动解析
首先第一个问题:文件上传时,浏览器是将文件以什么样的形式提交到服务器端的?答:以流的形式
创建一个页面,代码如下所示:
<form action='<c:url value='/UploadServlet3'></c:url>' method="post" enctype="multipart/form-data">
用户名:<input type="text" name="username" /> <br/>
头像:<input type="file" name="portrait" /> <br/>
<input type="submit" value="提交" />
</form>
在浏览器F12查看请求内容,其如下所示:
在请求内容中会发现,每一个分隔符下面都有一个表单项的相关内容,我们称其为部件,每一个部件包含了请求头、空行、请求体。
普通的表单项只有一个请求头:Content-Disposition,即表单项的名称。而请求体则是表单项的值;
而文件表单项则包含了两个请求头:Content-Disposition、Content-Type,前者表示表单项的名称并且还附带文件名,后者表示文件的MIME类型。MIME类型这里不过多解释,这里只举一个例子:text/html就是MIME,表示后缀为html的文本文件。而请求体则是文件的内容。
第二个问题:如何获取到表单项的值?
在先前学习的时候,都是通过request.getParameter(String name)来获取表单项的值,但是这个方法返回的是一个字符串类型,在这已经不适用了。那么就得用另一个方法来获取表单项的值,即request.getInputStream()方法来获取ServletInputStream。这里提一下ServletInputStream类的作用,其提供流从请求对象读取二进制数据。
接下来演示一下使用ServletInputStream来得到表单的数据,代码如下所示:
public class UploadServlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response) {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
ServletInputStream in = request.getInputStream();
System.out.println(IOUtils.toString(in, "UTF-8"));
}
}
结果会发现读取的内容和在浏览器中看的内容是一致的。
接着第三个问题:如何将文件的内容提取出来并保存至WEB-INF文件夹内?
观察一下Http请求体中的结构,一行随机字符串标识符,一行内容文件头(即两个请求头),一行空行,第四行才是文件的内容。那么想要得到文件的二进制数据就得去掉前三行的内容。
代码如下所示:
public class UploadServlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response) {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
// 获得请求的数据流
ServletInputStream in = request.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
String firstLine = br.readLine();
System.out.println("随机字符串:" + firstLine);
String fileName = br.readLine();
/*
* 有些浏览器上传的文件是绝对路径,即带有盘符路径的文件
* 根据路径的特性,只需要找到最后的\即可,如果有这个符号则截取后半部分
* 需要注意的是获取最后部分的文件名时需要将双引号去掉
* */
fileName = fileName.substring(fileName.lastIndexOf("filename=") + 9).replace("\"", "");
int index = fileName.lastIndexOf("\\");
if (index != -1) {
fileName = fileName.substring(index + 1);
}
System.out.println("文件名:" + fileName);
System.out.println("Content-Type:" + br.readLine());
System.out.println("空行:" + br.readLine());
// 获取上传的文件夹路径的真实路径
String path = getServletContext().getRealPath("/WEB-INF");
PrintWriter out = new PrintWriter(path + "\\" + fileName);
System.out.println("文件内容:");
String content = null;
while((content = br.readLine()) != null) {
// System.out.println(content);
if (content.contains(firstLine + "--"))
break;
out.println(content);
}
out.close();
br.close();
in.close();
}
}
以上传图片为例,找到WEB-INF文件夹,图片成功上传,结果如下所示:
FileUpload
每次做上传功能都需要对流进行解析处理,未免过于麻烦,好在Apache已经编写了一个工具专门来处理解析,其名为FileUpload,它会帮忙解析request中的上传数据,解析后的结果是一个表单项数据封装到了一个FileItem对象中。
向编译器中导包,需要注意的是,不仅仅需要导入FileUpload的包还要导入一个名为commons-io的jar包,因为io包是FileUpload的依赖包。
在使用之前先介绍一下需要用到的几个类:DiskFileItemFactory、ServletFileUpload以及FileItem。
从API来看,DiskFileItemFactory是创建FileItem类的实例然后将内容保存在内存中或者硬盘中。其构造方法如下所示:
public DiskFileItemFactory()
public DiskFileItemFactory(int sizeThreshold, java.io.File repository)
其中,第一个参数sizeThreshold表示如果上传的文件小于这个设定的阈值就将文件保存在内存中,如果大于这个阈值就讲文件保存在硬盘里。默认值为10KB;第二个参数repository是临时目录,其默认值为System.getProperty("java.io.tmpdir")。
至于ServletFileUpload可以每个HTML部件处理多个文件,解析器的构造方法如下所示:
public ServletFileUpload()
public ServletFileUpload(FileItemFactory fileItemFactory)
ServletFileUpload通过其中的一个方法解析request,如下所示:
public java.util.List parseRequest(javax.servlet.http.HttpServletRequest request)
它会处理符合RFC 1867协议的多部分组件,至于RFC 1867概括地说就是符合上传的前提的协议。它会返回一个List<FileItem>的对象。
最后FileItem是一个接口类,实现类有两个,一个是DefaultFileItem,另一个是DiskFileItem。主要学习第二个,其有几个方法获取表单项相关的数据,如下所示:
// 判断是否为普通的表单项,true为普通表单项,false为文件表单项
public boolean isFormField();
// 返回当前表单项的名称
public String getFieldName();
// 返回表单项的值
public String getString(String charset);
// 返回上传的文件名称
public String getName()
// 返回上传文件的大小,即字节数
public long getSize();
// 返回上传文件对应的输入流
public InputStream getInputStream()
// 将上传的文件保存到指定文件
public void write(File file);
使用这个小工具的步骤如下:由工厂创建解析器,然后使用解析器解析request,得到FileItem集合,以获取需要的表单项数据。
了解完这三个类后,就可以开始编写代码演示:
public class UploadServlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response) {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
String path = request.getServletContext().getRealPath("/WEB-INF");
// 1.创建工厂类
DiskFileItemFactory factory = new DiskFileItemFactory();
// 2.创建ServletFileUpload实例
ServletFileUpload sfu = new ServletFileUpload(factory);
try {
// 3.解析请求
List<FileItem> fileItems = sfu.parseRequest(request);
for (FileItem fileItem : fileItems) {
if (fileItem.isFormField()) {
System.out.println("普通表单项演示:" + fileItem.getFieldName() +
" = " + fileItem.getString("UTF-8"));
} else {
System.out.println("文件表单项演示:");
System.out.println("Content-Type:" + fileItem.getContentType());
System.out.println("size:" + fileItem.getSize());
System.out.println("fileName:" + fileItem.getName());
File destFile = new File(path + "\\" + fileItem.getName());
fileItem.write(destFile);
}
}
} catch (FileUploadException e) {
throw new RuntimeException(e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
显示结果如下:
上传的几个注意事项
大小限制
所谓的大小限制可以分为两种:单个文件的大小限制以及整个请求所有数据的大小限制。需要注意的是,设置大小限制需要在解析之前就完成,否则没有效果。相关方法如下所示:
// 设置单个上传文件的最大字节
public void setFileSizeMax(long fileSizeMax);
// 设置整个请求的最大字节
public void setSizeMax(long sizeMax);
通过ServletFileUpload就可以使用这两个方法,但实际上这两个方法是FileUploadBase的方法,而FileUploadBase则是前者的父类。
当超出设置限制的值时,在解析request时会抛出异常,这个异常可用于后续的处理。
目录分配
这里说的其实是不能在WEB-INF下存放太多文件。为了不让一个目录下存放太多文件,使用哈希值来分配目录。具体的思路如下:
通过文件名获得哈希值,转换成16进制的哈希值后提取前两位来生成包含目录,即目录为两层,两位各一层。
还需要处理的问题就是:为了避免可能出现的同目录同名问题,在文件名前添加uuid。
演示代码如下:
public class UploadServlet04 extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response) {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
String path = request.getServletContext().getRealPath("/WEB-INF/uploads/");
// 1.创建工厂类
DiskFileItemFactory factory = new DiskFileItemFactory();
// 2.创建ServletFileUpload实例
ServletFileUpload sfu = new ServletFileUpload(factory);
try {
// 3.解析请求
List<FileItem> fileItems = sfu.parseRequest(request);
for (FileItem fileItem : fileItems) {
if (!fileItem.isFormField()) {
String fileName = fileItem.getName();
// 处理路径问题
int index = fileName.lastIndexOf("\\");
if (index != -1)
fileName = fileName.substring(index+1);
String saveName = CommonUtils.uuid() + "_" + fileName;
// 根据哈希值创建目录
String hashCode = Integer.toHexString(fileName.hashCode());
File dirFile = new File(path, hashCode.charAt(0) + "/" + hashCode.charAt(1));
dirFile.mkdirs();
// 保存文件
File destFile = new File(dirFile, saveName);
fileItem.write(destFile);
}
}
} catch (FileUploadException e) {
throw new RuntimeException(e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
结果如下所示:
文件下载
在文件上传的时候说过,上传的文件内容其实是一堆二进制字节码,那么下载其实就是将保存在服务器的文件变成一个字节数组,然后响应给浏览器,触发下载工具。
下载的要求
下载的时候,浏览器需要知道文件的类型(即MIME是什么)、文件名称以及最重要的文件的数据。
下载的演示
下载的使用比上传更加地简单,用例代码如下所示(以刚刚分配目录的文件为例):
public class DownLoadServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) {
String fileName = request.getParameter("name");
String path = this.getServletContext().getRealPath("/WEB-INF/uploads/");
// 根据文件名的哈希值确定目录
String hashCode = Integer.toHexString(fileName.hashCode());
File dirFile = new File(path, hashCode.charAt(0) + "/" + hashCode.charAt(1));
if(dirFile.exists()) {
File[] files = dirFile.listFiles();
for(File file : files) {
if (file.isFile()) {
if(file.getName().contains(fileName)) {
/*
* 上传的时候是以UUID_fileName为命名的
* 因此需要从_字符开始截取后面的文件名
*/
path = file.getPath();
fileName = path.substring(path.lastIndexOf("_") + 1);
String contentType = this.getServletContext().getMimeType(fileName);
String contentDisposition = "attachment;filename=" + new String(fileName.getBytes("GBK"), "ISO-8859-1");
FileInputStream input = new FileInputStream(file);
response.setHeader("Content-Type", contentType);
response.setHeader("Content-Disposition", contentDisposition);
ServletOutputStream output = response.getOutputStream();
IOUtils.copy(input, output);
input.close();
}
}
}
} else {
System.out.println("文件夹不存在");
return ;
}
}
}
网页代码如下所示:
<body>
<!-- 疯狂Java实战演义.pdf -->
<a href="<c:url value='/DownLoadServlet?name=无标题.png' />">点击这里下载</a>
</body>
结果如下所示:
现在讲解一下下载的各个知识点。如果只是单纯的一个下载的话,笼统地说,只需要三步即可完成:
1.获取文件的类型
2.获取文件的名称
3.通过真实路径获取文件,并且写入输出流中
这三步其实就是满足前面所提到的三个要求,而代码所体现的则是通过
String contentType = this.getServletContext().getMimeType(fileName);
String contentDisposition = "attachment;filename=" + fileName);
ServletOutputStream output = response.getOutputStream();
response.setHeader("Content-Type", contentType);
response.setHeader("Content-Disposition", contentDisposition);
IOUtils.copy(input, output);
设置Content-Type则是告诉浏览器:传递给浏览器的文件是什么MIME类型;具体则通过ServletContext中的getMimeType()方法获取MIME类型。
设置Content-Disposition则是让浏览器直接打开文件还是弹出下载窗口。其默认值为inline,即浏览器内打开。若想要弹出下载窗口则是另一个值attachment。而分号后面则是代表显示在下载窗口中的文件名,即下载文件到硬盘后的文件名。
需要注意的是下载框中显示中文名称乱码问题,一般情况而言,只需要对文件名作如下处理即可解决:
fileName = new String(fileName.getBytes("GBK"), "ISO-8859-1");
即将文件名的编码转成浏览器能够识别的ISO-8859-1编码。
而有一个浏览器比较特殊,火狐浏览器。它是BASE64编码的,而其他浏览器则是URL编码的。因此需要获取浏览器,如果是火狐则特殊处理,而其他都用通用方法即可。即主要代码如下所示:
public class DownUtils {
public static String fileNameEncoding(String fileName, HttpServletRequest request) {
String agent = request.getHeader("User-Agent"); // 获取浏览器
if (agent.contains("Firefox")) {
BASE64Encoder base64Encoder = new BASE64Encoder();
filename = "=?utf-8?B?" + base64Encoder.encode(filename.getBytes("UTF-8"))
+ "?=";
} else {
filename = URLEncoder.encode(filename, "UTF-8");
}
}
}
后话
'''
上传和下载没有多大的难度,只需要记住其中的思路以及一些细节即可
'''