目录
文件上传概述
实现web开发中的文件上传功能,需要完成如下二步操作:
- 在web页面中添加上传输入项
- 在servlet中读取上传文件的数据,并保存到本地硬盘中
如何在web页面中添加上传输入项
<input type='file'>
标签用于在web页面中添加上传输入项,设置文件上传输入项的时需要注意:必须要设置
input
输入项的name
属性,否则浏览器将不会发送上传文件的数据。这就是之前发现的那个,如果一个标签没有name
属性,是不会被提交的 。必须把
form
的enctype
属性设置multipart/form-data
, 设置该值后,浏览器在上传文件时,把文件数据附带在http
请求消息体 中,并使用MIME
协议对上传的文件进行描述,以方便接收方对上传数据进行解析和处理 。数据格式
:这种表单提交,和以前的表单提交不一样,比如以前获取用户名,直接获取提交参数,就好了;使用了MIME
协议以后,提交的数据格式将不再和以前一样,格式如下所示,因此,要获取其中的额数据,我们需要去使用特定的解析 ;-----------------------------24464570528145 Content-Disposition: form-data; name="username" 暗色调 -----------------------------24464570528145 Content-Disposition: form-data; name="file"; filename="duola.jpg" Content-Type: image/jpeg ÿØÿà-sßÉKX‡é/ü3î·ÿ(文件数据,二进制,这里显示成乱码了) -----------------------------24464570528145 Content-Disposition: form-data; name="file2"; filename="" Content-Type: application/octet-stream -----------------------------24464570528145--
如何在 servlet
中读取文件上传数据,并保存到本地硬盘中?
Request
对象提供一个getInputStream
方法,通过这个方法可以获取客户端提交过来的数据。- 但是由于用户可能会同时上传多个文件,在
servlet
端编程直接读取上传的数据,并解析出相应文件的数据是一件非常麻烦的工作。 - 为了方便处理用户上传数据,
Apache
开源组织提供了一个用来处理表单上传文件的一个开源组件(Commons-fileupload
),该组件性能优异,并且 API 使用简洁 ; 使用 Commons-fileupload 组件实现文件上传,需要导入该组件相应的jar包:
·Commons-fileupload ·Commons-io;(虽然它不属于上传文件的开发jar,但是上传文件的jar包依赖于它)
fileupload组件工作流程 ;
1、首先 IE 端,会将表单的数据封装到 request 里面,提交给服务器;
2、组件提供一个工厂 DiskFileItemFactory ,通过工厂获取 ServletFileUpload ,
ServletFileupLoad 内部也是通过 getInputStream 。
3、调用 ServlFileupLoad 的parseRequest 方法解析数据, 解析完数据以后,将数据封装在一个 FileItem 类型的list中,
调用 FileItem 的 isFormField 方法,判断其是基本字段还是文件上传(返回TRUE 就是基本字段);
4、如果是基本字段,则调用 getFieldName 获取字段的名字,调用 getString 获取字段的值 ;
5、如果是上传文件,则调用 getName 获取上传文件的名字。
(获取的名字,根据浏览器的版本决定,有的获取全部名字,有的获取最后的名字),因此,我们要截取一下 。
调用 getInputStream 读取文件 ;
上传细节之乱码问题
乱码问题
文件名乱码问题
因为
解析器
是老外写的,里面的内置的编码是拉丁码表
,因此,在读取中文的时候,会出现乱码;即使我们设置了
request
的码表,也是无济于事的,对此,我们需要设置下解析器的码表 ;普通字段乱码问题
这里由于表单类型是
Multipart
,即使我们设置了request
的编码,也是无济于事的,因为,问题出在,getString()
方法上,它内部使用了拉丁码表
。要想改变它,用
getString(“码表”)
的重载方法 ;
上传细节之验证表单类型
验证表单类型
- 在开始操作之前,先判断下表单类型,
ServletFileUpload.isMultipartContent(request)
;
返回true
表示是上传表单 ;用于检测是否有人搞破坏!
- 在开始操作之前,先判断下表单类型,
上传细节之缓冲区问题
文件大小、缓冲区问题
上传文件特别大情况,设置
缓冲区大小
和临时文件位置
,临时文件需要自己设置自动删除 ;public void setSizeThreshold(int sizeThreshold)
;设置内存缓存区大小,默认为
10K
。当上传文件大于缓冲区大小的时候,fileupload
组件将使用临时文件缓存上传文件 ;也就是说,当文件大小
小于
缓冲区的时候,使用缓冲区保存文件;如果临时文件大于
缓冲区大小,则不使用缓冲区,而是先将文件写到临时文件里面保存 ;也就是说,当文件
小于
缓冲区大小的时候,fileupload
组件的内置缓冲区
作为一个中转站,先将文件写到缓冲区中,再从缓冲区写到服务器上 ;如果文件大小
大于
缓冲区大小,则不再使用缓冲区作为中转站,而是使用临时文件
作为中转,先将客户端的文件写到fileupload
组件一个临时文件中,再从临时文件中写到服务器上 ;备注:为什么需要中转?
原因很简单,中转说白了,就是一个缓冲区,我们将文件从文本上传到服务器,如果不使用缓冲区,直接从本地到服务器,每次磁头读取一次,然后送到服务器,然后磁头再次读取,循环往复;而每次磁头读取硬盘都是一个耗时的工作,我们应该减少磁头读取硬盘次数,或者说磁头读取一次硬盘,尽量的多读取数据,从而减少读取的次数,这样我们就需要,一个中转,让磁头一次读取更多的数据,然后保存到缓冲中;
如果没有中转当缓冲,那么一次磁头读取一个字节以后,就必须停止,待数据送达服务器端,才可以再次读取;这显然是不合理的,因此,我们需要缓冲区!
设置临时文件位置
// 设置临时文件的保存位置 ; factory.setRepository(new File(this.getServletContext().getRealPath("/WEB-INF/temp")));
使用
public void setRepository(java.io.File respository)
,指定临时文件位置,参数接受一个文件路径,这里我们是在JEE中使用File,因此,需要得到文件的绝对路径,而不是相对路径;使用
servletContext
获取文件真实路径指定临时文件目录,默认值是
System.getProperty("java.io.tmpdir") ;
临时文件需要我们删除掉,使用
item.delete()
方法 ;注意删除方法,要放在关闭流之后,否则还有流与文件关联,是删不掉的 ,确保被删掉,还需要放在
finally
里面;
上传细节之文件分配(保存目录)
对于保存上传文件,文件的保存目录,是绝对不能被外界访问到的;否则上传一段JSP
文本,然后访问这个jsp
文件,JSP
就会被执行!
这个目录的地址,需要好好写,不要在获得真实路径的字符串最后加上 /
,路径和名字,中间用 路径分隔符 拼接,使用File.sep...
来适应不同的系统 ;
其中IDEA
,在上传文件的时候,web
项目中的文件夹下,是没有文件产生的,产生的文件在Out
文件下 ;
文件的保存路径,我们必须放在WEB
文件夹下面,还有一个问题需要解决,windows操作系统,每个文件夹下面的直接文件数量超过1000
的时候,打开该文件,就会卡顿,文件夹中文件夹中的数量,不算在直接文件数量中;
因此,我们需要设计出一个算法,来分配上传的文件;这里,我们根据文件名的哈希码来分配文件位置,哈希码是一个int值,一共32位,每4位为一个层次,生成文件夹,最多可以生成8层,可以保存上兆的文件了。;
算法思想
:
获取文件名的哈希码,每4位二进制位,作为一个整数位;
第一个四位,就可以生成0-15号文件夹;
第二个四位,又会生成0-15号文件夹;
它们嵌套在一起,就可以存储16*16*1000个文件;
...第三个、第四个四位...
根据我们的需要,我们一共可以生成8层文件夹嵌套;
我用计算器算了一下,至少可以保存 4294967296000 这么多的文件,都到达兆的级别了,妥妥够用了;
每次保存文件的时候,先取文件名的哈希值,看对应的文件夹是否存在,
不存在就创建出文件路径返回,存在就直接返回路径 ;
/**
* 随机打乱文件存储位置,文件分配
*
* @param path
* @param name
* @return
*/
public String generateRandomPath(String path, String name) {
// 根据名字的哈希码
int hashcode = name.hashCode();
// 用哈希码的每4位 生成一个文件夹,我们这里嵌套3层 文件夹,可以最多保存 16 * 16 * 16 * 1000
// 获取低四位
int one = hashcode & 0xf;
// 获取低 五 - 八 位
int two = (hashcode >> 4) & 0xf;
// 获取低 九 - 十三 位
int three = (hashcode >> 4 >> 4) & 0xf;
// 生成路径。需要先判断,路径是否存在 ;
File file = new File(path + File.separator + one + File.separator + two + File.separator + three);
// 判断路径是否存在
if (!file.exists()) {
// 创建多级目录
file.mkdirs();
}
return file.getPath();
}
上传细节之限制上传文件类型
在处理文件的时候,先获取文件的后缀名,做个判断,看是否在我们允许的文件类型之中 ;
String suffix = name.substring(name.lastIndexOf("."));
// 对文件后缀进行判断
// limits是一个集合,里面保存着我们允许的后缀名
if (!(limits.contains(suffix))) {
request.setAttribute("message", "暂不支持 " + suffix + "文件的上传。。");
request.getRequestDispatcher("/WEB-INF/jsp/Message.jsp").forward(request, response);
return;
}
上传细节之限制上文文件大小
// 在解析器上设置
// 设置上传文件大小限制为500M
upload.setFileSizeMax(1024 * 1024 * 500);
上传细节之客户端没有文件上传
如果文件没有上传,则服务器端获取 文件名
的时候是 空串
;
// 获取输入流
InputStream input = item.getInputStream();
int len = 0;
byte[] bytes = new byte[1024];
// 获取上传文件的名字 如果没有文件上传,则获取空串
String name = item.getName();
// 没有上传文件
if (name.trim().equals("")) {
continue;
}
上传细节之文件名相同的问题
利用UUID
生成随机ID ,再加上原本文件的名字,来防止文件名冲突的问题;
// 对名字进行截取,以适应不同的浏览器
name = name.substring(name.lastIndexOf("\") + 1);
// 生成随机名字
name = generateUuuidName(name);
上传细节之动态添加上传文件项
JSP
中利用JS
代码,动态的添加、删除 ;
JS代码:
<script type="text/javascript">
function add() {
var input = document.createElement("input");
input.type = "file";
input.name = "file";
var btn_del = document.createElement("input");
btn_del.type = "button";
btn_del.value = "删除" ;
btn_del.onclick = function del() {
//this 谁触发了这个方法,this 就是谁
this.parentNode.parentNode.removeChild(this.parentNode) ;
};
var div = document.createElement("div");
div.appendChild(input);
div.appendChild(btn_del);
// 添加到已经存在的节点上,appendChild添加,默认作为最后一个孩子节点
var uploadfile = document.getElementById(" uploadFile");
uploadfile.appendChild(div);
}
</script>
上传细节之进度条
为解析器注册一个监听器,解析器里面有方法,当有数据被解析的时候,会被调用 ;
根据这个方法的参数,可以获取当前上传文件的进度 ;
// 解析器要在开始解析之前,进行注册!!!
// 实现没上传 1 M 进度条更新一下 ;
ProgressListener progressListener = new ProgressListener(){
private long megaBytes = -1;
public void update(long pBytesRead, long pContentLength, int pItems) {
// 已传输字节 是否 达到 1M
long mBytes = pBytesRead / 1000000;
// 只有传输字节比之前大于1 M ,才能更新进度条
if (megaBytes == mBytes) {
return;
}
// 保存已经传输的字节以1M的比值,初始为 -1
megaBytes = mBytes;
if (pContentLength == -1) {
System.out.println("So far, " + pBytesRead + " bytes have been read.");
} else {
System.out.println("So far, " + pBytesRead + " of " + pContentLength
+ " bytes have been read.");
}
}
};
上传细节之超链接中文
超链接中有中文的时候,需要URL编码 ;
在JSP 标签中,我们使用 < c : url >
标签,来完成URL编码 ;
resquest
设置编码,对get方法提交的数据无效 ;
<c:url var="url" value="/downloadServlet">
<%--会自动的进行url编码--%>
<c:param name="fileName" value="${entry.value}"></c:param>
</c:url>
${entry.key} <a href="${url}">下载</a><br>
下载细节之文件名含有空格、文件名乱码
String path = file.getSavePath();
String simpleName = file.getSimpleName();
// 浏览器分为 火狐 和 非火狐
if (request.getHeader("USER-AGENT").toLowerCase().indexOf("firefox") >= 0) {
// 火狐头大,需要独特设置一下
simpleName = new String(simpleName.getBytes("UTF-8"), "iso-8859-1");
} else {
simpleName = URLEncoder.encode(simpleName, "UTF-8");
// IE 文件名有空格会被加号代替。需要自己替换回去
simpleName = simpleName.replaceAll("\+","%20");
}
// 文件名有空格。火狐则会截断文件名,需要将文件名用字符串包起来
// 告诉浏览器下载方式打开.,
response.addHeader("Content-Disposition", "attachment;filename="" + simpleName+""");
下载步骤
- 递归列出服务器的所有文件,数据量大的情况下,注意分页 ;
- 在
JSP
中显示文件的时候,注意URL编码; - 点击下载的时候,将文件的
UUID
带过去; - 在
servlet
中,根据UUID
获取,要下载的文件对象,这个文件对象中封装该文件的信息; - 检查判断文件是否存在;
- 文件存在,则进行文件名的编码,这里注意
Firefox
和 其他浏览器的不同点; 对文件名中的空格,进行特殊处理;
// IE 文件名有空格会被加号代替。需要自己替换回去 simpleName = simpleName.replaceAll("\+","%20") ; // 文件名有空格。火狐则会截断文件名,需要将文件名用字符串包起来 // 告诉浏览器下载方式打开., response.addHeader("Content-Disposition", "attachment;filename="" + simpleName+""");
告诉浏览器下载方式打开;
- 最后,获取response的输出流,将文件写给浏览器
ServletFileUpload 核心API
// 判断上传表单是否为 mulipart/form-data 类型 ;
boolean isMultipartContent(HttpServletRequest request) ;
// (限制单个文件大小)单位是K
setFileSizeMax(long fileSizeMax)
// 设置上传文件的最大值,如果超过大小现在,则抛出异常,异常是一个内部类异常 FileUploadBase.FileSizeLimitExceededException
//设置上传文件总量的最大值
// (限制多个文件大小)
setSizeMax(long sizeMax)
// 设置编码格式
// 获取字段的值,设置码表
String value = item.getString("utf-8");
// 设置解析器码表,解决文件名乱码
upload.setHeaderEncoding("utf-8");