今天讲文件的上传……
<form action="fileUploadServlet" enctype="multipart/form-data" method="post">
//文件上传的时候必须指明encodingType,指明按照表单数据装成多个部分,由于文件大小可能比较大,所以提交的方式必须是post!
名称: <input type="text" name="name"><br>
文件: <input type="file" name="afile"/>
<input type="submit" />
</form>
上传的时候要把客户端的东西变成字节流传到服务器那一边,然后服务器接收到字节流之后再写到
自己的硬盘里面去!
但是问题是如何区别上述两个表单区域呢?怎样区分text和file??
如果一次性的上传4、5个文件,这些文件之间的边界如何区分??
应该在每一个参数的前后加上标识……来标识一个域
纯粹用JSP做文件上传是很困难的:
如果使用一个FileUploadServlet来接收表单的话,
public class FileUploadServlet extends HttpServlet
{
public void doPost(HttpServletRequest request,HttpServletResponse response)
throws ServletException,IOException
{
Enumeration em = request.getHeaderNames() ;//将所有的请求报头拿出来
while(em.hasMoreElements())
{
String name = (String)em.nextElements() ;
System.out.println(name + " : " + request.getHeader(name));
}
//拿出了请求的报头后还得拿到请求体,需要从请求中得到InputStream或者InputReader
InputStream in = request.getInputStream() ; //得到请求的输入流
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String s = null ;
while((s=reader.readLine())!=null) //读出请求流中的每一行!
{
System.out.println(s) ;
}
}
}
这个Servlet的目的是为了观察上面表单页面中提交过来的请求的所有信息,从而分析出上传文件所需要的。
报头中的content-type里面有个boundary,这是浏览器生成的,用来分割不同的表单区域信息……
可以在控制台里面看到text表单和file表单被这个boundary给分割开了,所以上传文件的时候完全可以通过
报头中的boundary得到分隔符,两个“-”加上boundary代表一个体的开始,如果两个“-”加上这个boundary,
后面又加上了两个“-”的话……就是最后结束了。
但是问题是如果你传递上来的是一个二进制文件的话……就是很难搞清楚的乱码了。
文件上传肯定得按照字节流去读取,不能用字符流!
我们做的只是纯文本的文件上传而已……
String contentType = request.getContentType();//
if(contentType==null && !contentType.startsWith("multipart"))
{
return ;
}
String boundary = contentType.substring(contentType.lastIndexOf("=")+1);
InputStream in = request.getInputStream() ; //得到请求的输入流
BufferedReader reader = new BufferedReader(new InputStreamReader(in));
String s = null ;
while((s=reader.readLine())!=null) //读出请求流中的每一行!
{
//boolean newPartBegin = false ;
if(s.equals("--"+boundary))
{
newPartBegin = true ;
continue ;
}
System.out.println(s) ;
}
======================================================
如果使用apache开源项目的上传组件的话,事情就会相当的容易了!
在doPost方法中添加如下代码:
DiskFileItemFactory factory = new DiskFileItemFactory() ;
ServletFileUpload upload = new ServletFileUpload(factory) ;
upload.setHeaderEncoding("UTF-8");//注意这里的编码设置要和jsp页面的编码一致才不会出现乱码!
List items = upload.parseRequest(request) ;
Iterator it = items.iterator() ;
while(it.hasNext())
{
FileItem item = (FileItem)it.next();
if(!item.isFormField())
{
String filename = item.getName().substring(item.getName().lastIndexOf("//")+1);
//得到文件的名字
File file = new File("f://"+filename);
item.write(file) ;
}
}
===============================================================================
下午课程开始:
看看在Struts中是如何完成文件上传的……
用Struts进行文件上传的时候可以看到Struts中其实也提供了文件上传的组件commons-fileupload.jar
而且:
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html"%>
要使用html标签才可以……
//注意如果设置了enctype="multipart/form-data",那么再想从请求中用request.getParameter()
//的话,就做不到了。
<body>
<html:form action="/upload" enctype="multipart/form-data" method="post">//文件可能很大,所以必须post提交
<html:file property="file" />
<html:submit/>
</html:form>
</body>
一个html标签必须得对应一个formbean:
public class upload extends ActionForm
{
private FormFile file ;
getter和setter方法略
public ActionErrors validate()
{}
public void reset()
{}
}
新建一个Action://文件上传后还要写数据库,
//但是文件上传和写数据库应该放在一个事务里面去做,二者必须同时成功或同时失败才对。
//需要commons中的dbcp、pools和collections三个包,可以从李浩给的压缩包里面拿……当然别忘了还有数据库的包。
//因为需要配置数据源,所以需要昨天的数据源的配置,具体方法见昨天的日记…………
public ActionForward execute()
{
String target = "failure" ;
UploadForm uploadForm = (UploadForm)form ;//form是execute方法传入的参数
FormFile file = uploadForm.getFile() ;
String filename = rename(file.getFileName(),request) ;//重命名
String path = "f://upload//" + filename ;
FileOutputStream out = new FileOutputStream(path) ;
InputStream in = file.getInputStream() ;
//也就是从上面那个输入流里面读,然后再写到一个输出流里面去
byte[] buf = new byte[512] ;
int pos = -1 ;
//如果正常返回那么读了多少就返回多少
while((pos=in.read(buf,0,512))!=-1)//从buf(缓存)中的0读到512
{
out.write(buf,0,pos) ;
out.flush();
}
registerData(request,file.getFileName(),path);//这个方法负责插入数据库。数据库的表设计:
//建立一个新表files:字段有
//id:Integer
//filename:varchar(200)
//path:varchar(300)
//uploadTime:DATETIME
//downloadTimes:Integer
target ="success" ;
if(out!=null)
{
out.close() ;
out = null ;
}
if(in!=null)
{
in.close() ;
in = null ;
}
return actionMapping.findForward(target) ;
}
//给文件重命名,可以在application中存储一个计数器,每次存储一个就加1,但是需要考虑同步。
protected String rename(String name,HttpServletRequest request)
{
DateFormat df = new SimpleDateFormat("yyyyMMddHHmmss") ;//格式化时间,注意如果这里写成小写的hh,那就成了12小时制了。
String newName = df.format(new Date()) + request.getSession().getId();//取当前时间的日期字符串
int position = name.lastIndexOf(".");
if(position!=-1) //说明原文件有扩展名
{
newName = newName + name.substring(position) ;//也就是以日期字符串和sessionID相加为新文件名,扩展名为原文件扩展名
}
return newName ;
}
protected void registerData(HttpServletRequest request , String filename , String path)
{
Connection conn = null ;
PreparedStatement pstmt = null ;
ResultSet rs = null ;
DataSource ds = this.getDataSource(request) ;
try
{
conn = ds.getConnection() ;
pstmt = conn.prepareStatement("insert into files(filename,path,uploadTime,downloadTimes) values(?,?,?,0)");
pstmt.setSring(1,filename) ;
pstmt.setSring(2,path) ;
pstmt.setTimestamp(3,new Timestamp(System.currentTimeMillis()));
if(pstmt.executeUpdate()!=1)
{
throw new SQLException() ;
}
}
catch(Exception e)
{
File f = new File(path);
f.delete() ;//如果出现了上面抛出的异常,也就是数据库插入失败的话,在服务器上把上传上来的文件删除掉,当然不删也可以,
//只要不插入数据库,那么文件对于客户端就是不可见的。
}
finally
{
//关闭连接
}
}
//注意如果要避免中文的乱码的话首先记住在struts-config中的datasources元素配置中的url要加上characterEncoding=gbk这个东西
之后如果上传成功的话我们就通过一个Action来把数据库中文件都拿出来,然后转到ListFile.jsp中去。
所以插入数据库成功后先去一个.do地址,通过这个.do地址找到上面的那个Action(ListFileAction)
在里面做的工作是:
conn = ds.getConnection() ;
pstmt = conn.prepareStatement("select * from files") ;
rs = pstmt.executeQuery() ;
ArrayList files = new ArrayList() ;
//上面这个集合中可以放javaBean或者Map,这两个可以在页面上用表达式语言来访问!
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH时mm分ss秒") ;
while(rs.next())
{
Map file = new HashMap() ;
file.put("id",rs.getString("id"));
file.put("filename",rs.getString("filename")) ;
//file.put("path",rs.getString(3)) ;
file.put("uploadTime",sdf.format(new Date(rs.getTimestamp("uploadTime").getTime())));
file.put("downloadTimes",rs.getString("downloadTimes")) ;
files.add(file) ;
}
request.setAttribute("files",files) ;
retrun actionMapping.findForward(""); //然后执行请求转发,到底转到哪里决定于自己在struts-config中的配置!
这样在jsp页面上就可以显示了:
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic"%>
<body>
<table> //这里面其实应该有个分页的问题,但是这里没有考虑……
<logic:iterate id="file" name="files"> //会到作用域里面去找files这个属性,不再像核心标签库里面的需要表达式语言
<tr>
<td>${file.id}</td>
<td>${file.filename}</td>
<td>${file.uploadTime}</td>
<td>${file.downloadTimes}</td>
<td><a href="download.do?id=${file.id}">下载?</a></td>
</tr>
</logic:iterate>
<table>
</body>
再建立一个DownLoadAction来根据传递过来的id在数据库中找到path,然后处理下载流程,
成功后最好使用redirect回到第一个在数据库中取全部文件的Action。
//下载文件的时候由于文件大小肯定大于输出流的缓存8192Byte,所以写完输出流后绝对不可以再请求转发了,也不能重定向!!!
public ActionForward execute
{
String id = request.getParameter("id") ;
//得到id后,可以想办法去根据这个id来取出path
pstmt= conn.prepareStatement("select path , filename from files where id=?");
pstmt.setInt(1,Integer.parseInt(id));
rs = pstmt.executeQuery();
if(rs.next)
{
String path = rs.getString(1) ;
FileInputStream in = new FileInputStream(path) ;//得到文件的输入流,可以把文件读进来了,应该把生成的响应传回去。
//如何从响应中去得到输出流是下面需要考虑的问题。这时候需要考虑使用OutputStream而不是writer,因为可能是二进制的
OutputStream out = response.getOutputStream() ;
response.setContentType("application/zip"); //
response.setHeader("content-disposition","attachment;filename=" +
new String(rs.getString(2).getBytes("GBK"),"iso-8859-1"));
//因为浏览器解析响应头的时候是以iso-8859-1的形式去显示的,所以转码的时候必须像上面那样。
//后面的attachment会显示一个提示对话框,来提示你是否下载
byte[] buf = new byte[512] ;
int size = -1 ;
while((size=in.read(buf,0,512))!=-1)
{
out.write(buf,0,size);
out.flush() ;
}
in.close() ;
target = "success" ;
}
}
但是响应response里面有个8K的buffer,但是我们传的文件肯定是大于8K了,这时候一旦flush了,就认为响应提交了,但是转向之前肯定要把
response里面的东西清空的,为了让客户端看不到这个页面的东西才这么做的。但由于文件比较大 会flush了好几次,再转向就没有办法了,也就是
被提交的响应没有办法再forward了。
由于重定向是生成一个临时的响应,但是由于下载文件的时候服务器已经给客户端生成了响应,所以这招也不能用了。所以只好干脆哪里都不转,
具体操作起来就是在execute方法里面返回null,也就是还是停留在原来的这个页面上面,但是也不会去刷新这个页面。
其实大文件的上传和下载是分批进行的,至于批量读写的大小取决于上面的缓冲区buf,我们创建的是512字节的缓冲区,这样也就是一次将
512字节的输入流弄到响应里面去,如果是8KB的文件的话,那么大概16次就弄完了。