上传和下载

文件上传

文件上传的要求

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>

对上传的文件进行抓包查看

java判断路径是否存在创建文件夹和文件_java web

java判断路径是否存在创建文件夹和文件_表单_02

发现正文部分是由多个部件组成,每个部件对应一个表单字段,每个部件都有自己的头信息。头信息下面是
空行,空行下面是字段的正文部分。多个部件之间使用随机生成的分隔线隔开。

普通表单项
        -一个头: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.上传文件的总大小限制

上传文件的表单中可能允许上传多个文件

java判断路径是否存在创建文件夹和文件_java_03

有时我们需要限制一个请求的大小。也就是说这个请求的最大字节数(所有表单项之和)!实现这一功能也很简单,只需要调用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;
    }
}