11-Upload&Download

文件上传

开发步骤

1. 提供表单,允许用户通过表单选择文件进行上传

  • 表单必须是 POST 提交(表单默认为 GET 提交,请求参数不能超过 1KB)
  • 表单输入项必须有 name 属性(虽然文件表单输入项的name到后台没啥用),一个表单输入项如果没有 name 属性,Browser 是不会把它当作请求参数提交的。示例:<input type="file" name="file1"/>
  • <form> 加上 enctype="multipart/form-data" 属性

2. 在 Servlet 中将上传的文件保存在 sever 的硬盘中

  • 由于没有提供原生 API,需要自己手动实现:先用request.getInputStream() 调用该方法获取包含请求正文的 ServletInputStream 对象,然后一大堆步骤,比如要拿到分隔符,然后分割请求正文 .... 很麻烦。所以,功能实现需要基于 apache 提供的 jar:commons-fileupload-1.2.1.jar 和 commons-io-1.4.jar。
  • 工作流程图示:
  • 对照图大致理解下代码:
    // 创建工厂
    DiskFileItemFactory factory = new DiskFileItemFactory();
    // 生产文件上传核心类
    ServletFileUpload fileUpload = new ServletFileUpload(factory);
    // 利用文件上传核心类解析request
    List<FileItem> list = fileUpload.parseRequest(request);
    // 遍历所有的FileItem
    for(FileItem item : list) {
        if(item.isFormField()) { // 当前是一个普通的字段项
            String name = item.getFieldName();
            String value = item.getString();
            System.out.println(name+" = "+value);
        } else { // 当前是一个文件上传项
            String fileName = item.getName();
            InputStream in = item.getInputStream();
            OutputStream out = new FileOutputStream(
                getServletContext().getRealPath("upload/"+fileName));
            IOUtils.transfer(in, out);
            IOUtils.close(in,out);
        }
    }
    

相关 API

  • DiskFileItemFactory
    DiskFileItemFactory(int sizeThreshold, File repository)
    DiskFileItemFactory()
        void setSizeThreshold(int sizeThreshold)
            设定内存缓冲区大小 [默认10KB]
        void setRepository(File repository)
            设定临时文件夹大小 [默认System.getProperty("java.io.tmpdir")]
    
  • ServletFileUpload
    static boolean isMultipartContent(HttpServletRequest request)
        判断上传表单是否为 multipart/form-data 类型
    List parseRequest(HttpServletRequest request)
        解析 request 对象,并把表单中的每一个输入项包装成一个FileItem 对象
        并返回一个保存了所有 FileItem 的 List
    setFileSizeMax(long fileSizeMax)
        设置单个上传文件的最大值;超过阈值抛 FileSizeLimitExceededException
    setSizeMax(long sizeMax)
        设置上传文件总量的最大值
    setHeaderEncoding(String encoding)
        设置编码格式,解决上传文件名乱码问题
    setProgressListener(ProgressListener pListener)
        实时监听文件上传状态 (注册监听要放在解析request之前进行!)
    
  • FileItem
    boolean isFormField() 判断FileItem是一个文件上传对象还是普通表单对象
    
    > 如果判断是一个普通表单对象
        String getFieldName() 获得普通表单对象的name属性
        String getString(String encoding) 获得普通表单对象的value属性,可用形参来解决乱码问题
    > 如果判断是一个文件上传对象
        String getName() 获得上传文件的文件名
        InputStream getInputStream() 获得上传文件的输入流
        void delete() 在关闭 FileItem 输入流后,删除临时文件
    

JS 实现多文件上传

每次动态增加一个文件上传输入框,都把它和删除按纽放置在一个单独的 <div> 中,并对删除按纽的 onclick 事件进行响应,使之删除删除按纽所在的 <div>

moreUpload.jsp

<html>
    <head>
    <title>多文件上传</title>
    <script>
        function addOne() {
            var fdiv = document.getElementById("fdiv");
            fdiv.innerHTML += "<div><input type='file' name='file' />"
                + "<input type='button' id='delBtn' onclick='delOne(this)'"
                + "value='删除'/><br></div>";
        }

        function delOne(btn) {
            btn.parentNode.parentNode.removeChild(btn.parentNode);
        }
    </script>
    </head>
    <body>
        <h1>文件上传</h1>
        <input type="button" id="addBtn" onclick="addOne()" value="加一个"/>
        <form action="${pageContext.request.contextPath }/servlet/UploadServlet2"
                method="POST" enctype="multipart/form-data">
            描述信息1: <input type="text" name="desc1" />
            描述信息2: <input type="text" name="desc2" />
            <div id="fdiv"></div>
            <input type="submit" value="提交" />
        </form>
    </body>
</html>

上传文件保存

  1. 文件名
    为防止多用户上传相同文件名的文件,而导致文件覆盖的情况发生,文件上传程序应保证上传文件具有唯一文件名。可以使用一个表示通用唯一标识符 (UUID) 的类。 UUID 表示一个 128 位的值。
    static UUID randomUUID() 生成一个不重复的 128 位的二进制
    public String toString() 128 位二进制 -> 32 位十六进制
    
  2. 文件的存储结构
    为防止单个目录下文件过多,影响文件读写速度,处理上传文件的程序应根据可能的文件上传总量,选择合适的目录结构生成算法,将上传文件分散存储(比如,根据 hash 值来分目录存储)。
  3. 文件在web应用中的存放路径
    为保证服务器安全,上传文件应保存在应用程序的 WEB-INF 目录下,或者不受 web 服务器管理的目录。防止用户上传 JSP恶意入侵或访问其他用户上传的资源

一个较完整的文件上传代码:

public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    response.setContentType("text/html;charset=utf-8");
    try {
        // 检查表单格式是否正确
        if(!ServletFileUpload.isMultipartContent(request))
            throw new RuntimeException("请用正确的表单格式上传");

        DiskFileItemFactory factory = new DiskFileItemFactory();
        factory.setSizeThreshold(100*1024);
        factory.setRepository(new File(getServletContext().getRealPath("WEB-INF/temp")));
        ServletFileUpload fileUpload = new ServletFileUpload(factory);

        // 设置单个文件的最大值
        fileUpload.setFileSizeMax(100*1024);
        // 设置上传文件总量的最大值
        fileUpload.setSizeMax(200*1024);
        // 设置编码集(解决上传文件名乱码问题)
        fileUpload.setHeaderEncoding("utf-8");
        // 设置文件上传监听
        // fileUpload.setProgressListener(new ProgressListener() {...});

        List<FileItem> list = fileUpload.parseRequest(request);
        for(FileItem item : list)
            if(item.isFormField()) {
                String name = item.getFieldName();
                String value = item.getString("utf-8");// 解决请求参数乱码
                System.out.println(name+" = "+value);
            } else {
                // ==== 存储 [上传文件] 前的准备工作 ====
                String fileName = item.getName();
                String uuidName = UUID.randomUUID().toString()+"_"+fileName;
                // 根据 hash 值实现分目录存储
                int hash = uuidName.hashCode();
                String hashStr = Integer.toHexString(hash);
                // 每一位代表一级目录, 一共可以有: 16^8 = 4294967296
                char[] dirArr = hashStr.toCharArray();
                String path = getServletContext().getRealPath("/WEB-INF/upload");
                for(char c : dirArr) path += "/"+c;
                // 系统若找不到指定目录,就应该先创建出这个层级目录
                new File(path).mkdirs();

                // ==== 存储 [上传文件] ====
                InputStream in = item.getInputStream();
                OutputStream out = new FileOutputStream(new File(path, uuidName));
                IOUtils.transfer(in, out);
                IOUtils.close(in, out);
                item.delete();
            }
    } catch (FileSizeLimitExceededException e) {
        response.getWriter().write("单个文件不超过10M, 总大小不超过100M");
    } catch (FileUploadException e) {
        e.printStackTrace();
    }
}

文件上传监视

fileUpload.setProgressListener(new ProgressListener() {
    Long beginTime = System.currentTimeMillis();
    @Override
    public void update(long pBytesRead, long pContentLength, int pItems) {
        BigDecimal br = new BigDecimal(pBytesRead).
                            divide(new BigDecimal(1024), 2,BigDecimal.ROUND_HALF_UP);
        BigDecimal cl = new BigDecimal(pContentLength)
                            .divide(new BigDecimal(1024), 2,BigDecimal.ROUND_HALF_UP);
        System.out.print("当前读取的是第"+pItems+"个field,总大小是"+cl+"KB,正在读取"+br+"KB,");
        // 剩余字节数
        BigDecimal rb = cl.subtract(br);
        System.out.print("剩余"+rb+"KB,");
        // 上传百分比
        // BigDecimal per = br.divide(cl,4,BigDecimal.ROUND_HALF_UP)
                                                .multiply(new BigDecimal(100));
        // 结果成了:85.7600  应该先乘以100
        BigDecimal per = br.multiply(new BigDecimal(100)
                                            .divide(cl,4,BigDecimal.ROUND_HALF_UP));
        System.out.print("已经完成"+per+"%,");
        // 上传用时
        Long nowTime = System.currentTimeMillis();
        Long useTime = (nowTime - beginTime)/1000;
        System.out.print("已经用时"+useTime+"秒,");
        // 上传速度
        BigDecimal speed = new BigDecimal(0);
        if(useTime != 0)
            speed = br.divide(new BigDecimal(useTime),2,BigDecimal.ROUND_HALF_UP);
        System.out.print("上传速度为"+speed+"KB/s,");
        // 大致剩余时间
        BigDecimal rt = new BigDecimal(0);
        if(!speed.equals(new BigDecimal(0)))
            rt = rb.divide(speed,0,BigDecimal.ROUND_HALF_UP);
        System.out.println("剩余时间"+rt+"秒");
    }
});

文件下载

public class DownloadServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String fileName = request.getParameter("file");
        // 通知 Browser 以附件形式打开
        response.setHeader("Content-Disposition"
            , "attachment;filename=" + URLEncoder.encode(fileName,"utf-8"));
        // 通知 Browser 发送的是什么格式的数据
        response.setContentType(getServletContext().getMimeType(fileName)); // MIME类型
        InputStream in = new FileInputStream(getServletContext().getRealPath(fileName));
        OutputStream out = response.getOutputStream();
        IOUtils.transfer(in, out);
        IOUtils.close(in, out);
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}
  • 发送响应头 Content-Disposition
  • 文件名 URL 编码
  • 设置 MIME 类型

exer:网盘

功能分析

  • index.jsp:提供 [上传]、[下载列表]
  • upload.jsp:提供上传表单,允许用户选择文件进行上传
  • UploadServlet:① 保存上传的文件到服务器;② 在 DB 中保存文件相关信息
  • DownloadListServlet:查询数据库,列出所有可供下载的资源信息,存入 request 域后转发到 downloadList.jsp 做显示
  • downloadList.jsp:遍历 request 域中所有资源信息,提供下载链接
  • DownloadServlet:下载指定 id 的资源

代码实现

Table:netdisk

Resource

public class Resource implements Serializable {
    private int id;
    private String uuidName;
    private String realName;
    private String savePath;
    private String uploadTime;
    private String description;
    private String ip;
    ...
}

UploadServlet

public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    if(!ServletFileUpload.isMultipartContent(request))
        throw new RuntimeException("请使用正确的表单格式上传文件");
    
    String upload = getServletContext().getRealPath("WEB-INF/upload");
    String temp = getServletContext().getRealPath("WEB-INF/temp");
    Map<String,String> paramMap = new HashMap<String,String>();
    paramMap.put("ip", request.getRemoteAddr());
    
    // 封装上传文件
    try {
        DiskFileItemFactory factory = new DiskFileItemFactory(1024,new File(temp));
        ServletFileUpload fileUpload = new ServletFileUpload(factory);
        fileUpload.setHeaderEncoding("utf-8");
        fileUpload.setFileSizeMax(100*1024*1024);
        List<FileItem> list = fileUpload.parseRequest(request);
        for(FileItem item : list) {
            if(item.isFormField()) {
                String name = item.getFieldName();
                String value = item.getString("utf-8");
                paramMap.put(name, value);
            } else {
                String realName = item.getName();
                paramMap.put("realName", realName);
                InputStream in = item.getInputStream();
                String uuidName = UUID.randomUUID().toString()+"_"+realName;
                paramMap.put("uuidName", uuidName);
                int hash = uuidName.hashCode();
                char[] dirArr = Integer.toHexString(hash).toCharArray();
                String savePath = "/WEB-INF/upload";
                for(char c : dirArr) {
                    upload += "/"+c;
                    savePath += "/"+c;
                }
                paramMap.put("savePath", savePath);
                new File(upload).mkdirs();
                OutputStream out = new FileOutputStream(new File(upload, uuidName));
                IOUtils.transfer(in, out);
                IOUtils.close(in, out);
                item.delete();
            }
        }
        // 向数据库插入数据
        Resource r = new Resource();
        BeanUtils.populate(r, paramMap);
        String sql = "insert into netdisk values(null, ?, ?, ?, null, ?, ?)";
        QueryRunner runner = new QueryRunner(DaoUtils.getSource());
        runner.update(sql, r.getUuidName(), r.getRealName()
                , r.getSavePath(), r.getDescription(), r.getIp());
        // 重定向回主页
        response.sendRedirect(request.getContextPath());
    } catch (Exception e) {
        e.printStackTrace();
        throw new RuntimeException(e);
    }
}

DownloadListServlet

public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    // 找出数据库中所有可供下载的资源信息
    String sql = "select * from netdisk";
    QueryRunner runner = new QueryRunner(DaoUtils.getSource());
    List<Resource> list = null;
    try {
        list = runner.query(sql, new BeanListHandler<Resource>(Resource.class));
    } catch (SQLException e) {
        e.printStackTrace();
        throw new RuntimeException(e);
    }
    // 存入 request 域中,带到 downloadList.jsp 做展示
    request.setAttribute("downloadList", list);
    request.getRequestDispatcher("/downloadList.jsp").forward(request, response);
}

downloadList.jsp

<body>
    <div align="center">
        <h1>资源下载列表</h1>
        <table border="1">
            <tr>
                <th>文件名称</th>
                <th>上传时间</th>
                <th>上传者IP</th>
                <th>描述信息</th>
                <th>可选操作</th>
            </tr>
            <c:forEach items="${requestScope.downloadList }" var="resource">
                <tr>
                    <td><c:out value="${resource.realName }" /></td>
                    <td><c:out value="${resource.uploadTime }" /></td>
                    <td><c:out value="${resource.ip }" /></td>
                    <td><c:out value="${resource.description }" /></td>
                    <td><a href="${pageContext.request.contextPath }
                            /servlet/DownloadServlet?id=${resource.id }">下载</a></td>
                </tr>
            </c:forEach>
        </table>
    </div>
</body>

DownloadServlet

public void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    String id = request.getParameter("id");
    String sql = "select * from netdisk where id = ?";
    QueryRunner runner = new QueryRunner(DaoUtils.getSource());
    Resource resource = null;

    try {
        resource = runner.query(sql, new BeanHandler<Resource>(Resource.class),id);
    } catch (SQLException e) {
        e.printStackTrace();
        throws new RuntimeException(e);
    }

    if(resource!=null) {
        response.setHeader("content-disposition"
            , "attachment;filename=" + URLEncoder.encode(resource.getRealName(), "utf-8"));
        response.setContentType(getServletContext().getMimeType(resource.getRealName()));
        InputStream in = new FileInputStream(new File(getServletContext()
            .getRealPath(resource.getSavePath()),resource.getUuidName()));
        OutputStream out = response.getOutputStream();
        IOUtils.transfer(in,out);
        IOUtils.close(in,null);
    } else response.getWriter().write("资源不存在");
}
原文地址:https://www.cnblogs.com/liujiaqi1101/p/13388679.html