JavaWeb 文件上传下载

Posted Mr.Aaron

tags:

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

1. 文件上传下载概述

1.1. 什么是文件上传下载

所谓文件上传下载就是将本地文件上传到服务器端,从服务器端下载文件到本地的过程。例如目前网站需要上传头像、上传下载图片或网盘等功能都是利用文件上传下载功能实现的。

文件上传下载实际上是两步操作,第一是文件上传,就是将本地文件上传到服务器端,实现文件多用户之间的共享,第二是文件下载,就是将服务器端的文件下载到本地磁盘。

1.2. 文件上传下载实现原理

首先,需要知道文件是如何实现上传及下载的。文件上传及下载实现原理如下:

文件上传实现流程如下:

  • 客户端浏览器通过文件浏览框,选择需要上传的文件内容(其中包括文件路径及文件内容)。
  • 客户端浏览器通过点击上传按钮,将本地文件上传到服务器端。
  • 服务器端通过程序接收本地文件内容,并将其保存在服务器端磁盘中。

 

文件下载实现流程如下:

  • 客户端浏览器通过点击下载按钮,将服务器端保存的文件下载到本地磁盘。
  • 服务器端通过程序将服务器端文件响应给客户端。

2. 文件上传实现

2.1. 文件上传客户端页面实现

Web应用程序中实现文件上传功能,只需要在客户端页面中添加需要上传输入项,在服务器端Servlet中读取上传文件的数据,并保存在服务器端硬盘中即可。

客户端浏览器页面实现文件上传功能,具体代码如下:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>My JSP \'upload.jsp\' starting page</title>
  </head>
  <body>
    <form action="/upload/upload" method="post" enctype="multipart/form-data">
        文件描述:<input type="text" name="filetext"><br>
        <input type="file" name="upload"><br>
        <input type="submit" value="上传">
    </form>
  </body>
</html>

需要注意的是:

  • <input type=”file”>标签必须指定name属性值,否则需要上传的文件数据是不会上传至服务器端。
  • 完成文件上传功能的表单的请求类型必须是POST方式。
  • 完成文件上传功能的表单的enctype属性值设置为“multipart/form-data”,该值的作用是将需要上传的文件数据添加到Http请求体中,并使用MIME协议对上传的文件进行描述。

2.2. commons-fileupload工具

完成客户端的文件上传功能之后,主要是在服务器端完成接收上传文件的数据内容。为了方便实现文件上传逻辑,可以使用第三方提供的文件上传包,具体如下:

  • jsp-smartupload.jar:使用JSP模型一时使用的,目前基本不再使用。
  • commons-fileupload.jar:由Apache基金会提供的,用来实现Java环境下的文件上传功能。
  • Servlet 3.0规范中提供对文件上传的支持。

commons-fileupload组件的官网地址:http://commons.apache.org/proper/commons-fileupload/。需要注意的是:在使用commons-fileupload组件时,需要依赖于commons-io包。commons-fileupload组件工作流程如下:

 

如何使用commons-fileupload组件实现文件上传功能,可以参考其官网的User Guide内容。

 

  • 创建DiskFileItemFactory文件项工厂对象。
  • 通过工厂对象获取文件上传请求核心解析类ServletFileUpload
  • 使用ServletFileUpload对应Request对象进行解析。
  • 遍历每个fileItem,判断是否为上传项。
  • IOUtils.copy(inputStream,OutputStream)(将上传的数据拷贝到服务器的硬盘上的简单方法)

具体实现代码如下:

public class UploadServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request, response);
    }
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 创建DiskFileItemFactory文件项工厂对象
        DiskFileItemFactory factory = new DiskFileItemFactory();
        // 通过工厂对象获取文件上传请求核心解析类ServletFileUpload
        ServletFileUpload upload = new ServletFileUpload(factory);
        try {
            // 使用ServletFileUpload对应Request对象进行解析
            List<FileItem> items = upload.parseRequest(request);
            // 遍历每个fileItem
            for (FileItem fileItem : items) {
                // 判断fileItem是否是上传项
                if (fileItem.isFormField()) {
                    // 返回true:表示不是上传项
                    String fieldName = fileItem.getFieldName();
                    String str = fileItem.getString("utf-8");
                    System.out.println(fieldName+" : "+str);
                }else{
                    // 返回false:表示是上传项
                    String name = fileItem.getName();
                    InputStream in = fileItem.getInputStream();
                    
                    String uploadPath = getServletContext().getRealPath("/upload");
                    OutputStream out = new FileOutputStream(new File(uploadPath, name));
                    int b;
                    while ((b = in.read()) != -1) {
                        out.write(b);
                    }
                    out.close();
                    in.close();
                }
            }
        } catch (FileUploadException e) {
            e.printStackTrace();
        }
    }
}

2.3. 动态多文件上传表单

上述案例实现的是单文件上传,如果想实现多文件上传功能的话,服务器端的逻辑是一样的,也就是说,只需要在客户端页面实现多文件上传控件即可。动态实现多文件上传表单代码如下:

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>My JSP \'upload.jsp\' starting page</title>
  </head>
  <script type="text/javascript">
        function add(){
            // 在div中添加上传输入项
            document.getElementById("uploaddiv").innerHTML += "<div><input type=\'file\' name=\'upload\' /><input type=\'button\' value=\'删除\' onclick=\'del(this);\' /></div>";
        }
        function del(obj){
            // 传入obj 就是你点击 按钮对象
            var div = obj.parentNode;
            div.parentNode.removeChild(div);
        }
  </script>
  <body>
    <form action="/upload/upload" method="post" enctype="multipart/form-data">
        <input type="button" value="上传文件" onclick="add();"/> 
        <div id="uploaddiv"></div>
        <input type="submit" value="开始上传" />
    </form>
  </body>
</html>

2.4. 上传文件至WEB-INF目录

到目前步骤,已经可以成功从客户端浏览器向服务器端上传文件。但是上传的路径存在一些问题,上述上传路径是自定义的文件夹,而上传至这种自定义的文件夹后,通过浏览器可以正常访问,这是非常危险的。

例如一个用户上传一个JSP页面,然后通过浏览器访问该JSP页面,而该JSP页面中可以包含一些恶意代码。这时如果允许用户运行该JSP页面的话,可能会对服务器端造成很大影响。

所以,通常情况下,会将上传目录创建在Web工程的WEB-INF目录下。因为该目录下的内容,是无法通过浏览器访问到的。

获取文件上传路径的代码,应该修改为如下内容:

String uploadPath = getServletContext().getRealPath("/WEB-INF/upload");

2.5. 上传文件名称的处理

对于上传文件的名称,可能是文件的完整路径,例如:C:\\upload\\aaa.jpg。在服务器端只需要保存其上传文件的名称即可,所以需要对上传文件的名称进行进一步地处理,具体处理代码如下:

String name = fileItem.getName();
// 文件名可能是绝对路径,进行切割
int index = name.lastIndexOf("\\\\");
// 获取"\\"之后的内容(真正的文件名)
if(index >= 0){
    name = name.substring(index + 1);
}

目前绝大多数的浏览器都不存在这个问题,仅仅只有一些比较老的浏览器版本存在,例如IE6.0版本。为保证兼容更多浏览器产品,这个问题依旧需要解决。

2.6. 上传文件中文乱码问题

如果现在上传文件的名称为中文的话,会引起中文乱码问题。commons-fileupload组件为解决中文乱码问题提供了两种解决方案,如下:

  • 利用Request对象的setCharacterEncoding(“UTF-8”)方法,该方法尽量编写在ServletdoGet()doPost()方法的顶端。
  • 利用ServletFileUpload类提供的setHeaderEncdoing(“UTF-8”)方法来解决。

一般情况下,不关心上传文件的内容,因为上传文件会保存在服务器端的磁盘中。但是,如果需要在控制台打印上传文件的内容,而刚好该上传文件的内容中包含中文的话,可以使用FileItemgetString(“UTF-8”)来处理编码。

2.7. 上传文件同名问题的处理

如果同一个用户上传多个同名的文件,默认情况下会出现被覆盖的情况,即前一次上传的文件会被后一次上传的文件覆盖。而这种情况是不希望看到的,解决这个问题的方法就是可以为每一个上传的文件名称增加UUID,因为UUID类的randomUUID()可以生成一个唯一标识符。具体做法如下:

// 为上传文件名称增加UUID的前缀.
name = UUID.randomUUID().toString() + "_" + name;

2.8. 一个目录不能存放过多文件

如果上传文件过多时,会导致上传目录中的文件过多,内容过大。这时可以考虑将不同文件存储在不同的目录中,而生成不同目录的规则参考如下:

  • 按照上传时间进行目录分离,例如2014-12-12为一个目录。
  • 按照上传用户进行目录分离,为每一个用户创建一个上传目录。
  • 按照固定数量进行目录分离,设定当一个上传目录包含文件超过指定数量,创建新的上传目录。
  • 按照唯一文件名的hashcode 进行目录分离。

这里以hashcode进行目录分离方式为例演示,具体思路如下:

  • 使用UUID类的randomUUID()方法生成唯一标识符。
name = UUID.randomUUID().toString() + "_" + name;

 

其值为2dab369c-1e4f-4e58-8b61-13c7aef855b0。

  • 通过hashCode()方法获取其唯一标识符的hashcode值。
int hashcode = uuidFileName.hashCode();

其值为:166846237,转换成二进制后的值为:1001111100011101111100011101。

  • 按照每4位值“与”二进制1111F)后,生成一级目录。

 

// 获得一级目录
int d1 = hashcode & 0xf;
  • 以此类推,每4位值“与”二进制1111后,生成一个级别的目录。
// 获得二级目录
int d2 = (hashcode >>> 4) & 0xf;

根据上述步骤,可以编写一个按照hashcode方式生成目录的工具类,具体代码如下:

public class UploadUtils {
    // 根据唯一文件名生成 hashcode目录分离算法
    public static String generateRandomDir(String uuidFileName) {
        // 获得唯一文件名的hashcode
        int hashcode = uuidFileName.hashCode();
        // 获得一级目录
        int d1 = hashcode & 0xf;
        // 获得二级目录
        int d2 = (hashcode >>> 4) & 0xf;
        return "/" + d2 + "/" + d1; 
    }
}

上述程序代码可以改写如下:

String randomDir = UploadUtils.generateRandomDir(name);

String uploadPath = getServletContext().getRealPath("/WEB-INF/upload" + randomDir);
// 生成随机目录
new File(uploadPath).mkdir();

2.9上传单个文件的大小限制

上传文件时,用户可能上传非常大的文件,可能导致上传时占用过多资源。所以,对于用户上传的单个文件大小,应做出相应限制。利用ServletFileUpload的setFileSizeMax(long)方法进行设置,其中参数表示设置的大小,单位为字节数,例如servletFileUpload.setFileSizeMax(1024*10)表示上限为10KB。

一旦上传的单个文件大小超过限制大小时,会抛出FileUploadBase.FileSizeLimitExceededException异常,可以捕获该异常后向页面输出相应的错误信息。具体实现代码如下:

public class UploadServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request, response);
    }
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        DiskFileItemFactory factory = new DiskFileItemFactory();
        ServletFileUpload upload = new ServletFileUpload(factory);
        // 设置单个上传文件的大小
        upload.setFileSizeMax(1024 * 10);
        try {
            List<FileItem> items = upload.parseRequest(request);
            for (FileItem fileItem : items) {
                if (fileItem.isFormField()) {
                    ……
                }else{
                    String name = fileItem.getName();
                    ……
                    in.close();
                }
            }
        } catch (FileUploadException e) {
            if (e instanceof FileUploadBase.FileSizeLimitExceededException) {
                // 在request中保存错误信息
                request.setAttribute("msg", "上传失败!上传的文件超出了10KB!");
                // 转发到index.jsp页面中!在index.jsp页面中需要使用${msg}来显示错误信息
    request.getRequestDispatcher("/index.jsp").forward(request, response);
            }
            e.printStackTrace();
        }
    }
}

2.10. 上传文件的总大小的限制

在实现多文件上传时,还需要设置上传文件的总大小。利用ServletFileUploadsetSizeMax(long)方法进行设置,其中参数表示设置的大小,单位为字节数,例如servletFileUpload. setSizeMax(1024*10)表示上限为10KB

一旦上传的文件大小超过限制大小时,会抛出FileUploadBase.SizeLimitExceededException异常,可以捕获该异常后向页面输出相应的错误信息。具体实现代码如下:

public class UploadServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request, response);
    }
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        DiskFileItemFactory factory = new DiskFileItemFactory();
        ServletFileUpload upload = new ServletFileUpload(factory);
        // 设置单个上传文件的大小
        upload.setSizeMax(1024 * 10);
        try {
            List<FileItem> items = upload.parseRequest(request);
            for (FileItem fileItem : items) {
                if (fileItem.isFormField()) {
                    ……
                }else{
                    String name = fileItem.getName();
                    ……
                    in.close();
                }
            }
        } catch (FileUploadException e) {
            if (e instanceof FileUploadBase.SizeLimitExceededException) {
                // 在request中保存错误信息
                request.setAttribute("msg", "上传失败!上传的文件超出了10KB!");
                // 转发到index.jsp页面中!在index.jsp页面中需要使用${msg}来显示错误信息
    request.getRequestDispatcher("/index.jsp").forward(request, response);
            }
            e.printStackTrace();
        }
    }
}

2.11. 文件缓存大小与临时目录

一般情况下,上传文件默认都是先存储在内存中,然后在拷贝到服务器端的磁盘中。但是这样会有一些问题出现,例如单个文件过大时,占用服务器端资源会过多,导致服务器性能变差。这时可以通过手动设置文件缓存大小和上传文件的临时目录来解决。如果不设置上传的文件缓存大小,默认值为10KB,其表示如果上传文件小于10KB的话,会先存储在服务器端的内存中,如果上传文件大小大于10KB的话,会先存储在服务器端默认指定的临时目录中,而上传文件的默认临时目录为System.getProperty("java.io.tmpdir")。

手动修改上传文件缓存大小及临时目录的方式如下:

  • 手动修改上传文件缓存大小:DiskFileItemFactory.setSizeThreshold(缓存字节数);
  • 手动修改上传文件临时目录:

DiskFileItemFactory.setRepository(new File(getServletContext().getRealPath(临时目录相对路径)));

具体实现代码如下:

public class UploadServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request, response);
    }
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        DiskFileItemFactory factory = new DiskFileItemFactory();
        // 内存缓冲区
        factory.setSizeThreshold(3 * 1024 * 1024); // 3M
        // 临时文件位置
        File repository = new File(getServletContext().getRealPath("/tmp"));
        factory.setRepository(repository);
        ServletFileUpload upload = new ServletFileUpload(factory);
        try {
            List<FileItem> items = upload.parseRequest(request);
            for (FileItem fileItem : items) {
                if (fileItem.isFormField()) {
                    ……
                }else{
                    String name = fileItem.getName();
                    ……
                    in.close();
                }
            }
        } catch (FileUploadException e) {
            e.printStackTrace();
        }
    }
}

想要删除临时目录下的临时文件的话,只需要调用FileItemdelete()方法即可。

2.12. 文件上传进度监听器

目前大部分具有文件上传功能的,在文件上传过程中,可以实时看到上传进度。可以使用ServletFileUpload提供的setProgressListener()方法实现,在客户端配置Ajax技术即可实现。

其中除使用setProgressListener()方法实现外,还需要计算如下几个结果:

  • 已用时间:当前时间 – 开始时间
  • 速度:已经上传大小 / 已用时间
  • 剩余大小:总大小 – 已经上传大小
  • 剩余时间:剩余大小 / 速度

根据上述内容,具体查看上传进度功能如下:

// 获取上传文件的开始时间
final long start = System.currentTimeMillis();
// 为文件上传对象,帮助查看进度监听器
upload.setProgressListener(new ProgressListener() {
    /*
     * update(long pBytesRead, long pContentLength, int pItems)
     *  * 该方法用于获取文件上传过程相关数据内容.
     *  * 参数pBytesRead:到目前为止已经读取字节总数(已上传大小)
     *  * 参数pContentLength:正在上传字节总数 (文件大小)
     *  * 参数pItems:当前上传文件,是表单的第几个元素
     */
    public void update(long pBytesRead, long pContentLength, int pItems) {
        if(pBytesRead == 0){
            // 刚开始上传,还没有数据
            return;
        }
        // 计算已经使用时间
        long hasUseTime = System.currentTimeMillis() - start;
        // 计算上传速度
        double speed = ((double)pBytesRead) / hasUseTime;
        // 计算剩余大小
        long restBytes = pContentLength - pBytesRead;
        // 计算剩余时间
        long restTime = (long) (restBytes / speed);
        
        System.out.println("已用时间:" + hasUseTime + ", 传输速度:" + speed + ", 剩余时间:" + restTime);
        try {
            Thread.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});

3. 文件下载实现

3.1. 实现文件下载

实现文件上传功能后,需要做的就是文件下载功能。文件下载不需要第三方组件支持,自定义完成即可,具体操作步骤如下:

  • 创建一个JSP页面用于显示下载文件列表。
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title>My JSP \'download.jsp\' starting page</title>
  </head>
  <body>
    <a href="/upload/download?filename=1.jpg">1.jpg</a>
    <a href="/upload/download?filename=2.rar">2.rar</a>
    <a href="/upload/download?filename=3.txt">3.txt</a>
  </body>
</html>
  • 创建一个Servlet用于文件下载功能。
public class DownloadServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 获取下载文件名
        String filename = request.getParameter("filename");
        // 判断下载文件是否存在
        String path = getServletContext().getRealPath("/tmp");
        File file = new File(path, filename);
        if (file.isFile() && file.exists()) {
            // 说明下载文件存在
            // 为下载设置ContentType和Content-Disposition
            response.setContentType(getServletContext().getMimeType(filename));
            response.setHeader("Content-Disposition", "attachment;filename="+filename);
            // 读取下载文件的内容,写到客户端
            InputStream in = new BufferedInputStream(new FileInputStream(file));
            IOUtils.copy(in, response.getOutputStream());
            in.close();
        }
    }
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doGet(request, response);
    }
}
  • 配置Web工程的web.xml文件。
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" 
    xmlns="http://java.sun.com/xml/ns/javaee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
    http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
  <servlet>
    <servlet-name>DownloadServlet</servlet-name>
    <servlet-class>app.java.servlet.DownloadServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>DownloadServlet</servlet-name>
    <url-pattern>/download</url-pattern>
  </servlet-mapping>    
</web-app>

3.2. 中文乱码解决

下载文件功能实现后,还需要解决中文乱码问题。由于下载页面中的文件名是使用GET方式提交请求的,所以可以使用如下代码解决:

filename = new String(filename.getBytes("ISO-8859-1"),"UTF-8");

而上述代码只能解决服务器端接收客户端请求时参数的中文乱码问题,但是点击文件下载时的文件名称依旧是乱码的。这个问题是由于浏览器本身的问题,不同浏览器的解决方式不同:

  • IE浏览器:使用URL编码。
filename = URLEncoder.encode(filename, "utf-8");
filename = filename.replace("+", " ");
  • 其他浏览器,使用BASE64编码。
BASE64Encoder base64Encoder = new BASE64Encoder();
filename = "=?utf-8?B?" + base64Encoder.encode(filename.getBytes("utf-8")) + "?=";

可以通过Http请求协议的请求头中的“User-Agent”内容,判断客户端当前使用的浏览器是哪个产品。