先来看看文件上传的部分,首先我先介绍下要实现文件上传,对于页面的要求,实际上也就是表单要多加一些属性。
- <h2>查看文件上传的请求正文</h2>
- <form id="form1" method="POST" enctype="multipart/form-data" action="SeeRequestContentServlet">
- <input type="text" name="myText" value="test" /><br/>
- <input type="file" name="myFile"/><br/>
- <input type="submit" value="提交" /><br/>
- </form>
<h2>查看文件上传的请求正文</h2>
<form id="form1" method="POST" enctype="multipart/form-data" action="SeeRequestContentServlet">
<input type="text" name="myText" value="test" /><br/>
<input type="file" name="myFile"/><br/>
<input type="submit" value="提交" /><br/>
</form>
首先,因为是文件上传,那么表单的method必须指定为POST,然后必须告诉服务器,这是一次带有附件的请求,因此必须加上enctype="multipart/form-data"的表单属性以及对应的值。对于文件控件,相应的就是修改input标签的type属性为file就可以了。SeeRequestContentServlet这是一个用于处理我这次文件上传的Servlet文件,详细的东西大家可以查看附件。
想要知道待会要介绍的fileupload这个jar包,大家必须先知道一个带有附件的请求的请求正文到底是什么样子的,这样有利于大家的理解。
所以接下来演示,不使用jar包,来分析带附件的请求的请求正文。请看SeeRequestContentServlet的doPost方法。
- public void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- System.out.println("本次请求正文长度为:" + request.getHeader("Content-Length"));
- System.out.println("本次请求类型及表单域分隔符:" + request.getContentType());
- ServletInputStream sis = request.getInputStream();
- ServletOutputStream sos = response.getOutputStream();
- byte[] b = new byte[2048];
- @SuppressWarnings("unused")
- int count = 0;
- while ((count = sis.read(b)) != -1) {
- sos.write(b, 0, count);
- }
- sos.flush();
- sos.close();
- }
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("本次请求正文长度为:" + request.getHeader("Content-Length"));
System.out.println("本次请求类型及表单域分隔符:" + request.getContentType());
ServletInputStream sis = request.getInputStream();
ServletOutputStream sos = response.getOutputStream();
byte[] b = new byte[2048];
@SuppressWarnings("unused")
int count = 0;
while ((count = sis.read(b)) != -1) {
sos.write(b, 0, count);
}
sos.flush();
sos.close();
}
代码实际上很简单,就是一些基础的流操作,我们来看一下运行结果,并做一个分析。
下面是我即将上传的文件:
我的表单数据:
请看上传后在网页上看到的结果:
- -----------------------------80233217916595
- Content-Disposition: form-data; name="myText"
- test
- -----------------------------80233217916595
- Content-Disposition: form-data; name="myFile"; filename="测试文件.txt"
- Content-Type: text/plain
- �����ļ�����
- -----------------------------80233217916595--
-----------------------------80233217916595
Content-Disposition: form-data; name="myText"
test
-----------------------------80233217916595
Content-Disposition: form-data; name="myFile"; filename="测试文件.txt"
Content-Type: text/plain
�����ļ�����
-----------------------------80233217916595--
以及我的控制台打印结果:
- 本次请求正文长度为:310
- 本次请求类型及表单域分隔符:multipart/form-data; boundary=---------------------------80233217916595
本次请求正文长度为:310
本次请求类型及表单域分隔符:multipart/form-data; boundary=---------------------------80233217916595
待会在处理乱码问题,这个不是关键。
通过控制台打印结果,以及网页的输出结果,我们发现了,对于含有附件的请求正文来说,表单域之前会有一个---------------------------80233217916595作为分隔符,并且对于附件域来说,还会有额外的 filename="测试文件.txt"的文本来标示文件在上传时使用的文件名。
于是大家如果能够知道请求正文的格式组成之后,剩下来要做的不就是根据分隔符以及相应的一些固定的字符串如filename,name,通过字符串操作来获取请求正文中大家需要的数据了。感觉很繁琐吧,这也是为什么大家现在完成文件上传功能是都是使用Apache提供的fileupload的jar包来实现的原因了。不过个人感觉理解了原理,再去应用,对于学习是很有好处的,所以写了这个例子。
接下来,解释下出现乱码的原因,很简单,就是编码不统一引起的,由于我编程时习惯上都把编码设置为UTF-8,包括上传表单的页面编码,而text文档在中文系统中的默认编码是GBK(关于这个大家可以用Editplus求证),所以在打印响应内容的时候,由于编码的不兼容引起了乱码,解决方式很简单,用Editplus打开文件,重新编码即可。
原始编码:
修改后编码:
网页结果:
- -----------------------------20169232712042
- Content-Disposition: form-data; name="myText"
- test
- -----------------------------20169232712042
- Content-Disposition: form-data; name="myFile"; filename="测试文件.txt"
- Content-Type: text/plain
- 测试文件内容
- -----------------------------20169232712042--
-----------------------------20169232712042
Content-Disposition: form-data; name="myText"
test
-----------------------------20169232712042
Content-Disposition: form-data; name="myFile"; filename="测试文件.txt"
Content-Type: text/plain
测试文件内容
-----------------------------20169232712042--
另外我们也发现了分隔符是随机生成的。以上例子并没有真正完成文件上传,只是想从请求正文的内容来分析文件上传原理,实际上大家自己有耐心地去做一些字符串截取就可以实现文件上传了,这里就省略了。进入fileupload的使用部分。
先看页面代码,跟之前的实际上没有太大区别,只是改了个action的地址。
- <h2>使用fileupload组件实现文件上传</h2>
- <form id="form2" method="POST" enctype="multipart/form-data" action="FileUploadServlet">
- <input type="text" name="myText" value="test" /><br/>
- <input type="file" name="myFile"/><br/>
- <input type="submit" value="提交" /><br/>
- </form>
<h2>使用fileupload组件实现文件上传</h2>
<form id="form2" method="POST" enctype="multipart/form-data" action="FileUploadServlet">
<input type="text" name="myText" value="test" /><br/>
<input type="file" name="myFile"/><br/>
<input type="submit" value="提交" /><br/>
</form>
来看FileUploadServlet的doPost方法
- @SuppressWarnings("unchecked")
- public void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- try {
- FileItemFactory factory = new DiskFileItemFactory();
- ServletFileUpload upload = new ServletFileUpload(factory);
- upload.setHeaderEncoding("utf8");//支持中文文件名
- List<FileItem> items = upload.parseRequest(request);
- Iterator<FileItem> iter = items.iterator();
- while (iter.hasNext()) {
- FileItem item = iter.next();
- if (item.isFormField()) {
- System.out.println("查找到一个普通文本数据");
- System.out.println("该文本数据的name为:"+item.getFieldName());
- System.out.println("该文本数据的value为:"+item.getString());
- System.out.println();
- } else {
- System.out.println("查找到一个二进制数据");
- System.out.println("该文件表单name为:"+item.getFieldName());
- System.out.println("该文件文件名为:"+item.getName());
- System.out.println("该文件文件类型为:"+item.getContentType());
- System.out.println("该文件文件大小为:"+item.getSize());
- System.out.println();
- File uploadedFile = new File(this.getServletContext().getRealPath("fileupload")+"\\"+item.getName());
- item.write(uploadedFile);
- }
- }
- } catch (FileUploadException e) {
- e.printStackTrace();
- } catch (Exception e) {
- e.printStackTrace();
- }
- response.sendRedirect("success.jsp");
- }
@SuppressWarnings("unchecked")
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
try {
FileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setHeaderEncoding("utf8");//支持中文文件名
List<FileItem> items = upload.parseRequest(request);
Iterator<FileItem> iter = items.iterator();
while (iter.hasNext()) {
FileItem item = iter.next();
if (item.isFormField()) {
System.out.println("查找到一个普通文本数据");
System.out.println("该文本数据的name为:"+item.getFieldName());
System.out.println("该文本数据的value为:"+item.getString());
System.out.println();
} else {
System.out.println("查找到一个二进制数据");
System.out.println("该文件表单name为:"+item.getFieldName());
System.out.println("该文件文件名为:"+item.getName());
System.out.println("该文件文件类型为:"+item.getContentType());
System.out.println("该文件文件大小为:"+item.getSize());
System.out.println();
File uploadedFile = new File(this.getServletContext().getRealPath("fileupload")+"\\"+item.getName());
item.write(uploadedFile);
}
}
} catch (FileUploadException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
response.sendRedirect("success.jsp");
}
唯一要注意的就是那个可以支持中文文件名的代码,其他的跟Apache官方提供的代码就一致了。
有了上面的代码相信要实现多文件上传也是很容易的事情了。
最后来个带上传进度的文件上传。这里需要AJAX的知识,不懂的朋友可以先去了解下。
先看页面的代码
- <h2>文件上传显示进度</h2>
- <iframe id='target_upload' name='target_upload' src='' style='display: none'></iframe>
- <form id="form3" method="POST" enctype="multipart/form-data" action="AJAXFileUploadServlet" target="target_upload">
- <input type="file" name="myFile"/><br/>
- <input type="button" value="提交" id="myButton"/><br/>
- </form>
- <div id="show"></div>
<h2>文件上传显示进度</h2>
<iframe id='target_upload' name='target_upload' src='' style='display: none'></iframe>
<form id="form3" method="POST" enctype="multipart/form-data" action="AJAXFileUploadServlet" target="target_upload">
<input type="file" name="myFile"/><br/>
<input type="button" value="提交" id="myButton"/><br/>
</form>
<div id="show"></div>
细心的朋友发现多一个iframe和div,iframe的作用是,当我们要显示上传进度的时候,一定是要求页面不能刷新的,因此你会发现form的target属性等于iframe的name属性,也就是说,当form提交数据的时候,当前页面不会刷新,而是iframe刷新了,而iframe又是dispaly:none的,所以看起来页面好像是没有发生跳转。div的作用则是用于显示上传进度。
接下来关注服务器的代码部分,想想其实就可以发现,服务器需要两个Servlet,一个正在用于文件上传,而一个则是用来返回AJAX关于上传进度的数据返回。
这里要介绍fileupload中很好用的一个接口,ProgressListener,她可以用来监听文件的上传进度,使用方法很简单,即在文件上传的代码中加入
- FileItemFactory factory = new DiskFileItemFactory();
- ServletFileUpload upload = new ServletFileUpload(factory);
- upload.setHeaderEncoding("utf8");// 支持中文文件名
- MyProgressListener myProgressListener = new MyProgressListener(request);
- upload.setProgressListener(myProgressListener);//进行文件上传进度监听
FileItemFactory factory = new DiskFileItemFactory();
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setHeaderEncoding("utf8");// 支持中文文件名
MyProgressListener myProgressListener = new MyProgressListener(request);
upload.setProgressListener(myProgressListener);//进行文件上传进度监听
MyProgressListener是实现ProgressListener的一个自定义类。下面是ProgressListener的代码。
- package com.mison.fileupload;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpSession;
- import org.apache.commons.fileupload.ProgressListener;
- public class MyProgressListener implements ProgressListener {
- private HttpSession session;
- private FileUploadStatus fileUploadStatus;
- public MyProgressListener(HttpServletRequest request) {
- session = request.getSession();
- }
- /***************************************************************************
- * pBytesRead - The total number of bytes, which have been read so far.
- * pContentLength - The total number of bytes, which are being read. May be
- * -1, if this number is unknown.
- * pItems - The number of the field, which is
- * currently being read. (0 = no item so far, 1 = first item is being read,
- * ...)
- */
- @Override
- public void update(long pBytesRead, long pContentLength, int pItems) {
- System.out.println("监听器被调用");
- fileUploadStatus = (FileUploadStatus) (session.getAttribute("fileUploadStatus") == null ? new FileUploadStatus()
- : session.getAttribute("fileUploadStatus"));
- fileUploadStatus.setPBytesRead(pBytesRead);
- fileUploadStatus.setPContentLength(pContentLength);
- fileUploadStatus.setPItems(pItems);
- session.setAttribute("fileUploadStatus", fileUploadStatus);
- }
- }
package com.mison.fileupload;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.apache.commons.fileupload.ProgressListener;
public class MyProgressListener implements ProgressListener {
private HttpSession session;
private FileUploadStatus fileUploadStatus;
public MyProgressListener(HttpServletRequest request) {
session = request.getSession();
}
/***************************************************************************
* pBytesRead - The total number of bytes, which have been read so far.
* pContentLength - The total number of bytes, which are being read. May be
* -1, if this number is unknown.
* pItems - The number of the field, which is
* currently being read. (0 = no item so far, 1 = first item is being read,
* ...)
*/
@Override
public void update(long pBytesRead, long pContentLength, int pItems) {
System.out.println("监听器被调用");
fileUploadStatus = (FileUploadStatus) (session.getAttribute("fileUploadStatus") == null ? new FileUploadStatus()
: session.getAttribute("fileUploadStatus"));
fileUploadStatus.setPBytesRead(pBytesRead);
fileUploadStatus.setPContentLength(pContentLength);
fileUploadStatus.setPItems(pItems);
session.setAttribute("fileUploadStatus", fileUploadStatus);
}
}
这样就了解了吧。在update方法中,将上传进度的有关信息保存到session对象中,那么AJAX请求就可以访问session来获取相应的上传进度了。
来看AJAX的服务器端代码
- package com.mison.fileupload;
- import java.io.IOException;
- import java.io.PrintWriter;
- import javax.servlet.ServletException;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import net.sf.json.JSONObject;
- @SuppressWarnings("serial")
- public class SeeProgressServlet extends HttpServlet {
- public void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- this.doGet(request, response);
- }
- @Override
- protected void doGet(HttpServletRequest request,
- HttpServletResponse response) throws ServletException, IOException {
- FileUploadStatus fileUploadStatus = (FileUploadStatus) request
- .getSession().getAttribute("fileUploadStatus");
- JSONObject jsonObject = JSONObject.fromObject(fileUploadStatus);
- PrintWriter out = response.getWriter();
- out.println(jsonObject.toString());
- out.flush();
- out.close();
- }
- }
package com.mison.fileupload;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.sf.json.JSONObject;
@SuppressWarnings("serial")
public class SeeProgressServlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this.doGet(request, response);
}
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
FileUploadStatus fileUploadStatus = (FileUploadStatus) request
.getSession().getAttribute("fileUploadStatus");
JSONObject jsonObject = JSONObject.fromObject(fileUploadStatus);
PrintWriter out = response.getWriter();
out.println(jsonObject.toString());
out.flush();
out.close();
}
}
上面使用了json的包,来实现对象到json字符串的转换,不懂的自己搜索下。
最后就是客户端的js代码了
- <script type="text/javascript" src="js/jquery126.js"></script>
- <script type="text/javascript">
- $(function(){
- $("#myButton").click(function(){
- $("#show").html("");
- $(this).attr("disabled",true);
- $("#form3").submit();
- setTimeout("showProgress()",500);
- });
- });
- function showProgress(){
- $.getJSON("SeeProgressServlet",function(json){
- $("#show").html("上传进度:"+(json.PBytesRead/json.PContentLength)*100+"%");
- if(json.PBytesRead == json.PContentLength){
- $("#show").html($("#show").html()+"上传结束~");
- $("#myButton").attr("disabled",false);
- }else{
- setTimeout("showProgress()",500);
- }
- });
- }
- </script>
<script type="text/javascript" src="js/jquery126.js"></script>
<script type="text/javascript">
$(function(){
$("#myButton").click(function(){
$("#show").html("");
$(this).attr("disabled",true);
$("#form3").submit();
setTimeout("showProgress()",500);
});
});
function showProgress(){
$.getJSON("SeeProgressServlet",function(json){
$("#show").html("上传进度:"+(json.PBytesRead/json.PContentLength)*100+"%");
if(json.PBytesRead == json.PContentLength){
$("#show").html($("#show").html()+"上传结束~");
$("#myButton").attr("disabled",false);
}else{
setTimeout("showProgress()",500);
}
});
}
</script>
本地上传的话,弄个100M的东西,貌似效果比较明显,截个图给大家看下。
不过网页很卡就是,求可以优化的做法。
*********************************************************************************
第二部分是文件下载,相比简单很多了,终于可以松口气。
网页代码
- <h2>文件下载</h2>
- <form id="form4" method="POST" action="FileDownloadServlet">
- 请输入文件名:<input type="text" name="fileName"/><br/>
- <input type="submit" value="提交" /><br/>
- </form>
<h2>文件下载</h2>
<form id="form4" method="POST" action="FileDownloadServlet">
请输入文件名:<input type="text" name="fileName"/><br/>
<input type="submit" value="提交" /><br/>
</form>
Servlet代码
- package com.mison.filedown;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.IOException;
- import javax.servlet.ServletException;
- import javax.servlet.ServletOutputStream;
- import javax.servlet.http.HttpServlet;
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- @SuppressWarnings("serial")
- public class FileDownloadServlet extends HttpServlet {
- public void doGet(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- this.doPost(request, response);
- }
- public void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
- String fileName = new String(request.getParameter("fileName").getBytes(
- "ISO-8859-1"), "UTF-8");
- FileInputStream fis = new FileInputStream(new File(getServletContext()
- .getRealPath("WEB-INF/download")
- + "\\" + fileName));
- System.out.println("客户端类型:" + request.getHeader("User-Agent"));
- if (request.getHeader("User-Agent").contains("Firefox")) {
- response.addHeader("content-disposition", "attachment;filename="
- + request.getParameter("fileName"));
- } else if (request.getHeader("User-Agent").contains("MSIE")) {
- response.addHeader("content-disposition", "attachment;filename="
- + java.net.URLEncoder.encode(fileName, "UTF-8"));
- }
- //相应的逻辑操作
- ServletOutputStream sos = response.getOutputStream();
- int count = 0;
- byte[] bytes = new byte[1024];
- while ((count = fis.read(bytes)) != -1) {
- sos.write(bytes, 0, count);
- }
- sos.flush();
- sos.close();
- }
- }
package com.mison.filedown;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@SuppressWarnings("serial")
public class FileDownloadServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this.doPost(request, response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String fileName = new String(request.getParameter("fileName").getBytes(
"ISO-8859-1"), "UTF-8");
FileInputStream fis = new FileInputStream(new File(getServletContext()
.getRealPath("WEB-INF/download")
+ "\\" + fileName));
System.out.println("客户端类型:" + request.getHeader("User-Agent"));
if (request.getHeader("User-Agent").contains("Firefox")) {
response.addHeader("content-disposition", "attachment;filename="
+ request.getParameter("fileName"));
} else if (request.getHeader("User-Agent").contains("MSIE")) {
response.addHeader("content-disposition", "attachment;filename="
+ java.net.URLEncoder.encode(fileName, "UTF-8"));
}
//相应的逻辑操作
ServletOutputStream sos = response.getOutputStream();
int count = 0;
byte[] bytes = new byte[1024];
while ((count = fis.read(bytes)) != -1) {
sos.write(bytes, 0, count);
}
sos.flush();
sos.close();
}
}
比较有意思的是关于下载时指定中文文件名的部分,由于ie和火狐的处理方式不一样,因此写了个判断分支。对于ie来讲,使用URLEncoder这个类将中文编码为16进制的asci码,这样ie客户端可以自己进行转码,可惜火狐没有这功能;火狐的处理方式是将中文的字符用ISO-8859-1编码,火狐会在客户端自己进行转码,同样的,这样的功能ie也没有,真蛋疼。