1. 文件上传与下载的定义

  • 数据上传是指客户端向服务器上传数据,客户端向服务器发送的所有请求都属于数据上传。文件上传是数据上传的一种特例,指客户端向服务器上传文件。即将保存在客户端的文件上传至服务器中的一个副本,保存到服务器中。
  • 文件下载指的是文件从服务器下载到浏览器后,浏览器不直接解析,而是以附件的形式保存到客户端。

2. 以表单的方式实现文件上传

上传表单的要求

  • method属性值必须是post,不能是get (因为post对请求上传的文件大小原理上没有要求)
  • 要为表单指定enctype属性值为mutipart/form-data (enctype即encoding type,编码类型)
<form action="/myWeb/one" method="post" enctype="multipart/form-data">
        姓名:<input type="text" name="name"><br/>
        年龄:<input type="text" name="age"><br/>
        照片:<input type="file" name="photo"><br/>
      <input type="submit" value="注册">
    </form>

服务端需要做的事情:

  1. 判断是否是multipart请求
  2. 创建一个FileItem工厂
  3. 创建文件上传核心组件
  4. 解析请求,获取到所有的item
  5. 遍历获取到的item,如果只是普通的表单项则直接获取即可,若是文件表单项,则先获取文件上传的原始名称,获取输入流,同时指定要存储路径,然后创建目标文件用于保存上传
  6. 根据目标文件创建文件输出流,将输入流中的数据写入到输出流中
public class OneServlet extends HttpServlet {
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        if (!ServletFileUpload.isMultipartContent(request)){ // 1. 判断是否为multipart请求
            throw new RuntimeException("当前请求不支持文件上传");
        }
        try {
            // 创建一个FileItem工厂
            DiskFileItemFactory factory = new DiskFileItemFactory();

            // 设置使用临时文件的边界值,大于该值,上传文件会先保存在临时文件中,否则,上传文件将直接写入到内存中
            // 单位:字节
            factory.setSizeThreshold(1024*1024*1); // 设置边界值为1M

            // 更改临时文件存放位置
            String tempPath = this.getServletContext().getRealPath("/temp");
            File temp = new File(tempPath);
            factory.setRepository(temp);


            // 创建文件上传核心组件
            ServletFileUpload upload = new ServletFileUpload(factory);

            // 设置每一个item的头部字符编码,其可以防止文件名中文乱码问题
            upload.setHeaderEncoding("utf-8");

            // 设置单个文件上传的最大边界值为2M
            upload.setFileSizeMax(1024 * 1024 * 2);

            // 设置一次上传所有文件的最大边界值为5M
            upload.setSizeMax(1024 * 1024 * 5);

            // 解析请求,获取到所有的item
            List<FileItem> items = upload.parseRequest(request);
            for (FileItem item : items){
                if (item.isFormField()){ // 若item为普通表单项
                    String fieldName = item.getFieldName(); // 获取表单名称
                    String fieldValue = item.getString("utf-8"); // 获取表单项的值,并指定编码格式
                    System.out.println(fieldName + " = " + fieldValue);
                }else{ // 若item为文件表单项
                    String fileName = item.getName(); // 获取文件上传的原始名称
                    fileName = System.currentTimeMillis() + fileName; // 防止文件名重复
                    InputStream is = item.getInputStream(); // 获取输入流,其中有上传文件的内容
                    String path = this.getServletContext().getRealPath("/images"); // 获取文件保存在服务器的路径

                    /**
                     * 避免所有资源放同一文件夹,因为在linux系统中一个文件夹中的文件数量是有限制的
                     */
                    // 获取当前系统时间
                    Calendar now = Calendar.getInstance();
                    // 获取年月日
                    int year = now.get(Calendar.YEAR);
                    int month = now.get(Calendar.MONTH) + 1;
                    int day = now.get(Calendar.DAY_OF_MONTH);


                    path = path + "/" + year + "/" + month + "/" + day;
                    File dirFile = new File(path);
                    if (!dirFile.exists()){ // 如果该目录不存在则创建该目录
                        dirFile.mkdirs();
                    }

                    // System.out.println(path);
                    File descFile = new File(path, fileName); // 创建目标文件,将来用于保存上传
                    OutputStream os = new FileOutputStream(descFile); // 创建文件输出流
                    // 将输入流中的数据写入到输出流中
                    int len = -1;
                    byte [] buf = new byte[1024];
                    while ((len = is.read(buf)) != -1){
                        os.write(buf,0, len);
                    }
                    // 关闭流
                    os.close();
                    is.close();

                    item.delete(); // 删除临时文件
                }
            }
        } catch (FileUploadException e) {
            e.printStackTrace();
        }
    }
}

部分代码分析

  1. 设置在什么情况下上传的文件会暂时保存在内存,什么情况下会保存在临时文件中。(最好指定一下,因为临时文件是存放在磁盘中,可以为服务器减少点压力)
// 设置使用临时文件的边界值,大于该值,上传文件会先保存在临时文件中,否则,上传文件将直接写入到内存中
            // 单位:字节
            factory.setSizeThreshold(1024*1024*1); // 设置边界值为1M
  1. 更改临时文件的存放位置,易于管理和掌控。
// 更改临时文件存放位置
            String tempPath = this.getServletContext().getRealPath("/temp");
            File temp = new File(tempPath);
            factory.setRepository(temp);
  1. 删除临时文件(没有下面这行代码的话,它在上传完成之后也不会自动删除,占用服务器磁盘空间)
item.delete(); // 删除临时文件
  1. 设置item头部字符编码,防止文件名中文乱码问题。upload.setHeaderEncoding("utf-8");
  2. 获取普通表单项的值时指定编码,防止中文乱码String fieldValue = item.getString("utf-8");
  3. 设置上传文件的路径
/**
                     * 避免所有资源放同一文件夹,因为在linux系统中一个文件夹中的文件数量是有限制的
                     */
                    // 获取当前系统时间
                    Calendar now = Calendar.getInstance();
                    // 获取年月日
                    int year = now.get(Calendar.YEAR);
                    int month = now.get(Calendar.MONTH) + 1;
                    int day = now.get(Calendar.DAY_OF_MONTH);


                    path = path + "/" + year + "/" + month + "/" + day;
                    File dirFile = new File(path);
                    if (!dirFile.exists()){ // 如果该目录不存在则创建该目录
                        dirFile.mkdirs();
                    }

这段代码就很有讲究了,它会根据不同天去创建文件夹,然后存放该天上传的所有文件。
7. 设置存放到服务器的文件名(即目标文件名)fileName = System.currentTimeMillis() + fileName; 为了防止用户上传两次1.png,导致前一次上传的1.png被覆盖的情况,这里对目标文件名在原文件名的基础上加上了当前毫秒数,避免因文件名重复而出现覆盖的情况。

需要用到的jar包

  1. commons-fileupload-xxxx.jar
  2. commons-io-xxx.jar 第二个的版本需要根据第一个的版本作选择,具体怎么选看文档即可。

3. 以超链接的方式实现文件下载

简单形式:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<a href="${pageContext.request.contextPath}/images/1.png">1.png</a>
</body>
</html>

直接让超链接指向该文件即可。但是这样会有一个缺点就是,对于浏览器能解析的资源浏览器会直接解析显示,然后需要用户进一步点击另存为保存下载,对于浏览器解析不了的,才会以附件的形式出现。即资源文件的出现形式由浏览器解析能力决定,不由服务器决定

于是我们进行改进,首先让超链接指向一个servlet

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Title</title>
</head>
<body>
<a href="${pageContext.request.contextPath}/two">1.png</a>
</body>
</html>

然后在该servlet中进行返回资源的操作

public class TwoServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String fileName = "陌言.png";

        // 打散:按当前字符编码进行打散
        byte[] bytes = fileName.getBytes("utf-8");
        // 组装:按目标字符编码进行组装
        fileName = new String(bytes, "ISO8859-1");

        // 修改响应的头部属性content-disposition值为attachment
        response.setHeader("content-disposition", "attachment;filename=" + fileName);

        // 获取连接服务器端资源文件的输入流
        InputStream is = this.getServletContext().getResourceAsStream("/images/1.png");
        // 获取输出流
        ServletOutputStream os = response.getOutputStream();
        // 将输入流中数据写入到输出流
        int len = -1;
        byte [] buf = new byte[1024];
        while ((len = is.read(buf)) != -1){
            os.write(buf, 0, len);
        }
        // 关闭流
        os.close();
        is.close();
    }
}