文件上传下载

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了文件上传下载相关的知识,希望对你有一定的参考价值。

  文件上传下载功能几乎遍布于所有的软件系统中,其核心的原理机制主要是对文件流的读写处理,开发者既可以手动编写处理文件流底层的程序,也可以调用第三方开源组件提高开发效率。开发过程中常引用Apache下的commons-fileuploadcommons-io组件来处理文件上传下载,组件封装了底层技术细节,使文件的上传下载功能实现变得更加简单。

  由于B/S架构系统的流行趋势,开发基于Web的应用逐渐成为了开发者的首选,因此本文主要描述Java-Web文件上传下载的核心原理。

一 文件上传

  文件上传通常需要一个提交表单,该表单作为用户选择文件并上传的入口,后台服务器端会有处理文件上传的action,如下的表单信息表明:当用户选择了文件并点击提交按钮时,将以POST请求的方式提交给UploadHandleServlet处理,服务器端的UploadHandleServlet读取上传文件的信息并写入指定的存储路径。

  <form action="${pageContext.request.contextPath}/servlet/UploadHandleServlet" enctype="multipart/form-data" method="post">
        上传用户:<input type="text" name="username"><br/>
        上传文件1:<input type="file" name="file1"><br/>
        上传文件2:<input type="file" name="file2"><br/>
        <input type="submit" value="提交">
    </form>

      处理文件上传的POST请求时,创建commons-fileupload组件中的DiskFileItemFactoryServletFileUpload类,其中DiskFileItemFactory为处理文件的工厂类,可设置工厂的缓冲区大小及文件上传时生成的临时文件保存目录;ServletFileUpload是文件处理的核心类,可以解析上传文件的信息、限制上传文件的大小和文件上传容量上限并监听文件上传的进度,核心代码如下:

package com.learn.FileUploadDownload.Controller;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadBase;
import org.apache.commons.fileupload.ProgressListener;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
/**
 * 文件上传处理Servlet
 * Created by lfq on 2017/7/7.
 */
public class UploadHandleServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //获取上传文件的保存路径,为保证数据安全性,应将文件存储于外界无法直接访问的WEB-INF目录下
        String savePath = this.getServletContext().getRealPath("/WEB-INF/upload");
        //上传时生成的临时文件保存目录
        String tempPath = this.getServletContext().getRealPath("/WEB-INF/temp");
        File tempfile = new File(tempPath);
        if (!tempfile.isDirectory() && !tempfile.exists()) {
            System.out.println(savePath + "目录不存在,需要创建");
            tempfile.mkdir();
        }
        String message = "";
        //处理文件的工厂
        DiskFileItemFactory factory = new DiskFileItemFactory();
        //设置工厂的缓冲区大小,当上传的文件大小超过缓冲区时,就会生成一个临时文件存放至临时目录当中
        factory.setSizeThreshold(1024 * 100);
        //设置上传时生成的临时文件的保存目录
        factory.setRepository(tempfile);
        //Apache文件上传组件的文件上传解析器
        ServletFileUpload fileUpload = new ServletFileUpload(factory);
        //监听文件上传的进度
        fileUpload.setProgressListener(new ProgressListener() {
            @Override
            public void update(long pByteRead, long pContextLength, int args2) {
                System.out.println("文件大小为:" + pContextLength + ",当前处理:" + pByteRead);
            }
        });

        //设置上传单个文件的大小的最大值,目前是设置为1024*1024字节,也就是1MB
        fileUpload.setFileSizeMax(1024 * 10124);
        //设置上传文件总量的最大值,最大值=同时上传的多个文件的大小的最大值的和,目前设置为10MB
        fileUpload.setSizeMax(1024 * 1024 * 10);
        //设置上传编码格式为UTF-8防止中文乱码
        fileUpload.setHeaderEncoding("UTF-8");
        //判断提交的数据是否为表单的数据
        if (!ServletFileUpload.isMultipartContent(request)) {
            return;
        }
        try {
            //解析请求中上传文件的信息并返回List<FileItem>
            List<FileItem> fileItemList = fileUpload.parseRequest(request);
            //遍历文件信息
            for (FileItem item : fileItemList) {
                //若为普通的表单域,则打印域名和值
                if (item.isFormField()) {
                    String name = item.getFieldName();
                    String value = item.getString("UTF-8");
                    System.out.println(name + "=" + value);
                } else {
                    String fileName = item.getName();
                    System.out.println(fileName);
                    if (fileName == null || fileName.trim().equals("")) {
                        continue;
                    }
                    fileName = fileName.substring(fileName.lastIndexOf("\\") + 1);
                    String saveFileName = makeFileName(fileName);
                    String saveFilePath = makeFileSavePath(fileName, savePath);
                    //读入文件流
                    InputStream in = item.getInputStream();
                    //写文件至目标路径
                    FileOutputStream out = new FileOutputStream(saveFilePath + "\\" + saveFileName);
                    //设置缓冲区
                    byte buffer[] = new byte[1024];
                    int length = 0;
                    while ((length = in.read(buffer)) > 0) {
                        out.write(buffer, 0, length);
                    }
                    //关闭输入流
                    in.close();
                    //关闭输出流
                    out.close();
                    //删除临时文件
                    item.delete();
                    message = "文件上传成功!";
                }
            }
        }catch (FileUploadBase.FileSizeLimitExceededException e){
            e.printStackTrace();
            request.setAttribute("message", "单个文件超过最大值");
            request.getRequestDispatcher("/message.jsp").forward(request, response);
            return;
        }catch (FileUploadBase.SizeLimitExceededException e){
            e.printStackTrace();
            request.setAttribute("message", "上传文件总量超过最大值");
            request.getRequestDispatcher("/message.jsp").forward(request, response);
            return;
        } catch (Exception e) {
            message="文件上传失败";
            e.printStackTrace();
        }
        request.setAttribute("message", message);
        request.getRequestDispatcher("/message.jsp").forward(request, response);
    }
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
    //获取文件保存的名称
    private String makeFileName(String originalFile) {
        //为防止文件覆盖,产生唯一的文件名
        return UUID.randomUUID().toString() + "_" + originalFile;
    }
    //获取文件保存的路径
    private String makeFileSavePath(String saveFileName, String saveFilePath) {
        int hashCode = saveFileName.hashCode();
        int dir1 = hashCode & 0xf;
        int dir2 = (hashCode & 0xf0) >> 4;
        //构造新目录
        String dir = saveFilePath + "\\" + dir1 + "\\" + dir2;
        File file = new File(dir);
        if (!file.exists()) {
            file.mkdirs();
        }
        return dir;
    }
}

  需要注意的是,为防止文件的覆盖,常需要产生唯一的文件名并将同一个目录下的文件打散,通过以上的文件流读写操作便可以将上传的文件存储于目标路径中,以上的注释解释了大部分技术细节,再此不再赘述。

二  文件下载

   假设以上的操作未出现异常,则文件被成功的上传至/WEB-INF/upload路径下,现在需要提供文件下载链接列表,完成文件的下载,该操作需要首先遍历服务器存储上传文件路径下的所有文件,然后渲染所有文件列表信息至页面供用户下载,所以服务器端需要读取存储文件列表的Servlet,核心代码如下:

package com.learn.FileUploadDownload.Controller;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
 * 列出下载目录下的所有文件
 * Created by lfq on 2017/7/8.
 */
public class ListFileServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //获取上传文件保存的目录
        String fileSavePath = this.getServletContext().getRealPath("/WEB-INF/upload");
        Map<String, String> fileNameMap = new HashMap<>();
        listFile(new File(fileSavePath), fileNameMap);
        request.setAttribute("fileNameMap", fileNameMap);
        request.getRequestDispatcher("/listFile.jsp").forward(request, response);
    }
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
    //递归遍历指定目录下的文件
    public void listFile(File file, Map<String, String> map) {
        if (!file.isFile()) {
            File files[] = file.listFiles();
            for (File f : files) {
                listFile(f, map);
            }
        } else {
            String realName = file.getName().substring(file.getName().indexOf("_") + 1);
            map.put(file.getName(), realName);
        }
    }
}

  文件下载列表页面核心代码如下:

  <c:forEach var="map" items="${fileNameMap}">
      <li>
          <c:url value="/servlet/DownLoadServlet" var="downUrl">
              <c:param name="fileName" value="${map.key}"> </c:param>
          </c:url>
      </li>
      ${map.value} <a href="${downUrl}">下载</a>
  </c:forEach>
 以上ListFileServlet中会递归读取服务器端存储路径下的文件信息,并以Key(fileName),Value(realName)键值对的形式存于Map,真实开发并不会这么处理,常规会将文件的路径存储于数据库中,下载时仅需从数据库中读取对应的文件存储路径即可。
提供了文件下载链接后,则必须有处理下载请求的Servlet,本案例中DownloadSevlet用于处理文件下载请求,核心代码如下:
package com.learn.FileUploadDownload.Controller;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;

/**
 * 文件下载处理Servlet
 * Created by lfq on 2017/7/8.
 */
public class DownLoadServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //获取下载文件名并设置编码格式
        String fileName = new String(request.getParameter("fileName").getBytes("iso8859-1"), "UTF-8");
        //获取文件存储路径
        String fileSavePath = this.getServletContext().getRealPath("/WEB-INF/upload");
        //根据文件名搜索文件真实存储路径
        String path = findFileSavePathByFileName(fileName, fileSavePath);
        File file = new File(path + "\\" + fileName);
        //如果文件不存在
        if (!file.exists()) {
            request.setAttribute("message", "您要下载的资源已被删除!!");
            request.getRequestDispatcher("/message.jsp").forward(request, response);
            return;
        }
        //获取真实文件名
        String realFileName = fileName.substring(fileName.indexOf("_") + 1);
        //设置响应头及文件编码格式
        response.setHeader("contend-diposition", "attachment;filename=" + URLEncoder.encode(realFileName, "UTF-8"));
        FileInputStream in = new FileInputStream(path + "\\" + fileName);
        OutputStream out = response.getOutputStream();
        byte buffer[] = new byte[1024];
        int len = 0;
        while ((len = in.read(buffer)) > 0) {
            out.write(buffer, 0, len);
        }
        //关闭输入流
        in.close();
        //关闭输出流
        out.close();
    }
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
    public String findFileSavePathByFileName(String fileName, String saveRootPath) {
        int hashCode = fileName.hashCode();
        int dir1 = hashCode & 0xf;
        int dir2 = (hashCode & 0xf0) >> 4;
        String fileDir = saveRootPath + "\\" + dir1 + "\\" + dir2;
        File file = new File(fileDir);
        if (!file.exists()) {
            file.mkdirs();
        }
        return fileDir;
    }
}

   以上即为文件下载的后台处理过程,需要注意的技术细节便是需要正确设置文件编码格式,并根据上传文件名获取真实的存储路径,底层通过读写文件流完成下载过程。

三  总结

  文件上传下载属于比较基础却很常用的技术,其核心就是文件的读写,但实现过程中需要注意维护文件命名的唯一性、文件的安全性(一般存储于外界无法直接访问的/WEB-INF目录下),正确设置文件格式防止中文乱码(一般统一设置编码格式为UTF-8)。实际项目中文件路径一般存储于数据库中,但为了数据安全,有时候重要文件路径需要进行加密处理,而存储的路径可以直接从数据库中读取。

 

 

以上是关于文件上传下载的主要内容,如果未能解决你的问题,请参考以下文章

Alamofire 文件上传出现错误“JSON 文本未以数组或对象开头,并且允许未设置片段的选项”

使用 libtorrent 下载特定片段

根据图片的url地址下载图片到本地保存代码片段

Xamarin Android 片段库

将存储在内存中的文件上传到s3

片段中的Firebase数据不是持久的,会重新下载