文件上传

文件上传概述

  • 实现web开发中的文件上传功能,需完成如下二步操作:
    (1)在web页面中添加上传输入项;
    (2)在servlet中读取上传文件的数据,并保存到本地硬盘中。
  • 如何在web页面中添加上传输入项?<input type=”file”>标签用于在web页面中添加文件上传输入项,设置文件上传输入项时须注意:
    (1)必须要设置input输入项的name属性,否则浏览器将不会发送上传文件的数据;
    (2)必须把form的enctype属值设为multipart/form-data.设置该值后,浏览器在上传文件时,将把文件数据附带在http请求消息体中,并使用MIME协议对上传的文件进行描述,以方便接收方对上传数据进行解析和处理。
  • 如何在Servlet中读取文件上传数据,并保存到本地硬盘中?
    (1)Request对象提供了一个getInputStream方法,通过这个方法可以读取到客户端提交过来的数据。但由于用户可能会同时上传多个文件,在servlet端编程直接读取上传数据,并分别解析出相应的文件数据是一项非常麻烦的工作;
    (2)为方便用户处理文件上传数据,Apache 开源组织提供了一个用来处理表单文件上传的一个开源组件( Commons-fileupload ),该组件性能优异,并且其API使用极其简单,可以让开发人员轻松实现web文件上传功能,因此在web开发中实现文件上传功能,通常使用Commons-fileupload组件实现。
  • 使用Commons-fileupload组件实现文件上传,需要导入该组件相应的支撑jar包:Commons-fileupload和commons-io。commons-io 不属于文件上传组件的开发jar文件,但Commons-fileupload 组件从1.1 版本开始,它工作时需要commons-io包的支持。

文件上传原理

      对请求正文是multipart/form-data类型的数据进行解析。

fileupload工作流程

java上传文件和下载 javaweb文件上传与下载_java

fileupload核心API

DiskFileItemFactory工厂类

      DiskFileItemFactory 是创建 FileItem 对象的工厂,这个工厂类常用方法:

  • public DiskFileItemFactory(int sizeThreshold, java.io.File repository):构造方法;
  • public void setSizeThreshold(int sizeThreshold) :设置内存缓冲区的大小,默认值为10K。当上传文件大于缓冲区大小时, fileupload组件将使用临时文件缓存上传文件。
  • public void setRepository(java.io.File repository) :指定临时文件目录,默认值为当前用户的系统临时文件目录,可通过System.getProperty(“java.io.tmpdir”)打印查看;

ServletFileUpload解析器

      ServletFileUpload 负责处理上传的文件数据,并将表单中每个输入项封装成一个 FileItem 对象中。常用方法有:

  • boolean isMultipartContent(HttpServletRequest request) :判断上传表单是否为multipart/form-data类型;
  • List parseRequest(HttpServletRequest request):解析request对象,并把表单中的每一个输入项包装成一个fileItem 对象,并返回一个保存了所有FileItem的list集合;
  • setFileSizeMax(long fileSizeMax) :设置上传文件的最大值;
  • setSizeMax(long sizeMax):设置上传文件总量的最大值;
  • setHeaderEncoding(java.lang.String encoding) :设置编码格式;

FileItem表单字段域对象

      FileItem类负责处理表单提交的字段,常用方法有:

1、处理普通字段方法

  • boolean isFormField() //判断是否为普通字段;
  • String getFieldName() //返回表单字段(普通字段)名称;
  • String getString() //返回普通字段的值;
  • String getString(String encoding) //返回普通字段的值,并设置请求参数编码,常用于get提交

2、处理上传字段方法

  • String getContentType() //返回表单请求类型,即request请求头中的Content-type的内容,可用来限制文件上传的类型;
  • String getName() //返回文件名(全路径名:例如C:\a.txt);
  • InputStream getInputStream() //返回表单输入流对象;
  • void write(File file) //将上传文件写入到磁盘;
  • void delete() //删除该上传字段存储在磁盘上的临时文件;

文件上传案例

实现步骤

  1. 创建DiskFileItemFactory对象,设置缓冲区大小和临时文件目录;
  2. 使用DiskFileItemFactory 对象创建ServletFileUpload对象,并设置上传文件的大小限制;
  3. 调用ServletFileUpload.parseRequest方法解析request对象,得到一个保存了所有上传内容的List对象;
  4. 对list进行迭代,每迭代一个FileItem对象,调用其isFormField方法判断是否是上传文件;如果为普通表单字段,则调用getFieldName、getString方法得到字段名和字段值;如果为上传文件,则调用getInputStream方法得到数据输入流,从而读取上传数据。

上传文件的处理细节

1、中文文件乱码问题

      文件名中文乱码问题,可调用ServletUpLoader的setHeaderEncoding方法,或者设置request的setCharacterEncoding属性

2、临时文件的删除问题

  • 由于文件大小超出DiskFileItemFactory.setSizeThreshold方法设置的内存缓冲区的大小时,Commons-fileupload组件将使用临时文件保存上传数据,因此在程序结束时,务必调用FileItem.delete方法删除临时文件;
  • Delete方法的调用必须位于流关闭之后,否则会出现文件占用,而导致删除失败的情况;

3、文件存放位置

  • 为保证服务器安全,上传文件应保存在应用程序的WEB-INF目录下,或者不受WEB服务器管理的目录;
  • 为防止多用户上传相同文件名的文件,而导致文件覆盖的情况发生,文件上传程序应保证上传文件具有唯一文件名;
  • 为防止单个目录下文件过多,影响文件读写速度,处理上传文件的程序应根据可能的文件上传总量,选择合适的目录结构生成算法,将上传文件分散存储;

4、显示上传进度

(1)ProgressListener显示上传进度

ProgressListener progressListener = new ProgressListener() {
    public void update(long pBytesRead, long pContentLength, int pItems) {
        System.out.println("到现在为止,  " + pBytesRead + " 字节已上传,总大小为 "
              + pContentLength);
    }
};
upload.setProgressListener(progressListener);

(2)以KB为单位显示上传进度

long temp = -1;   //temp注意设置为类变量
long ctemp = pBytesRead /1024; 
if (mBytes == ctemp)  
    return; 
temp = mBytes;

编码实现

      使用工具类:GUIDUtils.java、DirectoryUtils.java

package com.study.java.utils;

import java.math.BigInteger;
import java.util.Random;

/**
* @Name: GUIDUtils
* @Description: 创建GUID唯一字符串工具类,参考struts2源码
* @Author: XXX
* @CreateDate: XXX
* @Version: V1.0
 */
public class GUIDUtils {

    public static String generateGUID() {
        return new BigInteger(165, new Random()).toString().toUpperCase() ;
    }
}
package com.study.java.utils;

import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
* @Name: DirectoryUtils
* @Description: 目录创建工具类
* @Author: XXX
* @CreateDate: XXX
* @Version: V1.0
 */
public class DirectoryUtils {

    /**
    * @Name: getChildDirectoryByDate
    * @Description: 根据当前日期创建子目录
    * @Author: XXX
    * @Version: V1.0
    * @CreateDate: XXX
    * @Parameters: @param storeDirectory 文件存储主目录
    * @Return: String
     */
    public static String getChildDirectoryByDate(String storeDirectory) {
        Date now = new Date() ;
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd") ;
        String childDirectory = sdf.format(now) ;
        File file = new File(storeDirectory, childDirectory) ;
        if(!file.exists()) {
            file.mkdirs() ;
        }
        return childDirectory ;
    }

    /**
    * @Name: getChildDirectoryByHashCode
    * @Description: 根据文件名称字符串的hash码创建二级子目录
    * @Author: XXX
    * @Version: V1.0
    * @CreateDate: XXX
    * @Parameters: @param storeDirectory 文件存储主目录
    * @Parameters: @param filename 文件名称
    * @Return: String
     */
    public static String getChildDirectoryByHashCode(String storeDirectory, String filename) {
        int hashCode = filename.hashCode() ;
        int dir1 = hashCode & 0xf ;
        int dir2 = (hashCode & 0xf0) >> 4 ;
        String childDirectory = dir1 + "/" + dir2 ;
        File file = new File(storeDirectory, childDirectory) ;
        if(!file.exists()) {
            file.mkdirs() ;
        }
        return childDirectory ;
    }
}

      实现文件上传代码:UploadServlet.java

package com.study.java.servlet;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadBase;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import com.study.java.utils.DirectoryUtils;
import com.study.java.utils.GUIDUtils;

/**
* @Name: UploadServlet
* @Description: 文件上传
* @Author: XXX
* @CreateDate: XXX
* @Version: V1.0
 */
public class UploadServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8") ;
        PrintWriter out = response.getWriter() ;
        //设置请求参数编码,适用于post提交,用于处理上传文件中文编码问题
        request.setCharacterEncoding("UTF-8") ;
        //1、创建FileItem工厂对象
        DiskFileItemFactory factory = new DiskFileItemFactory() ;
        //设置缓冲区大小1M,默认10kb
        factory.setSizeThreshold(1*1024*1024) ;
        //设置临时文件存放目录,默认为当前用户系统临时文件存放目录
        String template = getServletContext().getRealPath("/WEB-INF/template") ;
        File templatePath = new File(template) ;
        if(!templatePath.exists()) {
            templatePath.mkdirs() ;
        }
        factory.setRepository(templatePath) ;
        //2、创建上传文件核心解析器对象
        ServletFileUpload sfu = new ServletFileUpload(factory) ;
        //判断表单提交类型是否为multipart/form-data
        boolean isMultipartContent = sfu.isMultipartContent(request) ;
        if(!isMultipartContent) {
            out.write("表单提交类型不正确!!!") ;
        }
        //限制单文件上传大小
        sfu.setFileSizeMax(3*1024*1024) ;
        //限制一次上传总文件大小
        sfu.setSizeMax(10*1024*1024) ;
        //3、解析表单提交参数
        try {
            List<FileItem> items = sfu.parseRequest(request) ;
            //获取表单提交参数信息对象
            for (FileItem item : items) {
                if(item.isFormField()) {
                    //普通字段
                    processFormField(item) ;
                } else {
                    //上传字段
                    processUploadField(item) ;
                }
            }
            out.write("上传成功!!!") ;
            response.setHeader("Refresh", "2;URL=" + request.getContextPath()) ;
        } catch (FileUploadBase.FileSizeLimitExceededException e) {
            out.write("单文件上传大小超出限制3M.") ;
        } catch (FileUploadBase.SizeLimitExceededException e) {
            out.write("总文件上传大小超出限制10M.") ;
        } catch (FileUploadException e) {
            out.write("表单提交参数解析失败!!!") ;
        }
    }

    /**
    * @Name: processUploadField
    * @Description: 上传表单提交的文件
    * @Author: XXX
    * @Version: V1.0
    * @CreateDate: XXX
    * @Parameters: @param item
    * @Return: void
     */
    private void processUploadField(FileItem item) {
        //获取文件上传主目录
        String storeDirectory = getServletContext().getRealPath("/WEB-INF/upload") ;
        File file = new File(storeDirectory) ;
        if(!file.exists()) {
            file.mkdirs() ;
        }
        //获取上传文件名
        String filename = item.getName() ;
        if(filename != null && !"".equals(filename.toString().trim())) {
            filename = GUIDUtils.generateGUID() + "_" + filename.substring(filename.lastIndexOf("//") + 1) ;
        }
        //获取文件上传子目录
        String childDirectory = DirectoryUtils.getChildDirectoryByHashCode(storeDirectory, filename) ;
        File f = new File(storeDirectory + "/" + childDirectory, filename) ;
        //上传
        try {
            item.write(f) ;
        } catch (Exception e) {
            throw new RuntimeException("文件上传失败!!!") ;
        }
    }

    /**
    * @Name: processFormField
    * @Description: 处理表单提交普通字段,打印到控制台
    * @Author: XXX
    * @Version: V1.0
    * @CreateDate: XXX
    * @Parameters: @param item
    * @Return: void
     */
    private void processFormField(FileItem item) {
        try {
            String fieldName = item.getFieldName() ;
            String fieldValue = item.getString("UTF-8") ;
            System.out.println(fieldName + "->" + fieldValue);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        this.doGet(request, response);
    }
}

文件下载

Web应用中实现文件下载的两种方式

  1. 超链接直接指向下载资源;
  2. 程序实现下载:程序实现下载需设置两个响应头,
    (1)设置Content-Type 的值为:application/x-msdownload。Web 服务器需要告诉浏览器其所输出的内容的类型不是普通的文本文件或 HTML 文件,而是一个要保存到本地的下载文件;
    (2)Web 服务器希望浏览器不直接处理相应的实体内容,而是由用户选择将相应的实体内容保存到一个文件中,这需要设置 Content-Disposition 报头。该报头指定了接收程序处理数据内容的方式,在 HTTP 应用中只有 attachment 是标准方式,attachment 表示要求用户干预。在 attachment 后面还可以指定 filename 参数,该参数是服务器建议浏览器将实体内容保存到文件中的文件名称。在设置 Content-Dispostion 之前一定要指定 Content-Type。
    (3)因为要下载的文件可以是各种类型的文件,所以要将文件传送给客户端,其相应内容应该被当做二进制来处理,所以应该调用response.getOutputStream()方法返回 ServeltOutputStream 对象来向客户端写入文件内容。
response.setContentType("application/x-msdownload") ;
String str = "attachment;filename=" + java.net.URLEncoder.encode(fileName, "UTF-8") ;
response.setHeader("Content-Disposition", str) ;
InputStream is = new FileInputStream(file) ;
OutputStream os = response.getOutputStream() ;
int len = -1 ;
byte[] buf = new byte[1024] ;
while((len = is.read(buf)) != -1) {
    os.write(buf, 0, len) ;
    os.flush() ;
}
is.close() ;
os.close() ;

下载案例

      遍历上传目录下的所有文件显示给用户,并允许用户完成下载。

(1)ShowAllUploadFiles.java

package com.study.java.servlet;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @Name: ShowAllUploadFiles
* @Description: 显示所有已经上传的文件
* @Author: XXX
* @CreateDate: XXX 
* @Version: V1.0
 */
public class ShowAllUploadFiles extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        Map<String, String> map = new HashMap<String, String>() ;
        //获取文件上传主目录
        String storeDirectory = getServletContext().getRealPath("/WEB-INF/upload") ;
        File file = new File(storeDirectory) ;
        treeWalk(file, map) ;
        request.setAttribute("map", map) ;
        request.getRequestDispatcher("/listFiles.jsp").forward(request, response) ;
    }

    /**
    * @Name: treeWalk
    * @Description: 将文件上传主目录中的所有文件名称放入到Map集合中
    * @Author: XXX
    * @Version: V1.0
    * @CreateDate: XXX
    * @Parameters: @param file
    * @Parameters: @param map
    * @Return: void
     */
    private void treeWalk(File file, Map<String, String> map) {
        if(file.isDirectory()) {
            File[] list = file.listFiles() ;
            for (File f : list) {
                treeWalk(f, map) ;
            }
        } else {
            String guidFileName = file.getName() ;
            String oldFileName = guidFileName.substring(guidFileName.indexOf("_") + 1) ;
            map.put(guidFileName, oldFileName) ;
        }
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        this.doGet(request, response);
    }
}

(2)JSP视图页面

<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
    <title></title>
    <meta http-equiv="pragma" content="no-cache">
    <meta http-equiv="cache-control" content="no-cache">
    <meta http-equiv="expires" content="0">    
      </head>
  <body>
    <c:forEach items="${map}" var="me">
        <c:url value="/servlet/DownloadServlet" var="url">
            <c:param name="guidFileName" value="${me.key}"/>
        </c:url><hr/>
        ${me.value}
          
        <a href="${url}">下载</a><hr/>
    </c:forEach>
  </body>
</html>

(3)DownloadServlet.java

package com.study.java.servlet;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.URLEncoder;

import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.study.java.utils.DirectoryUtils;

/**
* @Name: DownloadServlet
* @Description: 文件下载
* @Author: XXX
* @CreateDate: XXX
* @Version: V1.0
 */
public class DownloadServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //获取请求链接中的参数,并进行编码,解决文件名中带中文的情况
        String guidFileName = request.getParameter("guidFileName") ;
        guidFileName = new String(guidFileName.getBytes("ISO-8859-1"), "utf-8") ;
        //获取文件储存目录
        String storeDirectory = getServletContext().getRealPath("/WEB-INF/upload") ;
        String childDirectory = DirectoryUtils.getChildDirectoryByHashCode(storeDirectory, guidFileName) ;
        File file = new File(storeDirectory + "/" + childDirectory, guidFileName) ;
        if(file.exists()) {
            //通知客户端下载
            String oldFileName = guidFileName.substring(guidFileName.indexOf("_") + 1) ;
            response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(oldFileName, "UTF-8")) ;
            //下载
            InputStream is = new FileInputStream(file) ;
            OutputStream os = response.getOutputStream() ;
            int len = -1 ;
            byte[] buf = new byte[1024] ;
            while((len = is.read(buf)) != -1) {
                os.write(buf, 0, len) ;
                os.flush() ;
            }
            is.close() ;
            os.close() ;
        } else {
            throw new RuntimeException("下载文件不存在!!!") ;
        }
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        this.doGet(request, response);
    }
}