一、文件上传原理:
1、文件上传的前提:
表单的method必须是post方式。
表单的enctype必须是multipart/form-data。
表示的File类型的input,必须有name属性,值不重要可以随便写。
2、如果表单的enctype的值是multipart/form-data,那么传统的获取请求参数的方法失效。
3、正文内容是使用MIME协议进行描述的。
关于MIME协议可到RFC中查找相关的资源,这里简单谈谈个人理解。通过将页面请求传给Servlet处理,Servlet中通过request.getInputStream获取输入流打印出来。
jsp页面如下:
<form action="${pageContext.request.contextPath}/servlet/Demo1Servlet" method="post" enctype="multipart/form-data">
用户名::<input type="text" name="username"> <br />
文件1::<input type="file" name="file1"> <br />
文件2::<input type="file" name="file2"> <br />
<input type="submit" value="提交" />
form>
Servlet处理代码如下:
InputStream in = request.getInputStream();
int len = -1;
byte[] b = new byte[1024];
while((len=in.read(b))!=-1){
System.out.print(new String(b,0,len));
}
结果发现打印如下,也就是MIME的格式:
------WebKitFormBoundaryyBXT4NAF1dkh993S
Content-Disposition: form-data; name="username"
chen
------WebKitFormBoundaryyBXT4NAF1dkh993S
Content-Disposition: form-data; name="file1"; filename="a.txt"
Content-Type: text/plain
aaaa
------WebKitFormBoundaryyBXT4NAF1dkh993S
Content-Disposition: form-data; name="file2"; filename="b.txt"
Content-Type: text/plain
bbbb
------WebKitFormBoundaryyBXT4NAF1dkh993S--
4.MIME协议总结如下:
1>多个输入域,不管是File还是text等等都是以------WebKitFormBoundary连接16位随机Base64位编码的字符串进行隔开的。多个输入域之间的分隔字符串是一致的都是------WebKit...。
2>多个输入域,每个输入域都有自己单独的请求头与请求体。各个请求头与请求体都被------WebKit...字符串分隔。
3>输入域请求头格式:
Content-Dispostion:form-data;空格name=”xxx”; filename=”xx”
Content-Type: xxxx(若输入域不是File,则请求头截止到name就没了只有一行。而File类型有filename与Content-Type,整个请求头有二行。)
4>输入域请求体格式:
注意请求头与请求体之间有一行空行的(这点与HTTP协议是一样滴)。请求体中为输入域的内容。若输入域是非File类型,则请求体直接是输入的文本内容。若输入域是File类型,则请求体是File的二进制流信息。
通过对上面的了解,若需要文件上传则必须将表单enctype指定为multipart/form-data,而在服务端获取请求数据,必须遵循MIME协议从InputStream输入流中按MIME协议格式解析数据。手动来解析还是比较麻烦的。所以有Apache提供了优秀的common-fileupload组件。
二、Apache commons-fileupload
需要依赖两个jar包:commons-fileupload.jar 与 commons-io.jar
主要有以三个核心类:
DiskFileItemFactory、ServletFileupLoad、FileItem三种之间的关系如下:
1> DiskFileItemFactory 是创建 FileItem 对象的工厂,常用方法有:
public void setSizeThreshold(int sizeThreshold)
设置内存缓冲区的大小,默认值为10K。当上传文件大于缓冲区大小时, fileupload组件将使用临时文件缓存上传文件。
public void setRepository(java.io.File repository)
指定临时文件目录,默认值为System.getProperty("java.io.tmpdir").
public DiskFileItemFactory(int sizeThreshold, java.io.File repository) 构造函数,也可以直接new DiskFileItemFactory()采用默认的缓存区大小与临时文件目录。
2>ServletFileUpload 负责处理上传的文件数据,并将表单中每个输入项封装成一个 FileItem 对象中。常用方法有:
boolean isMultipartContent(HttpServletRequest request)
判断上传表单是否为multipart/form-data类型
List parseRequest(HttpServletRequest request)
解析request对象,并把表单中的每一个输入项包装成一个fileItem 对象,并返回一个保存了所有FileItem的list集合。
setFileSizeMax(long fileSizeMax) //设置单个上传文件的最大值
setSizeMax(long sizeMax) //设置上传文件总量的最大值
setHeaderEncoding(java.lang.String encoding) //设置编码格式
setProgressListener(ProgressListener pListener) //提供可注册监听器来监听上传的进度
3>FileItem封装每个输入域中的数据,包括请求头与请求体。请求体是通过getInputStream得到。请求头分别被封装成getFieldName、getString、getName、getContentType相应的属性。因为常用的方法如下:
boolean isFormField() //判断此FileItem是否普通的输入域(非File类型的输入域)
String getFieldName //获取此普通输入域的name中的值,即字段名。
String getString() /获取本地默认字符编码下的,普通输入域的值,即字段值。
String getString(chartSet)
//获取指定字符编码下的普通输入域的值,如getString(“utf-8”)。
String getContentType() //普通文件的MIME类型如text/plain 纯文本。
InputStream getInputStream() //获取请求体内容。FileItem为File类型时则是文件内容的二进流数据,有这个流那么可以构造输入出存入到服务器中即就是文件上传了。
void delete()
//删除临时文件。可以在FileItem的输入流in.close时再调用delete方法
文件上传示例:
public class UploadServlt extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
DiskFileItemFactory factory = new DiskFileItemFactory(); //1.构造DiskFileItemFactory对象
String tempFile = getServletContext().getRealPath("/WEB-INF/temp");
File tempF = new File(tempFile);
factory.setRepository(tempF); //设置临时文件
//factory.setSizeThreshold(100*1024); //启用临时文件的大小,超过100k,用临时文件。否则直接写到upload中。
ServletFileUpload upload = new ServletFileUpload(factory); //2.获取ServletFileUpload对象。
registListener(upload); //设置文件上传进度的监听器来监听上传进度...
if(!upload.isMultipartContent(request)){
throw new ExceptionInInitializerError("表单类型有错");
}
try {
/*upload.setFileSizeMax(1*1024*1024);*/ //设置单个文件是1M左右大小
/*upload.setSizeMax(1*1024*1024); */ //一次上传所有文件总大小不能超过1M
List
for(FileItem item:items){
if(item.isFormField()){
String name = item.getFieldName();
String paraValue = item.getString("utf-8");
//System.out.println("普通字段有::"+name+" = "+paraValue);
}else{ //剩下的都是文件类型
String contentType = item.getContentType();
/*if(!contentType.startsWith("image/")){
throw new FileTypeException("文件格式不对,只能上传图片文件");
}*/
String fileName = item.getName(); //获取文件名 获取不得路径名的,直接是文件名如 a.jgp
System.out.println(fileName);
InputStream in = item.getInputStream();
fileName = fileName.substring(fileName.lastIndexOf("\\")+1);
handUpload(request,response,factory,upload,in,fileName,item);
}
}
request.setAttribute("message", "上传成功了");
request.getRequestDispatcher("/message.jsp").forward(request, response);
}catch(FileUploadBase.FileSizeLimitExceededException e){
request.setAttribute("message", "上传失败了,文件大小不能超过"+upload.getFileSizeMax());
request.getRequestDispatcher("/message.jsp").forward(request, response);
}catch(FileUploadBase.SizeLimitExceededException e){
request.setAttribute("message", "上传失败了,总文件大小不能超过"+upload.getSizeMax());
request.getRequestDispatcher("/message.jsp").forward(request, response);
}
/*catch(FileTypeException e){
request.setAttribute("message", e.getMessage());
request.getRequestDispatcher("/message.jsp").forward(request, response);
}
*/
catch (Exception e) {
e.printStackTrace();
}
}
private void handUpload(HttpServletRequest request,
HttpServletResponse response, DiskFileItemFactory factory,
ServletFileUpload upload, InputStream in,String fileName,FileItem item) throws ServletException, IOException {
String realPath = getServletContext().getRealPath("/WEB-INF/upload");
/*System.out.println(realPath);*/
String newFileName = UUID.randomUUID().toString()+"_"+fileName;
String inPath = PathHashUtils.getPathFromHash(newFileName);
File file = new File(realPath+"/"+inPath);
if(!file.exists()){
file.mkdirs();
}
byte[] buf = new byte[1024];
int len = -1;
BufferedInputStream bin = new BufferedInputStream(in);
BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File(file,newFileName)));
while((len=bin.read(buf))!=-1){
bout.write(buf, 0, len);
bout.flush();
}
bin.close();
bout.close();
item.delete(); //item删除掉,若DiskFileItemFactory中设置了临时文件,则可以将临时文件删除掉。
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request,response);
}
}
文件上传的几个注意事项:
1.中文乱码问题
a、普通输入域请求参数的中文乱码问题
FileItem.getString("UTF-8");
b、上传的文件中文名称问题
方式一:request.setCharacterEncoding("UTF-8");
方式二:ServletFileUpload.setHeaderEncoding("UTF-8");
2、如何保证服务器的安全
上传的文件存放地址不能让用户直接访问到。放到WEB-INF目录下
3、多次上传同名文件的覆盖
如若上传的文件名是:a.txt
方式一:123193219321_a.txt 当前时间的毫秒值。(乐观者)
方式二:UUID_a.txt
4、如何防止同一目录下文件太多的问题
方式一:当前的日期作为一个目录。当天上传的文件放到当前的日期目录中。(乐观者)
方式二:hashCode算法,打散存储文件。 将文件名如UUID_a.txt,计算出此文件名字符串的低4位hashCode值,再计算字符串的第二个4位的hashCode值用16进制字符获取计算结果。如结果为:12/8
如:/WEB-INF/upload/12/8/a.txt
5、上传文件的大小控制(单个文件和总大小)及友好的提示用户
a、单个文件大小控制
ServletFileUpload类的setFileSizeMax(long?fileSizeMax)方法来控制,
超过大小,通过catch出此FileUploadBase.FileSizeLimitExceededException类异常来进行处理。
b、总文件大小控制
ServletFileUpload类的setSizeMax(long?sizeMax) 方法来控制,
超过大小,通过catch出此FileUploadBase.SizeLimitExceededException类异常来进行处理。
6、超出10k的文件的临时文件的处理
组件不会自动删除临时文件。
FileItem.delete()手工删除临时文件。必须关流之后再删除。
7、限制上传文件的类型
若只允许jpg文件
方法一:过滤文件名的后缀名。(乐观者)
方法二:FileItem.getContentType得到MIME类型,进一步的过滤
8、监听文件的上传进度
注册监听器:
ServletFileUpload.setProgressListener(new ProgressListener())
9、用户没有选择文件上传时的问题
通过FileItem.getName().length()长度来判断,用户在前面应该选择的文件域是否选择了文件。若length<1,则没有选择文件。
三、文件下载
其实就是通过获取文件名后,构造InputStream。然后读取InputStream,将流中的数据写入到Servlet.getOutputStream中就可以了。注意文件名中文乱码及设置response.setHeader(“content-disposition”,”attachment;filename=x”); 见“那么年web乱码解决终极办法”。