文件的上传与下载

文件的上传与下载

1、前言

       文件的上传与下载在JavaWeb中的应用是非常常见的,我们随便打开一个网站,基本上都会有上传与下载的功能,就比如我们经常使用某雷下载某老师的动作片,典型的文件上传下载,别人上传资源,然后你下载,然后你的那个朋友就一天不如一天。

文件的上传与下载文件的上传与下载

2、环境搭建

       由于涉及到文件的操作,所以肯定跟流有关,如果要在Servlet读取上传数据,可以通过Request对象提供的一个 getInputStream 方法来读取到客户端提交过来的数据(具体来说是 http 的请求体 entity)。但由于用户可能会同时上传多个文件,再在Servlet 端编程直接读取上传数据,并分别解析出相应的文件数据是一项非常麻烦的工作。那么为了方便处理上传数据,我们一般选择采用apache的开源工具common-fileupload这个文件上传组件,该组件可以将“multipart/form-data”类型请求的各种表单域解析出来,并实现一个或多个文件上传,同时也可以限制上传文件的大小等内容,其性能十分优异,使用极其简单。而common-fileupload是依赖于common-io这个包的,所以还需要下载这个包。下载链接这里给出:

根据自己需求下载对应版本,我这里都是下载最新的版本(注意下载的是二进制文件)。

然后我们创建一个FileUploadAndDownLoad项目,将这两个jar包导入到项目的lib目录中,如下图所示:

文件的上传与下载

接下来我们就可以编写相关代码了。

3、文件上传组件的核心API

    fileupload组件的工作流程图如下:

文件的上传与下载

从上图可知文件上传组件中有三个非常重要的类:DiskFileItemFactory、ServletFileUpload和FileItem,下面我们来学习一下。

DiskFileItemFactory核心API:DiskFileItemFactory是用来创建ServletFileUpload对象的工厂,这个工厂类常用方法:

  • 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核心API: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):设置编码格式。
  • setProgressListener(ProgressListener pListener):设置监听器用来显示进度条。

FileItem核心API:上面说了ServletFileUpload的作用是将表单的每个输入项封装成一个FileItem对象,那么我们就可以通过FileItem获取到封装的表单信息。常用方法有:

  • getFieldName(对于非文件上传域) :获得表单的name属性。
  • getString(对于非文件上传域):获得表单中该输入项的值(value)。
  • getName(对于文件上传域) :获得文件名。
  • getInputStream(对于文件上传域): 获得文件内容。
  • isFormField():是否为文件上传域,true不是文件上传,false是文件上传。

至此我们对这三个非常重要的类有了一定的了解,它的使用分为这四个步骤:

  1. 创建 DiskFileItemFactory 对象,设置缓冲区大小和临时文件目录。

  2. 使用 DiskFileItemFactory 对象创建 ServletFileUpload 对象,并设置上传文件的大小限制。

  3. 调用 ServletFileUpload.parseRequest() 方法解析 request 对象,得到一个保存了所有上传内容的 List 对象。

  4. 对 List 进行遍历,每遍历一个 FileItem 对象,调用其 FileItem.isFormField() 方法判断该对象是否是上传文件对象,该方法的返回值具体含义为:True 为普通表单字段,则调用 getFieldName() 获得 name 属性、getString() 方法获得 value 属性。False 为上传文件,则调用 getInputStream() 方法得到文件内容、getName() 方法获得文件名。

4、文件上传

在文件上传的页面中我们要特别注意两点:

  1. 表单中必须将method属性设置为post,ectype属性设置为“multipart/form-data”类型。
  2. input标签中必须将type属性设置为file,而且必须设置name的属性(见名知意即可),否则浏览器不会发送上传文件的数据。

①、上传页面upload.jsp的代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>文件上传</title>
</head>
<body>
<!--表单的enctype属性要设置为multipart/form-data-->
<form action="${pageContext.request.contextPath}/UploadServlet" method="post" enctype="multipart/form-data">
    <table width="600">
        <tr>
            <td>上传者</td>
            <td><input type="text" name="name"/></td>
        </tr>
        <tr>
            <td>上传文件1</td>
            <td><input type="file" name="file1"/></td>
        </tr>
        <tr>
            <td>上传文件2</td>
            <td><input type="file" name="file2"/></td>
        </tr>
        <tr>
            <!--设置单元格可横跨的列数。-->
            <td colspan="2"><input type="submit" value="上传"/></td>
        </tr>
    </table>
</form>
</body>
</html>

②、返回信息message.jsp的代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>消息提示</title>
</head>
    <body>
        ${message}
    </body>
</html>

③、创建处理上传文件的UploadServlet,代码如下:

package com.thr;

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

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
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;

/**
 * @author tanghaorong
 * @date 2020-05-10
 * @desc 文件上传代码
 */
@WebServlet(name = "UploadServlet",urlPatterns = "/UploadServlet")
public class UploadServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //设置上传文件的保存目录,将上传的文件存放于WEB-INF目录下,不允许外界直接访问,保证上传文件的安全
        String savePath = this.getServletContext().getRealPath("/WEB-INF/upload");
        //打印一下,看看路径在哪
        System.out.println(savePath);
        File file = new File(savePath);
        //判断上传文件的保存目录是否存在
        if (!file.exists() && !file.isDirectory()) {
            System.out.println(savePath+"目录不存在,需要创建");
            //创建目录
            file.mkdir();
        }
        //消息提示
        String message = "";
        try{
            //使用Apache文件上传组件处理文件上传步骤:
            //1、创建一个DiskFileItemFactory工厂
            DiskFileItemFactory factory = new DiskFileItemFactory();
            //2、创建一个文件上传解析器
            ServletFileUpload upload = new ServletFileUpload(factory);
            //解决上传文件名的中文乱码
            upload.setHeaderEncoding("UTF-8");
            //3、判断提交上来的数据是否是上传表单的数据
            if(!ServletFileUpload.isMultipartContent(request)){
                //按照传统方式获取数据
                return;
            }
            //4、使用ServletFileUpload解析器解析上传数据,解析结果返回的是一个List<FileItem>集合,每一个FileItem对应一个Form表单的输入项
            List<FileItem> list = upload.parseRequest(request);
            for(FileItem item : list){
                //如果fileitem中封装的是普通输入项的数据
                if(item.isFormField()){
                    String name = item.getFieldName();
                    //解决普通输入项的数据的中文乱码问题
                    String value = item.getString("UTF-8");
                    //value = new String(value.getBytes("iso8859-1"),"UTF-8");
                    System.out.println(name + "=" + value);
                }else{//如果fileitem中封装的是上传文件
                    //得到上传的文件名称,
                    String filename = item.getName();
                    System.out.println(filename);
                    if(filename==null || filename.trim().equals("")){
                        continue;
                    }
                    //注意:不同的浏览器提交的文件名是不一样的,有些浏览器提交上来的文件名是带有路径的,如:  c:a1.txt,而有些只是单纯的文件名,如:1.txt
                    //处理获取到的上传文件的文件名的路径部分,只保留文件名部分
                    filename = filename.substring(filename.lastIndexOf("\")+1);
                    //获取item中的上传文件的输入流
                    InputStream in = item.getInputStream();
                    //创建一个文件输出流
                    FileOutputStream out = new FileOutputStream(savePath + "\" + filename);
                    //创建一个缓冲区
                    byte buffer[] = new byte[1024];
                    //判断输入流中的数据是否已经读完的标识
                    int len = 0;
                    //循环将输入流读入到缓冲区当中,(len=in.read(buffer))>0就表示in里面还有数据
                    while((len=in.read(buffer))>0){
                        //使用FileOutputStream输出流将缓冲区的数据写入到指定的目录(savePath + "\" + filename)当中
                        out.write(buffer, 0, len);
                    }
                    //关闭输入流
                    in.close();
                    //关闭输出流
                    out.close();
                    //删除处理文件上传时生成的临时文件
                    item.delete();
                    message = "文件上传成功!";
                }
            }
        }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);
    }
}

③、运行效果如下:

当文件上传成功之后,上传的文件保存在了WEB-INF目录下的upload目录,如下图所示:

文件的上传与下载

    上述的代码虽然可以成功将文件上传到服务器上面的指定目录当中,但是如果我们再做一次同样的操作的话(就是将上传过的文件再上传一次),我们可以发现并不会多出来相同的图片,这是因为上传的文件名字名字是一样的,从而使的文件覆盖掉了原来的文件。而且我们也没有设置文件上传的大小,没有设置上传文件的类型,也就说可以上传任意大小的任意文件,这样就会导致出现一系列问题。综合上面所描述的,我们应该对文件的上传做出更加细节的处理,以下列出的几点需要特别注意的:

  • 乱码问题:

    对于表单中文乱码的处理方式:fileItem.getString("utf-8") 。

    对于上传文件名乱码处理方式:servletFileupload.setHeaderEncoding("utf-8") 。

  • 文件存储位置:

    为保证服务器安全,上传文件应该放在外界无法直接访问的目录下,比如放于WEB-INF目录下。

  • 文件名命名:

    上面也说了,如果上传的文件名相同的是会覆盖到原来的文件的,那么为防止文件覆盖的现象发生,要为上传文件产生一个唯一的文件名。

    处理方式: filename = UUID.randomUUID().toString() + filename;

  • 限制文件上传的大小

    限制上传文件的最大值。

    方式为:upload.setFileSizeMax(1024*1024);表示上传的文件不能超过1MB。实现并通过捕获FileUploadBase.FileSizeLimitExceededException异常来给用户一个友好的提示。

  • 文件大小超出限制时:

    如果上传的文件大小超出了设置的限制大小(默认上传文件总大小是10k),那我们可以使用临时文件保存。

    处理方法:factory.setRepository(new File(this.getServletContext().getRealPath("/WEB-INF/temp")));

    但是一般来说,文件会先保存在临时文件中,之后才会从临时文件复制到真正的存储目录中,而且这个临时文件一般不会删除,如果我们需要设置让其删除,在处理上传文件后关闭流之后调用item.delete方法即可。

  • 文件上传的多目录分散

    当上传的文件过多时,如果不把文件分散到其它目录去的话,那么所有的文件就会集中在一个目录,这样访问某个文件需要很长时间,甚至不响应,查找困难,所以应该采用目录分散算法,有如下几种目录分散算法:

       1、按时间 —— 比如一天一个目录

       2、按用户 —— 比如淘宝某个商家上传的所有商品图片都放在一个单独目录

       3、每个目录存放固定数量文件 —— 每个目录存放1000个文件,每次上传文件时判断目录中文件是否超过1000,若超过则新建一个目录,保存在新目录中

       4、哈希目录分散算法 (推荐)—— 对于一个对象,它会有一个 32 位的 hashcode ,这个 hashcode 通过一定的哈希算法生成,允许重复。把 32 位的 hashcode 分成 4*8,每次与 1111 进行“位与”运算,得到一个 4 位的值(0~15),然后右移 4 位,继续“位与”运算,每右移一次,目录就会增加一个层级,可根据不同的业务要求决定具体的目录层级数目,对于小型项目而言,2 级目录即可满足需求(总共有(2^4)^2=256个文件夹)。具体如图所示:

文件的上传与下载

对于图中数据来说,它的 1 级目录号为 12,它 2 级目录号为 14,故这个文件位于12号文件夹的14号文件夹里面。

  • 文件上传进度的监听:

    为了让用户有更好的体验,页面应该实时显示上传的速度、剩余时间甚至用进度条美化来代替。文件上传进度的监听器为:ProgressListener。它是一个接口,其内部有一个非常重要的方法update。

    void update(long pBytesRead, long pContentLength, int pItems)。

    update方法中的三个参数说明:

    pBytesRead:表示已读取的字节总数。

    pContentLength :表示正在读取的总字节数。可能是-1,如果这个数字是未知的。

    pItems:表示当前正在读取的字段的编号。(0 =目前没有项,1 =第一项正在读取,…)。

    我们一般直接在程序中以内部类的方式使用文件上传进度的监听器为。

    //创建一个文件上传解析器
    ServletFileUpload upload = new ServletFileUpload(factory);
    //监听文件上传进度
    upload.setProgressListener(new ProgressListener(){
        public void update(long pBytesRead, long pContentLength, int pItems) {
            System.out.println("上传文件大小为:" + pContentLength + ",当前已处理:" + pBytesRead+
                    ",form第几项:" + pItems); }
    });

所以针对上面的一些细节问题,对前面的UploadHandleServlet进行了改进,改进后的代码如下:

package com.thr;

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.annotation.WebServlet;
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;

/**
 * @author tanghaorong
 * @date 2020-05-10
 * @desc 文件上传代码
 */
@WebServlet(name = "UploadServlet",urlPatterns = "/UploadServlet")
public class UploadServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //得到上传文件的保存目录,将上传的文件存放于WEB-INF目录下,不允许外界直接访问,保证上传文件的安全
        String savePath = this.getServletContext().getRealPath("/WEB-INF/upload");
        System.out.println(savePath);
        //上传时生成的临时文件保存目录
        String tempPath = this.getServletContext().getRealPath("/WEB-INF/temp");
        File tmpFile = new File(tempPath);
        if (!tmpFile.exists()) {
            //创建临时目录
            tmpFile.mkdir();
        }

        //消息提示
        String message = "";
        try{
            //使用Apache文件上传组件处理文件上传步骤:
            //1、创建一个DiskFileItemFactory工厂
            DiskFileItemFactory factory = new DiskFileItemFactory();
            //设置工厂的缓冲区的大小,当上传的文件大小超过缓冲区的大小时,就会生成一个临时文件存放到指定的临时目录当中。
            factory.setSizeThreshold(1024*100);//设置缓冲区的大小为100KB,如果不指定,那么缓冲区的大小默认是10KB
            //设置上传时生成的临时文件的保存目录
            factory.setRepository(tmpFile);
            //创建一个文件上传解析器
            ServletFileUpload upload = new ServletFileUpload(factory);
            //监听文件上传进度
            upload.setProgressListener(new ProgressListener(){
                public void update(long pBytesRead, long pContentLength, int pItems) {
                    System.out.println("上传文件大小为:" + pContentLength + ",当前已处理:" + pBytesRead+
                            ",form第几项:" + pItems); }
            });
            //解决上传文件名的中文乱码
            upload.setHeaderEncoding("UTF-8");
            //3、判断提交上来的数据是否是上传表单的数据
            if(!ServletFileUpload.isMultipartContent(request)){
                //按照传统方式获取数据
                return;
            }

            //设置上传单个文件的大小的最大值,目前是设置为1024*1024字节,也就是1MB
            upload.setFileSizeMax(1024*1024);
            //设置上传文件总量的最大值,最大值=同时上传的多个文件的大小的最大值的和,目前设置为10MB
            upload.setSizeMax(1024*1024*10);
            //4、使用ServletFileUpload解析器解析上传数据,解析结果返回的是一个List<FileItem>集合,每一个FileItem对应一个Form表单的输入项
            List<FileItem> list = upload.parseRequest(request);
            for(FileItem item : list){
                //如果fileitem中封装的是普通输入项的数据
                if(item.isFormField()){
                    String name = item.getFieldName();
                    //解决普通输入项的数据的中文乱码问题
                    String value = item.getString("UTF-8");
                    //value = new String(value.getBytes("iso8859-1"),"UTF-8");
                    System.out.println(name + "=" + value);
                }else{//如果fileitem中封装的是上传文件
                    //得到上传的文件名称,
                    String filename = item.getName();
                    System.out.println(filename);
                    if(filename==null || filename.trim().equals("")){
                        continue;
                    }
                    //注意:不同的浏览器提交的文件名是不一样的,有些浏览器提交上来的文件名是带有路径的,如:  c:a1.txt,而有些只是单纯的文件名,如:1.txt
                    //处理获取到的上传文件的文件名的路径部分,只保留文件名部分
                    filename = filename.substring(filename.lastIndexOf("\")+1);
                    //得到上传文件的扩展名
                    String fileExtName = filename.substring(filename.lastIndexOf(".")+1);
                    //如果需要限制上传的文件类型,那么可以通过文件的扩展名来判断上传的文件类型是否合法
                    System.out.println("上传的文件的扩展名是:"+fileExtName);
                    //获取item中的上传文件的输入流
                    InputStream in = item.getInputStream();
                    //得到文件保存的名称
                    String saveFilename = makeFileName(filename);
                    //得到文件的保存目录
                    String realSavePath = makePath(saveFilename, savePath);
                    //创建一个文件输出流
                    FileOutputStream out = new FileOutputStream(realSavePath + "\" + saveFilename);
                    //创建一个缓冲区
                    byte buffer[] = new byte[1024];
                    //判断输入流中的数据是否已经读完的标识
                    int len = 0;
                    //循环将输入流读入到缓冲区当中,(len=in.read(buffer))>0就表示in里面还有数据
                    while((len=in.read(buffer))>0){
                        //使用FileOutputStream输出流将缓冲区的数据写入到指定的目录(savePath + "\" + filename)当中
                        out.write(buffer, 0, len);
                    }
                    //关闭输入流
                    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);
    }

    /**
     * @Method: makeFileName
     * @Description: 生成上传文件的文件名,文件名以:uuid+"_"+文件的原始名称
     *
     * @param filename 文件的原始名称
     * @return uuid+"_"+文件的原始名称
     */
    private String makeFileName(String filename){  //2.jpg
        //为防止文件覆盖的现象发生,要为上传文件产生一个唯一的文件名
        return UUID.randomUUID().toString() + "_" + filename;
    }

    /**
     *
     * @Method: makePath
     * @Description: 为防止一个目录下面出现太多文件,要使用hash算法打散存储
     *
     * @param filename 文件名,要根据文件名生成存储目录
     * @param savePath 文件存储路径
     * @return 新的存储目录
     */
    private String makePath(String filename,String savePath){
        //得到文件名的hashCode的值,得到的就是filename这个字符串对象在内存中的地址
        int hashcode = filename.hashCode();
        int dir1 = hashcode&0xf;  //0--15
        int dir2 = (hashcode&0xf0)>>4;  //0-15
        //构造新的保存目录
        String dir = savePath + "\" + dir1 + "\" + dir2;  //upload23  upload35
        //File既可以代表文件也可以代表目录
        File file = new File(dir);
        //如果目录不存在
        if(!file.exists()){
            //创建目录
            file.mkdirs();
        }
        return dir;
    }

    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        doGet(request, response);
    }
}

这些细节问题进行改进之后,我们的文件上传功能就算是做得比较完善了。

4、文件下载

文件的下载,首先我们要列出所有上传的文件,然后才能进行下载。

①、创建列出网站所有文件的ListFileServlet,代码如下:

package com.thr;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
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;

/**
 * @author tanghaorong
 * @date 2020-05-10
 * @desc 显示下载文件的页面
 */
@WebServlet(name = "ListFileServlet",value = "/ListFileServlet")
public class ListFileServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //获取上传文件的目录
        String uploadFilePath = this.getServletContext().getRealPath("/WEB-INF/upload");
        System.out.println(uploadFilePath);
        //存储要下载的文件名
        Map<String,String> fileNameMap = new HashMap<String,String>();
        //递归遍历filepath目录下的所有文件和目录,将文件的文件名存储到map集合中
        listfile(new File(uploadFilePath),fileNameMap);//File既可以代表一个文件也可以代表一个目录
        //将Map集合发送到listfile.jsp页面进行显示
        request.setAttribute("fileNameMap", fileNameMap);
        request.getRequestDispatcher("/download.jsp").forward(request, response);
    }

    /**
     * @Method: listfile
     * @Description: 递归遍历指定目录下的所有文件
     *
     * @param file 即代表一个文件,也代表一个文件目录
     * @param map 存储文件名的Map集合
     */
    public void listfile(File file, Map<String,String> map){
        //如果file代表的不是一个文件,而是一个目录
        if(!file.isFile()){
            //列出该目录下的所有文件和目录
            File files[] = file.listFiles();
            //遍历files[]数组
            for(File f : files){
                //递归
                listfile(f,map);
            }
        }else{
            /**
             * 处理文件名,上传后的文件是以uuid_文件名的形式去重新命名的,去除文件名的uuid_部分
             file.getName().indexOf("_")检索字符串中第一次出现"_"字符的位置,如果文件名类似于:9349249849-88343-8344_阿_凡_达.avi
             那么file.getName().substring(file.getName().indexOf("_")+1)处理之后就可以得到阿_凡_达.avi部分
             */
            String realName = file.getName().substring(file.getName().indexOf("_")+1);
            //file.getName()得到的是文件的原始名称,这个名称是唯一的,因此可以作为key,realName是处理过后的名称,有可能会重复
            map.put(file.getName(), realName);
        }
    }

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

②、下载页面download.jsp,代码如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>显示下载文件页面</title>
</head>
<body>
    <!-- 遍历Map集合 -->
    <c:forEach var="me" items="${fileNameMap}">
        <c:url value="/DownLoadServlet" var="downurl">
            <c:param name="filename" value="${me.key}"></c:param>
        </c:url>
        ${me.value}<a href="${downurl}">下载</a>
        <br/>
    </c:forEach>
</body>
</html>

③、创建处理下载文件的DownloadServlet,代码如下:

    因为要下载的文件可以是各种类型的文件,所以要将文件传送给客户端,其相应的内容应该被当作二进制来处理,所以应该调用response.getOutputStream方法返回ServletOutputStream对象来向客户端写入文件内容。

package com.thr;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
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;

/**
 * @author tanghaorong
 * @date 2020-05-10
 * @desc 文件下载代码
 */
@WebServlet(name = "DownLoadServlet",value = "/DownLoadServlet")
public class DownLoadServlet extends HttpServlet {
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        //得到要下载的文件名
        String fileName = request.getParameter("filename");  //23239283-92489-阿凡达.avi
        fileName = new String(fileName.getBytes("iso8859-1"),"UTF-8");
        //上传的文件都是保存在/WEB-INF/upload目录下的子目录当中
        String fileSaveRootPath=this.getServletContext().getRealPath("/WEB-INF/upload");
        //通过文件名找出文件的所在目录
        String path = findFileSavePathByFileName(fileName,fileSaveRootPath);
        //得到要下载的文件
        File file = new File(path + "\" + fileName);
        //如果文件不存在
        if(!file.exists()){
            request.setAttribute("message", "您要下载的资源已被删除!!");
            request.getRequestDispatcher("/message.jsp").forward(request, response);
            return;
        }
        //处理文件名
        String realname = fileName.substring(fileName.indexOf("_")+1);
        //设置响应头,控制浏览器下载该文件
        response.setHeader("content-disposition", "attachment;filename=" + URLEncoder.encode(realname, "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();
    }

    /**
     * @Method: findFileSavePathByFileName
     * @Description: 通过文件名和存储上传文件根目录找出要下载的文件的所在路径
     *
     * @param filename 要下载的文件名
     * @param saveRootPath 上传文件保存的根目录,也就是/WEB-INF/upload目录
     * @return 要下载的文件的存储目录
     */
    public String findFileSavePathByFileName(String filename,String saveRootPath){
        int hashcode = filename.hashCode();
        int dir1 = hashcode&0xf;  //0--15
        int dir2 = (hashcode&0xf0)>>4;  //0-15
        String dir = saveRootPath + "\" + dir1 + "\" + dir2;  //upload23  upload35
        File file = new File(dir);
        if(!file.exists()){
            //创建目录
            file.mkdirs();
        }
        return dir;
    }

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

④、运行结果:

访问ListFileServlet,就可以在download.jsp页面中显示提供给用户下载的文件资源,如下图所示:

文件的上传与下载

然后点击【下载】超链接,将请求提交到DownLoadServlet就行处理就可以实现文件下载了。

参考资料:

https://www.cnblogs.com/xdp-gacl/p/4200090.html

https://www.jianshu.com/p/cdf5700b4abb

https://www.jianshu.com/p/a6751962f3e8