JavaWeb(十二)上传和下载

文件的上传和下载无时不在,文件是如何上传至服务器又是如何下载至服务器,文件在服务器的保存形式又是什么,浏览器又怎么知道这是个文件,这就是需要了解的地方。

目录

文件上传

    上传的前提

    上传的演示

手动解析

FileUpload

    上传的几个注意事项

大小限制

目录分配

文件下载

    下载的要求

    下载的演示

后话


文件上传

    上传的前提

    在学习如何实现上传之前,需要先知道上传所需的要求:

        1.必须通过表单,并且表单的请求方式为POST

        2.表单的属性enctype的值必须是multipart/form-data

        3.表单中需要附有file表单字段

    第二点需要说明一下,设置这个属性的值为multipart/form-data是为了设置表单传递到服务器端的不是key-value形式而是传递的字节码形式。

    上传的演示

手动解析

    首先第一个问题:文件上传时,浏览器是将文件以什么样的形式提交到服务器端的?答:以流的形式

    创建一个页面,代码如下所示:

    <form action='<c:url value='/UploadServlet3'></c:url>' method="post" enctype="multipart/form-data">
        用户名:<input type="text" name="username" /> <br/>
        头像:<input type="file" name="portrait" /> <br/>
        <input type="submit" value="提交" />
    </form>

    在浏览器F12查看请求内容,其如下所示:

    在请求内容中会发现,每一个分隔符下面都有一个表单项的相关内容,我们称其为部件,每一个部件包含了请求头、空行、请求体。

    普通的表单项只有一个请求头:Content-Disposition,即表单项的名称。而请求体则是表单项的值;

    而文件表单项则包含了两个请求头:Content-DispositionContent-Type,前者表示表单项的名称并且还附带文件名,后者表示文件的MIME类型。MIME类型这里不过多解释,这里只举一个例子:text/html就是MIME,表示后缀为html的文本文件。而请求体则是文件的内容。

    第二个问题:如何获取到表单项的值?

    在先前学习的时候,都是通过request.getParameter(String name)来获取表单项的值,但是这个方法返回的是一个字符串类型,在这已经不适用了。那么就得用另一个方法来获取表单项的值,即request.getInputStream()方法来获取ServletInputStream。这里提一下ServletInputStream类的作用,其提供流从请求对象读取二进制数据。

    接下来演示一下使用ServletInputStream来得到表单的数据,代码如下所示:

public class UploadServlet extends HttpServlet {

    public void doPost(HttpServletRequest request, HttpServletResponse response) {
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        
        ServletInputStream in = request.getInputStream();
        System.out.println(IOUtils.toString(in, "UTF-8"));
    }
}

    结果会发现读取的内容和在浏览器中看的内容是一致的。

    接着第三个问题:如何将文件的内容提取出来并保存至WEB-INF文件夹内?

    观察一下Http请求体中的结构,一行随机字符串标识符,一行内容文件头(即两个请求头),一行空行,第四行才是文件的内容。那么想要得到文件的二进制数据就得去掉前三行的内容。

    代码如下所示:

public class UploadServlet extends HttpServlet {

    public void doPost(HttpServletRequest request, HttpServletResponse response) {
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");

        // 获得请求的数据流
        ServletInputStream in = request.getInputStream();

        BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
        String firstLine = br.readLine();
        System.out.println("随机字符串:" + firstLine);
        String fileName = br.readLine();
        /*
         * 	有些浏览器上传的文件是绝对路径,即带有盘符路径的文件
         *  根据路径的特性,只需要找到最后的\即可,如果有这个符号则截取后半部分
         *  需要注意的是获取最后部分的文件名时需要将双引号去掉
         * */
        fileName = fileName.substring(fileName.lastIndexOf("filename=") + 9).replace("\"", "");
        int index = fileName.lastIndexOf("\\");
        if (index != -1) {
            fileName = fileName.substring(index + 1);
        }
        System.out.println("文件名:" + fileName);
        System.out.println("Content-Type:" + br.readLine());
        System.out.println("空行:" + br.readLine());
		
        // 获取上传的文件夹路径的真实路径
        String path = getServletContext().getRealPath("/WEB-INF");
        PrintWriter out = new PrintWriter(path + "\\" + fileName);
		
        System.out.println("文件内容:");
        String content = null;
        while((content = br.readLine()) != null) {
            // System.out.println(content);
            if (content.contains(firstLine + "--"))
                break;
            out.println(content);
        }
		
        out.close();
        br.close();
        in.close();
    }
}

    以上传图片为例,找到WEB-INF文件夹,图片成功上传,结果如下所示:

FileUpload

    每次做上传功能都需要对流进行解析处理,未免过于麻烦,好在Apache已经编写了一个工具专门来处理解析,其名为FileUpload,它会帮忙解析request中的上传数据,解析后的结果是一个表单项数据封装到了一个FileItem对象中。

    向编译器中导包,需要注意的是,不仅仅需要导入FileUpload的包还要导入一个名为commons-io的jar包,因为io包是FileUpload的依赖包。

    在使用之前先介绍一下需要用到的几个类:DiskFileItemFactoryServletFileUpload以及FileItem

    从API来看,DiskFileItemFactory是创建FileItem类的实例然后将内容保存在内存中或者硬盘中。其构造方法如下所示:

    public DiskFileItemFactory() 
    public DiskFileItemFactory(int sizeThreshold, java.io.File repository) 

    其中,第一个参数sizeThreshold表示如果上传的文件小于这个设定的阈值就将文件保存在内存中,如果大于这个阈值就讲文件保存在硬盘里。默认值为10KB;第二个参数repository是临时目录,其默认值为System.getProperty("java.io.tmpdir")。

    至于ServletFileUpload可以每个HTML部件处理多个文件,解析器的构造方法如下所示:

    public ServletFileUpload()
    public ServletFileUpload(FileItemFactory fileItemFactory)

    ServletFileUpload通过其中的一个方法解析request,如下所示:

    public java.util.List parseRequest(javax.servlet.http.HttpServletRequest request)

    它会处理符合RFC 1867协议的多部分组件,至于RFC 1867概括地说就是符合上传的前提的协议。它会返回一个List<FileItem>的对象。

    最后FileItem是一个接口类,实现类有两个,一个是DefaultFileItem,另一个是DiskFileItem。主要学习第二个,其有几个方法获取表单项相关的数据,如下所示:

    // 判断是否为普通的表单项,true为普通表单项,false为文件表单项
    public boolean isFormField();
    // 返回当前表单项的名称
    public String getFieldName();
    // 返回表单项的值
    public String getString(String charset);
    // 返回上传的文件名称
    public String getName()
    // 返回上传文件的大小,即字节数
    public long getSize();
    // 返回上传文件对应的输入流
    public InputStream getInputStream()
    // 将上传的文件保存到指定文件
    public void write(File file);

    使用这个小工具的步骤如下:由工厂创建解析器,然后使用解析器解析request,得到FileItem集合,以获取需要的表单项数据。

    了解完这三个类后,就可以开始编写代码演示:

public class UploadServlet extends HttpServlet {

    public void doPost(HttpServletRequest request, HttpServletResponse response) {
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        String path = request.getServletContext().getRealPath("/WEB-INF");
		
        // 1.创建工厂类
        DiskFileItemFactory factory = new DiskFileItemFactory();
        // 2.创建ServletFileUpload实例
        ServletFileUpload sfu = new ServletFileUpload(factory);
        try {
            // 3.解析请求
            List<FileItem> fileItems = sfu.parseRequest(request);
            for (FileItem fileItem : fileItems) {
                if (fileItem.isFormField()) {
                    System.out.println("普通表单项演示:" + fileItem.getFieldName() +
                                " = " + fileItem.getString("UTF-8"));
                } else {
                    System.out.println("文件表单项演示:");
                    System.out.println("Content-Type:" + fileItem.getContentType());
                    System.out.println("size:" + fileItem.getSize());
                    System.out.println("fileName:" + fileItem.getName());
					
                    File destFile = new File(path + "\\" + fileItem.getName());
                    fileItem.write(destFile);
                }
            }
        } catch (FileUploadException e) {
            throw new RuntimeException(e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

    显示结果如下:

    上传的几个注意事项

大小限制

    所谓的大小限制可以分为两种:单个文件的大小限制以及整个请求所有数据的大小限制。需要注意的是,设置大小限制需要在解析之前就完成,否则没有效果。相关方法如下所示:

    // 设置单个上传文件的最大字节
    public void setFileSizeMax(long fileSizeMax);
    // 设置整个请求的最大字节
    public void setSizeMax(long sizeMax);

通过ServletFileUpload就可以使用这两个方法,但实际上这两个方法是FileUploadBase的方法,而FileUploadBase则是前者的父类。

当超出设置限制的值时,在解析request时会抛出异常,这个异常可用于后续的处理。

目录分配

    这里说的其实是不能在WEB-INF下存放太多文件。为了不让一个目录下存放太多文件,使用哈希值来分配目录。具体的思路如下:

        通过文件名获得哈希值,转换成16进制的哈希值后提取前两位来生成包含目录,即目录为两层,两位各一层。  

    还需要处理的问题就是:为了避免可能出现的同目录同名问题,在文件名前添加uuid。

    演示代码如下:

public class UploadServlet04 extends HttpServlet {

    public void doPost(HttpServletRequest request, HttpServletResponse response) {
        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        String path = request.getServletContext().getRealPath("/WEB-INF/uploads/");
		
        // 1.创建工厂类
        DiskFileItemFactory factory = new DiskFileItemFactory();
        // 2.创建ServletFileUpload实例
        ServletFileUpload sfu = new ServletFileUpload(factory);
        try {
            // 3.解析请求
            List<FileItem> fileItems = sfu.parseRequest(request);
            for (FileItem fileItem : fileItems) {
                if (!fileItem.isFormField()) {
                    String fileName = fileItem.getName();
                    // 处理路径问题
                    int index = fileName.lastIndexOf("\\");
                    if (index != -1) 
                        fileName = fileName.substring(index+1);
                    String saveName = CommonUtils.uuid() + "_" + fileName;
					
                    // 根据哈希值创建目录
                    String hashCode = Integer.toHexString(fileName.hashCode());
                    File dirFile = new File(path, hashCode.charAt(0) + "/" + hashCode.charAt(1));
                    dirFile.mkdirs();
                    // 保存文件
                    File destFile = new File(dirFile, saveName);
                    fileItem.write(destFile);
                }
            }
        } catch (FileUploadException e) {
            throw new RuntimeException(e);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

    结果如下所示:

文件下载

在文件上传的时候说过,上传的文件内容其实是一堆二进制字节码,那么下载其实就是将保存在服务器的文件变成一个字节数组,然后响应给浏览器,触发下载工具。

    下载的要求

    下载的时候,浏览器需要知道文件的类型(即MIME是什么)、文件名称以及最重要的文件的数据。

    下载的演示

    下载的使用比上传更加地简单,用例代码如下所示(以刚刚分配目录的文件为例):

public class DownLoadServlet extends HttpServlet {	

    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        String fileName = request.getParameter("name");
        String path = this.getServletContext().getRealPath("/WEB-INF/uploads/");
        
        // 根据文件名的哈希值确定目录
        String hashCode = Integer.toHexString(fileName.hashCode());
        File dirFile = new File(path, hashCode.charAt(0) + "/" + hashCode.charAt(1));
        if(dirFile.exists()) {
            File[] files = dirFile.listFiles();
            for(File file : files) {
                if (file.isFile()) {
                    if(file.getName().contains(fileName)) {
                        /* 
                         * 上传的时候是以UUID_fileName为命名的
                         * 因此需要从_字符开始截取后面的文件名
                         */
                        path = file.getPath();
                        fileName = path.substring(path.lastIndexOf("_") + 1);
                        String contentType = this.getServletContext().getMimeType(fileName);
                        String contentDisposition = "attachment;filename=" + new String(fileName.getBytes("GBK"), "ISO-8859-1");
                        FileInputStream input = new FileInputStream(file);
                        response.setHeader("Content-Type", contentType);
                        response.setHeader("Content-Disposition", contentDisposition);
                        ServletOutputStream output = response.getOutputStream();
                        IOUtils.copy(input, output);
						
                        input.close();
                    }
                }
            }
        } else {
            System.out.println("文件夹不存在");
            return ;
        }
    }
}

    网页代码如下所示:

<body>
    <!--  疯狂Java实战演义.pdf -->
    <a href="<c:url value='/DownLoadServlet?name=无标题.png' />">点击这里下载</a>
</body>

    结果如下所示:

    现在讲解一下下载的各个知识点。如果只是单纯的一个下载的话,笼统地说,只需要三步即可完成:

        1.获取文件的类型

        2.获取文件的名称

        3.通过真实路径获取文件,并且写入输出流中

    这三步其实就是满足前面所提到的三个要求,而代码所体现的则是通过

        String contentType = this.getServletContext().getMimeType(fileName);
        String contentDisposition = "attachment;filename=" + fileName);
        ServletOutputStream output = response.getOutputStream();
        response.setHeader("Content-Type", contentType);
        response.setHeader("Content-Disposition", contentDisposition);
        IOUtils.copy(input, output);

    设置Content-Type则是告诉浏览器:传递给浏览器的文件是什么MIME类型;具体则通过ServletContext中的getMimeType()方法获取MIME类型。

    设置Content-Disposition则是让浏览器直接打开文件还是弹出下载窗口。其默认值为inline,即浏览器内打开。若想要弹出下载窗口则是另一个值attachment。而分号后面则是代表显示在下载窗口中的文件名,即下载文件到硬盘后的文件名。

    需要注意的是下载框中显示中文名称乱码问题,一般情况而言,只需要对文件名作如下处理即可解决:

    fileName = new String(fileName.getBytes("GBK"), "ISO-8859-1");

    即将文件名的编码转成浏览器能够识别的ISO-8859-1编码。

    而有一个浏览器比较特殊,火狐浏览器。它是BASE64编码的,而其他浏览器则是URL编码的。因此需要获取浏览器,如果是火狐则特殊处理,而其他都用通用方法即可。即主要代码如下所示:

public class DownUtils {
    public static String fileNameEncoding(String fileName, HttpServletRequest request) {
        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 {
            filename = URLEncoder.encode(filename, "UTF-8");
        }
    }
}

后话

    '''
        上传和下载没有多大的难度,只需要记住其中的思路以及一些细节即可
    '''

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值