上传和下载
文件上传
文件上传的要求
1. 必须使用表单,而不能是超链接;
2. 表单的method必须是POST,而不能是GET;
3. 表单的enctype必须是multipart/form-data,即设置为二进制传输数据;
4. 在表单中添加file表单字段,即 <input type="file" name="xxx"/>
<form action="/day22_1/AServlet" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="username"><br>
文 件:<input type="file" name="pic"><br>
<input type="submit" value="提交">
</form>
对上传的文件进行抓包查看
发现正文部分是由多个部件组成,每个部件对应一个表单字段,每个部件都有自己的头信息。头信息下面是
空行,空行下面是字段的正文部分。多个部件之间使用随机生成的分隔线隔开。
普通表单项
-一个头:Content-Disposition: form-data; name="username" 即表单项名称
正文,也就是值
文件表单项
-两个请求头
Content-Disposition: form-data; name="pic"; filename="a.jpg" 表单项名称和文件名称
Content-Type: image/jpeg 文件的MIME类型
正文
文件上传表单的数据也是被封装到request对象中的。
request.getParameter(String)方法获取指定的表单字段字符内容,但文件上传表单已经不在是字符内容,而是字节内容,所以失效,不能再通过这个方法来获取数据。
这时可以使用request的getInputStream()方法获取ServletInputStream对象,它是InputStream的子类,这个ServletInputStream对象对应整个表单的正文部分(从第一个分隔线开始,到最后),这说明我们需要的解析流中的数据。我们在servlet获取表单项,然后将其分割就可以得到表单项的头和表单项的体。当然解析它是很麻烦的一件事情,而Apache已经帮我们提供了解析它的工具:commons-fileupload。
fileupload工具
简介
fileupload是由apache的commons组件提供的上传组件。它最主要的工作就是帮我们解析request.getInputStream()。
fileupload组件需要的JAR包有:
commons-fileupload.jar,核心包;
commons-io.jar,依赖包。
fileupload核心类
fileupload的核心类有:DiskFileItemFactory、ServletFileUpload、FileItem。
使用fileupload组件的步骤如下:
1. 创建工厂类DiskFileItemFactory对象:DiskFileItemFactory factory = new DiskFileItemFactory()
2. 使用工厂创建解析器对象:ServletFileUpload fileUpload = new ServletFileUpload(factory)
3. 使用解析器来解析request对象:List<FileItem> list = fileUpload.parseRequest(request)
FileItem类。
一个FileItem对象对应一个表单项(表单字段)。一个表单中存在文件字段和普通字段,可以使用
FileItem类的isFormField()方法来判断表单字段是否为普通字段,如果不是普通字段,那么就是文件字段了。
String getName():获取文件字段的文件名称;
String getString():获取字段的内容,如果是文件字段,那么获取的是文件内容,当然上传的文件必须是文本文件,里面还可以添加编码参数,例如getString("utf-8");
String getFieldName():获取字段名称,例如:<input type=”text” name=”username”/>,返回的是username;
String getContentType():获取上传的文件的类型,例如:text/plain。
int getSize():获取上传文件的大小;
boolean isFormField():判断当前表单字段是否为普通文本字段,如果返回false,说明是文件字段;
InputStream getInputStream():获取上传文件对应的输入流;
void write(File):把上传的文件保存到指定文件中。
简单的上传案例
表单
<form action="/day22_1/AServlet" method="post" enctype="multipart/form-data">
用户名:<input type="text" name="username"><br>
文 件:<input type="file" name="pic"><br>
<input type="submit" value="提交">
</form>
完成我们的Servlet
public class AServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
DiskFileItemFactory df=new DiskFileItemFactory(); //获取工厂
ServletFileUpload sf=new ServletFileUpload(df); //获取解析器
try {
List<FileItem> l=sf.parseRequest(request); //获取FileItem集合
FileItem f1=l.get(0); //获取FileItem对象,我们表单就两项,所以就不遍历了
FileItem f2=l.get(1);
System.out.println("普通表单项"+f1.getFieldName()+":"+f1.getString("utf-8"));
System.out.println("文件表单项:文件名,"+f2.getName()+"文件大小,"+f2.getSize()+"文件类型,"+f2.getContentType());
File file=new File("d:/copy.jpg"); //创建文件
try {
f2.write(file); //将上传的文件写入到创建的文件中
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} catch (FileUploadException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
文件上传注意事项
1.要把上传的文件放在WEB-INF目录下
原因:
如果没有把用户上传的文件存放到WEB-INF目录下,那么用户就可以通过浏览器直接访问上传的文件,这是非常危险的。比如上传一个jsp页面,jsp页面包含了一些恶意的java代码。
解决方法:
通常我们会在WEB-INF目录下创建一个uploads目录来存放上传的文件,而在Servlet中找到这个目录需要使用ServletContext的getRealPath(String)方法,例如在我的upload1项目中有如下语句:
ServletContext servletContext = this.getServletContext();
String savepath = servletContext.getRealPath(“/WEB-INF/uploads”);
其中savepath为:F:\tomcat6_1\webapps\upload1\WEB-INF\uploads
2.上传的文件名称
有的浏览器上传的文件名称是绝对路径,也就是c:\\test\a.txt
现在大部分的浏览器都杜绝了这种情况,但为了我们程序的健壮性,我们应该解决一下这个问题。
我们只需要获取最后一个 “\”的位置,然后截取即可。
String name = file1FileItem.getName();
int lastIndex = name.lastIndexOf("\\");//获取最后一个“\”的位置
if(lastIndex != -1) {//注意,如果不是完整路径,那么就不会有“\”的存在。
name = name.substring(lastIndex + 1);//截取文件名称
}
3.上传文件同名问题
原因 :
通常我们会把用户上传的文件保存到uploads目录下,但如果用户上传了同名文件呢?这会出现覆盖的现象。
解决方法 :
处理这一问题的手段是使用UUID生成唯一名称,然后再使用“_”连接文件上传的原始名称。
4.一个目录不能存放过多的文件
原因:一个目录下不应该存放过多的文件,会造成卡顿等情况。
解决方法:我们按照某种规则生成目录。
按照日期生成:每天的日期作为目录,会出现某天上传文件特别多的情况。
按照首字母生成:中文的首字母过多,会造成目录过多的情况,
我们这里使用hash算法来打散:
1. 获取文件名称的hashCode:int hCode = name.hashCode();;
2. 获取hCode的低4位,然后转换成16进制字符;
3. 获取hCode的5~8位,然后转换成16进制字符;
4. 使用这两个16进制的字符生成目录链。例如低4位字符为“5”
这种算法的好处是,在uploads目录下最多生成16个目录,而每个目录下最多再生成16个目录,即256个目录,所有上传的文件都放到这256个目录下。如果每个目录上限为1000个文件,那么一共可以保存256000个文件。
例如上传文件名称为:新建 文本文档.txt,那么把“新建 文本文档.txt”的哈希码获取到,再获取哈希码的低4位,和5~8位。假如低4位为:9,5~8位为1,那么文件的保存路径为uploads/9/1/。
5.单个上传文件大小限制
限制上传文件的大小很简单,ServletFileUpload类的setFileSizeMax(long)就可以了。参数就是上传文件的上限字节数,例如servletFileUpload.setFileSizeMax(1024*10)表示上限为10KB。
一旦上传的文件超出了上限,那么就会抛出FileUploadBase.FileSizeLimitExceededException异常。我们可以在Servlet中获取这个异常,然后向页面输出“上传的文件超出限制”。
public class AServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
DiskFileItemFactory df=new DiskFileItemFactory(); //获取工厂
ServletFileUpload sf=new ServletFileUpload(df); //创建解析器
sf.setFileSizeMax(1024*10); //设置单个文件大小限制
try {
List<FileItem> l=sf.parseRequest(request);
FileItem f1=l.get(0);
FileItem f2=l.get(1);
File file=new File("d:/copy.jpg");
f2.write(file);
}catch (Exception e) {
if(e instanceof FileSizeLimitExceededException) //如果出现这个异常,就保存错误信息
{
request.setAttribute("msg", "文件超出10kb");
request.getRequestDispatcher("/index.jsp").forward(request, response);
return;
}
throw new RuntimeException();
}
}
}
6.上传文件的总大小限制
上传文件的表单中可能允许上传多个文件
有时我们需要限制一个请求的大小。也就是说这个请求的最大字节数(所有表单项之和)!实现这一功能也很简单,只需要调用ServletFileUpload类的setSizeMax(long)方法即可。
例如fileUpload.setSizeMax(1024 * 10);,显示整个请求的上限为10KB。当请求大小超出10KB时,ServletFileUpload类的parseRequest()方法会抛出FileUploadBase.SizeLimitExceededException异常。
7.缓存大小与临时目录
大家想一想,如果我上传一个蓝光电影,先把电影保存到内存中,然后再通过内存copy到服务器硬盘上,那么你的内存能吃的消么?
所以fileupload组件不可能把文件都保存在内存中,fileupload会判断文件大小是否超出10KB,如果是那么就把文件先保存到硬盘上,然后再转到上传目录,如果没有超出,那么就保存在内存中,转到上传目录。
10KB是fileupload默认的值,我们可以来设置它。
当文件保存到硬盘时,fileupload是把文件保存到系统临时目录,当然你也可以去设置临时目录。
public class AServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//设置缓存大小和临时目录
DiskFileItemFactory df=new DiskFileItemFactory(1024*20,new File("f://temp"));
ServletFileUpload sf=new ServletFileUpload(df);
try {
List<FileItem> l=sf.parseRequest(request);
FileItem f1=l.get(0);
FileItem f2=l.get(1);
File file=new File("d:/movie.avi");
f2.write(file);
}catch (Exception e) {
throw new RuntimeException();
}
}
}
我们可以看到临时目录会多出文件,上传结束后文件消失。
文件下载
下载就是将文件字节,通过response.getOutputStream获取输出流来响应给客户端
下载需要设置两个响应头
-Content-Type:文件的MIME类型
-Content-Disposition:默认值为inline。即为在浏览器窗口打开。比如我们下载一个文本文件,他会将文本直接呈现在浏览器上。所以我们需要改为 "attachment;name=xxx",即弹出下载框。name为指定文件名字
public class DownServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
FileInputStream in=new FileInputStream("f://down.avi"); //获取文件的输入流
String contenttype=request.getServletContext().getMimeType("down.avi"); //获取MiME类型
response.setHeader("Content-Type", contenttype); //设置两个响应头
response.setHeader("Content-Disposition", "attachment;name=down.avi");
ServletOutputStream out=response.getOutputStream(); //获取响应的输出流
IOUtils.copy(in, out); //调用我们的工具类将输入流的内容复制到输出流
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// TODO Auto-generated method stub
doGet(request, response);
}
}
下载时中文文件名乱码问题
我们可以在Servlet中将文件名进行编码,
filename=new String(file.getBytes("gbk"),"iso-8859-1")
但是这种方案会有一些字符显示不出来。
我们使用下面这种方法来解决
public class DownUtils {
public static String filenameEncoding(String filename, HttpServletRequest request) throws IOException {
String agent = request.getHeader("User-Agent"); //获取浏览器
if (agent.contains("Firefox")) {
BASE64Encoder base64Encoder = new BASE64Encoder();
filename = "=?utf-8?B?"
+ base64Encoder.encode(filename.getBytes("utf-8"))
+ "?=";
} else if(agent.contains("MSIE")) {
filename = URLEncoder.encode(filename, "utf-8");
} else {
filename = URLEncoder.encode(filename, "utf-8");
}
return filename;
}
}