这篇文章主要是想介绍的是文件上传和下载,因为在很多时候,这两个是非常常见的,比如我们下载音乐或者电影,还有我们将简历上传到招聘网站,这些都会用到文件的上传和下载,因此,当我们掌握了这两个技术之后,就可以实现很多这方面的功能,而我们的技术实力也会根据学到更多的新知识而变强。
1.文件上传简介
什么是文件上传呢?其实就是将我们本地的文件传到服务器上面,而在 Java
中的话,想将本地文件传到服务器上面,肯定就是使用文件流了,其实文件上传的技术有很多,比如:
FileUpload
Servlet 3.0
Struts2
我们在这里的话,主要是想介绍的是使用 FileUpload
这个工具来上传文件,使用这个工具上传文件也比较简单,我们会在后面进行详细说明。
2.文件上传的原理
在说文件上传的原理之前,我们先看看文件上传必须有哪些要素,在上传文件时肯定是需要有一个表单的,那么这个表单元素需要满足哪些条件呢?其实是有 3
个的。
1.表单提交的方式必须是POST方式;
2.表单中需要有<input type="file">元素,并且该元素中需要有name属性和相应的值;
3.表单元素中的enctype属性需要赋值为multipart/form-data。
先说第 1
个,表单提交方式必须是 POST
方式,这是因为 GET
提交方式是有大小限制的,如果我们要上传的文件大小比较大的话,肯定就不行了;第 2
个,表单中需要有 <input type="file">
的元素,这是因为只有该元素才能让我们选择文件进行上传,不然的话我们也做不了上传操作;第 3
个,enctype
的取值必须是 multipart/form-data
,这是因为如果该属性取默认值 application/x-www-form-urlencoded
的话,那我们提交表单时,是不会将选中文件中的数据也带过去的,因此我们在服务器端是接受不到数据的,其实我们是可以进行演示的。
比如我们有一个表单,当我们为该表单的 enctype
属性赋值为默认值 application/x-www-form-urlencoded
时,表单的代码应该是:
<form action="${ pageContext.request.contextPath }/uploadServlet" method="post" enctype="application/x-www-form-urlencoded">
文件说明:<input type="text" name="info"><br>
文件上传:<input type="file" name="uploadFile"><br>
<input type="submit" value ="提交">
</form>
我们在第 1
个普通的文本输入项里面输入 aa
,然后在下面的文件选择项中选择 D
盘中的 aa.txt
时,需要说明的是 aa.txt
中的文本内容为 hello
,然后点击提交按钮,我们就可以看到请求的报文为:
POST /FileUpload/uploadServlet HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Content-Length: 25
Pragma: no-cache
Cache-Control: no-cache
Origin: http://localhost:8080
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.84 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://localhost:8080/FileUpload/jsp/upload.jsp
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: JSESSIONID=660141415A34D23305B59CD1D967FE7E
info=aa&uploadFile=aa.txt
其实前面的报文就是请求行和请求头了,最后一行才是请求体,但是我们观察请求体的话,就可以发现对于文件上传项来说,它只上传了文件的名称,而文件中具体包含什么内容是没有带上的,因此我们在后台也是接受不到数据的。
下面我们再将表单元素中的 enctype
属性赋值为 multipart/form-data
属性,相应的表单代码为:
<form action="${ pageContext.request.contextPath }/uploadServlet" method="post" enctype="multipart/form-data">
文件说明:<input type="text" name="info"><br>
文件上传:<input type="file" name="uploadFile"><br>
<input type="submit" value ="提交">
</form>
然后我们和上面一样,也是在普通文本输入项中输入 aa
,然后在文本选择项中选择文件 aa.txt
,最后也是点击上传,我们会看到相应的报文:
POST /FileUpload/uploadServlet HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Content-Length: 284
Pragma: no-cache
Cache-Control: no-cache
Origin: http://localhost:8080
Upgrade-Insecure-Requests: 1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryH8GeNXAhzwxrL3F1
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.84 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://localhost:8080/FileUpload/uploadServlet
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: JSESSIONID=660141415A34D23305B59CD1D967FE7E
----WebKitFormBoundaryH8GeNXAhzwxrL3F1
Content-Disposition: form-data; name="info"
aa
----WebKitFormBoundaryH8GeNXAhzwxrL3F1
Content-Disposition: form-data; name="uploadFile"; filename="D:\aa.txt"
Content-Type: text/plain
hello
----WebKitFormBoundaryH8GeNXAhzwxrL3F1--
其实比较两次的请求信息的话,可以发现其实两者在请求行和请求头的差别不是很大的,但需要注意的是第 2
次请求时的 Content-Type
请求头,里面有一个 boundary=----WebKitFormBoundaryH8GeNXAhzwxrL3F1
,这个是什么意思呢?其实 boundary
就是分隔符的意思,因此当我们看下面的请求体时,就会发现请求体中的内容是被 ----WebKitFormBoundaryH8GeNXAhzwxrL3F1
这个分隔符所分割开的,那我们就可以开始看请求体中的内容了。
----WebKitFormBoundaryH8GeNXAhzwxrL3F1
Content-Disposition: form-data; name="info"
aa
----WebKitFormBoundaryH8GeNXAhzwxrL3F1
Content-Disposition: form-data; name="uploadFile"; filename="D:\aa.txt"
Content-Type: text/plain
hello
----WebKitFormBoundaryH8GeNXAhzwxrL3F1--
可以很明显的看到,我们上面的分隔符将请求体分为了两个部分,第一个部分当然就是我们的普通文本输入项了,可以看到该普通项的 name
属性以及属性值,其实除了文件项之外的所有输入项我们都可以叫做普通项,那下面的一个就是文件上传项了,可以看到其中的 name
属性,然后文件上传项比普通项多的就是 filename
这个属性了,也就是我们上传的文件名称,最后的话也可以看到我们所上传文件里面的内容为 hello
。
介绍了上面的内容之后,我们知道,在提交的表单之中,是分为普通项和文件上传项的,那我们想写代码实现的话思路应该是怎样的呢?其实可以这样:
1.首先获得分隔符;
2.获得请求体的全部内容(request.getInputStream());
3.利用分隔符将获得的内容进行分割;
4.判断分割之后的部分是普通项还是文件上传项
普通项:获得普通项名称和值;
文件上传项:获得文件名称和文件内容,并通过文件流写到服务器上。
其实上面这也就是文件上传的原理了,我们后面编程也是根据这个原则来做的。
3.文件上传的代码实现
因为是使用的 FileUpload
这个工具,所以我们在使用之前必须要先导入相应的 jar
包,这里的话就是:
commons-fileupload-1.2.1.jar
commons-io-1.4.jar
上面还导入了 io
的包也是因为依赖的关系,毕竟底层还是使用的文件流。
因为上面在介绍原理的时候已经粘贴过前台 JSP
页面之中的表单元素了,因此我们这里就只介绍后端代码了。
UploadServlet
// 1.创建磁盘文件项工厂
DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
// 2.创建核心解析类
ServletFileUpload servletFileUpload = new ServletFileUpload(
diskFileItemFactory);
// 3.利用解析类解析请求对象,解析后得到多个部分。为一个List集合,其中元素则为文件项(FileItem)
List<FileItem> fileItemList = servletFileUpload.parseRequest(request);
// 4.遍历文件项集合,得到每个文件项对象,根据文件项判断是否为文件上传项
for (FileItem fileItem : fileItemList) {
// 判断该文件项是普通项还是文件上传项
if(fileItem.isFormField()) {
// 普通项
// 接受普通项的值,不能再使用request.getParameter()
String fieldName = fileItem.getFieldName();
String fieldValue = fileItem.getString(StandardCharsets.UTF_8.toString());
System.out.println(fieldName+"---"+fieldValue);
}else {
// 文件上传项
// 获取文件的名称
String fileName = fileItem.getName();
// 获取文件的数据
InputStream inputStream = fileItem.getInputStream();
// 获得文件上传的路径:磁盘绝对路径
String realPath = this.getServletContext().getRealPath("/upload");
OutputStream outputStream = new FileOutputStream(new File(realPath+"/"+fileName));
int len = 0;
byte[] bytes = new byte[1024];
while((len = inputStream.read(bytes))!= -1) {
outputStream.write(bytes, 0, len);
}
inputStream.close();
outputStream.close();
}
}
其实上面的代码就是根据我们之前整理的思路来写的,只是因为 FileUpload
这个工具为我们封装了很多工具类,因此使用起来就非常简单了,首先是创建一个磁盘文件项工厂,然后再将该工厂对象传入到我们核心解析器的构造方法之中,来获取核心解析器,然后就可以使用解析器来解析请求对象,获得一个包含普通项和文件上传项的集合对象,我们就可以遍历该集合对象,然后如果是普通项,则可以获取属性名以及属性值了,如果是文件上传项的话,则可以获取文件名称以及将文件数据写入到服务器当中了。当然我们在下面还会对各个重要类的 api
进行详细的介绍。
4.DiskFileItemFactory(磁盘文件项工厂)
我们在上面的代码中已经使用过 DiskFileItemFactory
这个类了,并且就是使用的它的构造方法,但是我们还需要注意的是其实它还有一个重载构造器方法,先将两个构造器方法分别列在下面:
public DiskFileItemFactory()
public DiskFileItemFactory(int sizeThreshold,java.io.File repository)
我们在这里可以了解一下第 2
个构造器方法中的 sizeThreshold
以及 repository
这两个参数分别代表什么意思呢?其实我们再看 DiskFileItemFactory
这个类的方法的话,会发现还有两个方法与我们上面的这两个参数有关,那就是:
public void setSizeThreshold(int sizeThreshold)
public void setRepository(java.io.File repository)
这里也就很明白了,就是说 sizeThreshold
和 repository
这两个参数既可以在类的构造器中指定,也可以单独使用 set
方法来进行指定,那到底这两个参数是什么意思呢?其实 sizeThreshold
这个参数是设置缓冲区的大小,如果我们不进行设置的话,就是取默认值 10kb
了,而 repository
则是设置临时文件存放的目录。那我们就可以在使用这两个参数来进行缓冲区以及临时文件目录的设置了。
// 1.创建磁盘文件项工厂
DiskFileItemFactory diskFileItemFactory = new DiskFileItemFactory();
// 1.1 设置缓冲区大小为3M
diskFileItemFactory.setSizeThreshold(3*1024*1024);
// 1.2 设置临时文件存放的目录
String tempPath = this.getServletContext().getRealPath("/temp");
diskFileItemFactory.setRepository(new File(tempPath));
当我们上传的文件大小比较大时,比如是上传的视频文件,那就可能会产生临时文件存放在临时目录了,其实这个临时文件也是可以用作断点续传的功能的,当然在这里文件上传成功之后临时文件也是不会删除的,如果想要上传成功之后删除临时文件,还需要使用下面会介绍的 FileItem
类。
5.ServletFileUpload(核心解析类)
下面我们再来看看 ServletFileUpload
这个核心解析类的相关 api
,在上面的代码中我们已经使用了它的构造器方法,其实它也是有 2
个构造器方法的。
public ServletFileUpload()
public ServletFileUpload(FileItemFactory fileItemFactory)
在上面我们是使用的有参构造器,即传入了磁盘文件项工厂对象的,其实我们也可以使用无参构造器,但是如果使用无参构造器的话,就需要在解析请求对象之前,调用 setFileItemFactory()
设置一个文件磁盘项工厂对象。
当然 ServletFileUpload
还有很多有用的方法,下面一一列举说明:
public static final boolean isMultipartContent(javax.servlet.http.HttpServletRequest request)
这个方法可以用来判断我们上传文件表单的 enctype
属性是否赋值为 multipart/form-data
,如果是那就会返回 true
,否则的话就会返回 false
,那我们就可以在后台的最开始使用该方法先进行判断一下,如果是的话才应该继续往下走,不是的话则应该给出提示,并返回到上传文件页面了。
boolean flag = ServletFileUpload.isMultipartContent(request);
if(!flag) {
// enctype属性不是multipart/form-data
request.setAttribute("msg", "上传文件表单格式不正确!请修改enctype属性!");
request.getRequestDispatcher("/jsp/upload.jsp").forward(request, response);
}
当然我们前台的上传文件 JSP
也应该加入相应的代码了。
<h3><font color="red">${ msg }</font></h3>
这样就能够判断上传文件表单中的 enctype
属性是否正确了。
当然这个类中最重要的方法应该还是解析请求对象,然后获取得到普通项和文件上传项的 List
集合这个方法了,这个方法是在 ServletFileUpload
的父类的父类 FileUploadBase
中的。
public java.util.List parseRequest(javax.servlet.http.HttpServletRequest req)
还有一些有用的方法,比如设置上传文件最大大小的。
public void setFileSizeMax(long fileSizeMax)
public void setSizeMax(long sizeMax)
上面两个方法虽然都是设置上传文件大小的,但还是有区别的,第 1
个方法 setFileSizeMax()
是设置单个文件的最大大小的,而第 2
个方法 setSizeMax()
则是设置一次请求中所有文件的大小的。
还有就是防止中文文件名乱码的方法。
public void setHeaderEncoding(java.lang.String encoding)
最后一个就是可以为我们上传文件时设置一个监听器,而我们就可以利用该监听器完成别的功能,比如显示文件上传的进度。
public void setProgressListener(ProgressListener pListener)
6.FileItem(文件项)
我们在表单元素中的每一个元素都对应着一个 FileItem
,如果是一个文件上传组件,那就对应着文件上传项,而如果是普通文本输入项,那就是普通项了,而我们要区分这两者的话,FileItem
中也提供了相应的方法。
boolean isFormField()
就是上面这个 isFormField()
这个方法,如果返回 true
的话,则表示是一个普通项,如果返回 false
的话,就表示是一个文件上传项。
如果是普通项的话,那我们最常见的操作就会获取其中的属性名称和属性值了,FileItem
中当然就已经有相应的方法了。
java.lang.String getFieldName()
java.lang.String getString()
java.lang.String getString(java.lang.String encoding)
第 1
个方法 getFieldName()
就是获得普通项的名称了,而后面的两个重载方法 getString()
则是获取普通项值的方法了,第 2
个重载方法可以传入一种字符集的名称,很显然这样就可以很好的支持中文,防止乱码了。
上面就是普通项会使用到的方法了,下面再看文件上传项会使用到的方法。
java.lang.String getName()
这个方法就是用来获取文件上传项中的名称的,当然对于文件上传项来说的话,其实就是文件名称了。
java.io.InputStream getInputStream()
这个方法就是用来获取文件内容的方法了,因为文件的格式是多种多样的,不仅有文本文件,还有音乐和视频,所以使用流的方式处理是最好的了。
long getSize()
这个方法是用来获取我们上传文件的大小的,之前 ServletFileUpload
类中不是有 setFileSizeMax()
方法来设置上传文件最大的大小吗?其实它还有一个 getFileSizeMax()
方法是用来获取文件最大限制大小的,那我们就可以先使用该获取文件大小的方法与限制文件最大的大小比较一下,看文件大小是否是在最大值以内。
之前在说 DiskFileItemFactory
类的时候,我们说到了临时文件,当我们文件上传成功之后,临时文件是不会被删除掉的,那如果我们想要删除掉的话,应该怎么办呢?其实可以使用下面这个方法:
void delete()
这样的话,在我们将文件成功上传之后,临时文件也会被删除了。
7.案例:多文件上传
现在是想做一个小功能,在页面中有两个按钮,一个添加按钮,一个提交按钮,当点击添加按钮时,就会出现一个文件选择框,而在文件选择框后面还跟着一个删除按钮,可以在页面中删除自身以及前面对应的文件选择框,这样我们就可以选择任意多个文件进行上传了,当最后点击上传按钮时,所有的文件就都可以上传了。
我们首先看前台的页面应该怎么写?先是一个表单中会有两个按钮:
<form action="${ pageContext.request.contextPath }/uploadServlet" method="post" enctype="multipart/form-data">
<input type="button" value="添加" onclick="add()"><input type="submit" value="提交">
<div id = "div1"></div>
</form>
然后点击添加按钮是会有新的文件选择框以及对应的删除按钮出现:
<script type="text/javascript">
function add() {
var div1 = document.getElementById("div1");
div1.innerHTML += "<div><input type='file' name='upload'><input type='button' value='删除' onclick='del(this)'></div>";
}
</script>
最后当然就是我们点击删除按钮时,会删除掉删除按钮本身以及前面的文件选择框了,其实就是还需要添加一个 js
方法。
function del(who) {
var divv = who.parentNode;
divv.parentNode.removeChild(divv);
}
这样的话,前台页面就达到要求了,那后台代码呢?其实后台代码是不需要改变的,因为不管我们在前台添加多少个文件选择框,在后台解析的时候,都是遍历处理的,因此是没有问题的。
8.文件上传相关问题
下面我们再看看进行文件上传时,可能会出现的问题,第 1
个就是对于一些比较老的浏览器来说,当我们使用 FileItem
类中的 getName()
方法获取文件名时,这时候文件名中可能是带有路径的,比如说 D://aa.txt
,而现在的浏览器一般都是 aa.txt
,因此我们就需要兼顾这两种情况。
String fileName = fileItem.getName();
int idx = fileName.lastIndexOf("\\");
if (idx != -1) {
fileName = fileName.substring(idx + 1);
}
这样的话,文件名中带有路径的问题就解决了。
第 2
个问题那就是文件同名的问题,比如有两个用户,他们都上传了 aa.txt
这个名称的文件,但是它们各自上传的文件里面的内容是不一样的,如果我们不做处理的话,肯定就会发生文件覆盖的情况,这样肯定是有问题的,因此我们就需要解决这个问题。
要想解决这个问题,我们可以进行文件名的转换,比如说小明这个用户上传了一个 aa.txt
的文件的话,那我们可以将这个文件的名称转换为一个唯一的名称,比如 4817717ad590496bb93850bc9e198494.txt
,而小红这个用户也上传一个 aa.txt
的话,那我们也可以同样的进行转换,比如为 76519c284323424aa6a353e382ba528e.txt
,这样的话即使两个用户上传的文件名是一样的,我们也可以进行区分了,也许有人会说,那你这样转换了之后,原来的文件名你不是拿不到了,其实在实际的开发过程中,我们可以将转换前的文件名和转换后的文件名都存入到数据库中,这样当用户需要取自己的文件时,再使用自己原来的文件名也就好了。关于获得唯一文件名的方法,我们可以先简单地使用 UUID
这个类来生成:
public static String getUuidFileName(String fileName) {
String preFileName = UUID.randomUUID().toString().replace("-", "");
int idx = fileName.lastIndexOf(".");
String extFileName = fileName.substring(idx);
return preFileName + extFileName;
}
下面再看第 3
个问题,就是一个目录中可能存放的文件个数过多,那我们要做的就是不要将文件全部放到一个目录中了,而是应该将文件分开存放,那我们应该采用怎样的方式来进行区分呢?其实可以按下面这些:
按时间来分:按月、日、小时来分
按用户来分:每个用户都有自己的文件目录
按个数来分:一个目录里面只存放固定数目的文件
下面想要介绍一种算法来确定文件存放的目录,首先还是将原文件名根据上面的方法转换为唯一的文件名,然后取它的 hashCode
值,当然就是一个 int
类型数据了,所以应该是一个 32
位的数据,然后我们就可以使用该 hashCode
值与 0xf
做位的与运算,这样就可以得到一个 0
到 15
之间的数,我们就将该数作为第一级目录,接下来我们将 hashCode
值无符号右移 4
位,再与 0xf
做位的与运算,这样得到的数作为第二级目录,因为 hashCode
值只有 32
位,因此肯定就只能运算 8
次,那我们就可以得到一个 8
级的目录了,想一下,有 8
级目录,每一级目录有 0
到 15
这 16
种取值,这样算的话,我们可以得到的总目录个数就是 16
的 8
次方了,很显然是可以满足我们的要求的。
public static String getRealPath(String uuidFileName) {
int hashCode = uuidFileName.hashCode();
int count = 0;
StringBuffer stringBuffer = new StringBuffer();
while (count < 8) {
stringBuffer.append("/");
int result = hashCode & 0xf;
stringBuffer.append(result);
hashCode >>>= 4;
count++;
}
return stringBuffer.toString();
}
9.文件下载简介
上面已经介绍了文件上传,下面就开始介绍文件下载了,其实文件下载也是非常常用的功能了,因为我们可能就会经常使用,比如音乐或者电影的下载,因此掌握了之后也能为我们写的网站添彩,其实文件下载功能的实现也是非常简单的。
其实文件下载的话就是将服务器上面的文件通过文件流的方式写到本地磁盘中,实现的方式有以下两种:
1.通过超链接的方式进行下载;
2.使用手动编码的方式进行下载。
下面我们分别开始介绍。
10.使用超链接方式进行下载
下载文件的话,首先服务器上面肯定是需要有文件的,因此我们就可以在 WebContent
下面新建一个 download
目录,然后在其中放入 1.jpg
图片和 aa.zip
压缩文件,这样的话,我们就可以下载这两个文件了。
而使用超链接的方式实现文件下载的话,就可以直接在前台页面写如下的代码:
<h2>文件下载:超链接方式</h2>
<h3>
<a href="${ pageContext.request.contextPath }/download/1.jpg">1.jpg</a>
</h3>
<h3>
<a href="${ pageContext.request.contextPath }/download/aa.zip">aa.zip</a>
</h3>
这样的话,就可以了,是的,只需要在前台使用 a
这个超链接标签就可以完成下载功能了,就是这么简单,不过需要说明的是,这种超链接下载文件的方式,有一个缺点就是,如果我们下载的是浏览器不支持的文件的话,那么浏览器就会提示我们进行下载了,但是如果我们下载的文件是浏览就支持的话,比如图片等,那浏览器就不会提示下载,而是直接打开了,因此这一点是需要注意的。
11.手动编码方式实现文件下载
要想使用编码方式实现下载的话,是需要满足一定条件的,需要设置响应对象的两个响应头和获得下载文件的输入流。
Content-Type:下载文件的MIME类型
Content-Disposition:设置该属性之后,即使是浏览器支持的文件格式,也会提示下载
下载文件的输入流,因为输出流是固定的,都是给浏览器端的
下面就直接看代码了,首先看前端的页面代码:
<h2>文件下载:编码方式</h2>
<h3>
<a href="${ pageContext.request.contextPath }/downloadServlet?fileName=1.jpg">1.jpg</a>
</h3>
<h3>
<a href="${ pageContext.request.contextPath }/downloadServlet?fileName=aa.zip">aa.zip</a>
</h3>
然后就是后台的代码了:
// 1.接受参数
String fileName = request.getParameter("fileName");
// 2.下载文件:设置两个头和一个流
// 设置Content-Type
String type = this.getServletContext().getMimeType(fileName);
response.setContentType(type);
// 设置Content-Disposition
response.setHeader("Content-Disposition", "attachment;filename="
+ fileName);
String realPath = this.getServletContext().getRealPath("/download");
String newPath = realPath + "/" + fileName;
// 获得文件的输入流
InputStream is = new FileInputStream(newPath);
ServletOutputStream os = response.getOutputStream();
int len = 0;
byte[] bytes = new byte[1024];
while ((len = is.read(bytes)) != -1) {
os.write(bytes);
}
is.close();
首先是设置 Content-Type
这个响应头,应该是设置文件类型所对应的 MIME
类型,当然我们的 ServletContext
对象是提供了方法根据文件名来获取对应的 MIME
类型的,也就是 getMimeType()
方法,其实文件的 MIME
类型是在 Tomcat
服务器的配置文件 web.xml
中进行配置的;接下来便是 Content-Disposition
这个响应头的设置,其实设置这个响应头时,很大一部分都是固定不变的,也就是 attachment;filename=
,后面则是需要加上文件名了;最后的话则是需要获取到下载文件的输入流了,然后和我们的响应流 response.getOutputStream()
进行对接,这样就可以完成文件下载的功能了。
12.中文文件的下载
如果要想能够顺利下载中文文件的话,那肯定就要考虑到中文字符乱码的问题了,以及各个浏览器对中文字符编码的方式了。
首先,如果下载文件的文件名是中文的话,那我们在后台获取文件名时,就应该要考虑到这种情况了,因此可以使用如下的代码。
// 1.接受参数
String fileName = new String(request.getParameter("fileName").getBytes("ISO-8859-1"), "UTF-8");
下面再看关于不同浏览器对于中文字符不同的编码方式,其实这里可以分为 2
种:
1.火狐浏览器是使用的Base64编码
2,其它浏览器,如IE、Chrome则是使用的URL编码
知道了这两种区别之后,那我们应该如何在代码中进行区分呢?其实是可以利用请求对象中的 User-Agent
这个请求头的,如果该请求头中包含 Firefox
这个字符串的话,那肯定就是火狐浏览器了,而其它的我们就可以采用另一种编码方式了,因此这样的话就可以区分出来了。
需要说明一下的是,对于火狐浏览器采用的 Base64
编码方式,我们这里可以使用一个 DownloadUtil
工具类,然后在其中提供一个 Base64
的编码方法,这样就可以满足我们的要求了,具体方法可以如下:
public static String base64EncodeFileName(String fileName) {
BASE64Encoder base64Encoder = new BASE64Encoder();
try {
return "=?UTF-8?B?"
+ new String(base64Encoder.encode(fileName
.getBytes("UTF-8"))) + "?=";
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
这个就是针对火狐浏览器时所需要使用的编码方式了,下面则是 IE
或者其它浏览器采用 URL
编码方式的情况了。
fileName = URLEncoder.encode(fileName, "UTF-8");
那么我们设置 Content-Disposition
时比较完整的代码就应该是下面这样了。
// 设置Content-Disposition
String agent = request.getHeader("User-Agent");
if (agent.contains("Firefox")) {
// 是火狐浏览器
fileName = DownloadUtil.base64EncodeFileName(fileName);
} else {
// 其它浏览器
fileName = URLEncoder.encode(fileName, "UTF-8");
}
response.setHeader("Content-Disposition", "attachment;filename="+ fileName);
为了能够下载中文名的文件,那我们的代码整体上也应该进行一些调整。
// 1.接受参数
String fileName = new String(request.getParameter("fileName").getBytes(
"ISO-8859-1"), "UTF-8");
// 2.下载文件:设置两个头和一个流
// 设置Content-Type
String type = this.getServletContext().getMimeType(fileName);
response.setContentType(type);
String realPath = this.getServletContext().getRealPath("/download");
String newPath = realPath + "/" + fileName;
// 设置Content-Disposition
String agent = request.getHeader("User-Agent");
if (agent.contains("Firefox")) {
// 是火狐浏览器
fileName = DownloadUtil.base64EncodeFileName(fileName);
} else {
// 其它浏览器
fileName = URLEncoder.encode(fileName, "UTF-8");
}
response.setHeader("Content-Disposition", "attachment;filename="
+ fileName);
// 获得文件的输入流
InputStream is = new FileInputStream(newPath);
ServletOutputStream os = response.getOutputStream();
int len = 0;
byte[] bytes = new byte[1024];
while ((len = is.read(bytes)) != -1) {
os.write(bytes);
}
is.close();
13.案例:给定目录下文件下载
我们现在想做的就是在页面中显示某个目录中所有的文件,并且当我们在页面中点击文件名称时,可以进行下载操作,那这样的话,其实是应该分为两部分的,第一部分就应该是显示某个目录中的所有文件列表,而且如果是多级目录的话,也应该能够遍历的到,第二部分则是下载操作了。
首先看看显示文件的部分,由于可能多级目录的存在,因此我们可以使用一个队列来存放我们需要遍历的目录,首先将根目录放到队列当中,然后就是遍历该队列,查看目录中的所有文件和目录,如果是文件的话,那就直接显示在页面上,而如果是目录的话,那就可以先加入到队列当中,以便再次进行循环。
下面直接看代码:
<%
// 1.创建一个队列
Queue<File> queue = new LinkedList<File>();
// 2.将根节点入队
File root = new File("D://Blog");
queue.offer(root);
// 3.对队列进行循环遍历
while (!queue.isEmpty()) {
File file = queue.poll();
File[] files = file.listFiles();
for (File f : files) {
if (f.isFile()) {
%>
<h4>
<a
href="${ pageContext.request.contextPath }/downloadListServlet?fileName=<%=f.getCanonicalPath()%>"><%=f.getName()%></a>
</h4>
<%
} else {
queue.offer(f);
}
}
}
%>
需要注意的一点就是因为各个文件所在的目录可能不一致,因此我们向后台 Servlet
发送请求时,就直接使用 getCanonicalPath()
方法获得文件的带路径完整文件名,这样在后台代码中也便于处理。
下面则是后台代码:
String path = new String(request.getParameter("fileName").getBytes(
"ISO-8859-1"), "UTF-8");
File file = new File(path);
String fileName = file.getName();
String type = this.getServletContext().getMimeType(fileName);
response.setContentType(type);
String agent = request.getHeader("User-Agent");
if (agent.contains("Firefox")) {
fileName = DownloadUtil.base64EncodeFileName(fileName);
} else {
fileName = URLEncoder.encode(fileName, "UTF-8");
fileName = fileName.replace("+", " ");
}
response.setHeader("Content-Disposition", "attachment;filename="
+ fileName);
// 设置输入流
InputStream is = new FileInputStream(path);
OutputStream os = response.getOutputStream();
int len = 0;
byte[] bytes = new byte[1024];
while ((len = is.read(bytes)) != -1) {
os.write(bytes, 0, len);
}
is.close();
因此这个案例也就写完了,其实和前面下载一个文件也是没什么实质区别的。
14.总结
文件的上传和下载就介绍完了,其实只要遵循相应的规则,也就能很容易实现我们想要的功能了,比如文件上传时表单需要满足哪些要素,然后就是 FileUpload
这个工具使用的步骤,掌握了这些也就可以掌握文件上传了,而文件下载时,则是需要设置两个响应头和获得一个文件的输入流,这样的话也就能掌握文件下载的实现了,其实很多东西都是这样,只要能掌握其中最基本的步骤就好办了。