28、文件上传和下载(JavaEE笔记)

一、文件上传概述

  • 实现web开发中的文件上传功能,需完成如下二步操作

    • 在web页面中添加上传输入项
    • 在servlet中读取上传文件的数据,并保存到本地硬盘中。
  • 如何在web页面中添加上传输入项
    <input type="file">标签用于在web页面中添加文件上传输入项,设置文件上传输入项时须注意:

    • 1、必须要设置input输入项的name属性,否则浏览器将不会发送上传文件的数据。
    • 2、必须把form的enctype属性值设置为multipart/form-data。设置该值后,浏览器在上传文件时,将把文件数据附带在http请求消息体中,并使用MIME协议对上传文件进行描述,以方便接收方对上传数据进行解析和处理。
  • 如何在Servlet中读取文件上传数据,并保存到本地硬盘中?

    • Request对象提供了一个getInputStream方法,通过这个方法可以读取到客户端提交过来的数据。但由于用户可能会同时上传多个文件,在servlet端编程直接读取上传数据,并分别解析出相应的文件数据是一项非常麻烦的工作,示例。
    • 为方便用户处理文件上传数据,Apache 开源组织提供了一个用来处理表单文件上传的一个开源组件(Commons-fileupload ),该组件性能优异,并且其API使用极其简单,可以让开发人员轻松实现web文件上传功能,因此在web开发中实现文件上传功能,通常使用Commons-fileupload组件实现,注意:此包依赖Commons-io包。
  • 使用Commons-fileupload组件实现文件上传,需要导入该组件相应的支撑jar包:Commons-fileuploadcommons-io。commons-io 不属于文件上传组件的开发jar文件,但Commons-fileupload组件从1.1 版本开始,它工作时需要commons-io包的支持。
    注意:其实tomcat已经继承了相应的工具包以完成上述两个工具包的功能,只是使用起来不太一样。这里我们还是使用上述两个包。

  • fileupload组件工作流程

fileupload组件工作流程.png
  • 核心API-DiskFileItemFactory
    DiskFileItemFactory是创建 FileItem 对象的工厂,这个工厂类常用方法:
    public void setSizeThreshold(int sizeThreshold)
    设置内存缓冲区的大小,默认值为10K。当上传文件大于缓冲区大小时, fileupload组件将使用临时文件缓存上传文件。
    public void setRepository(java.io.File repository)指定临时文件目录,默认值为System.getProperty("java.io.tmpdir")
    Public DiskFileItemFactory(int sizeThreshold, java.io.File repository)构造函数

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

二、相关示例

2.1示例一

UploadServlet1.java

package cn.itcast.web.servlet;
//文件上传,这里我们还是用的commons-fileupload和commons-io,但是tomcat中已经包含了相关的文件上传的包
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/*import org.apache.tomcat.util.http.fileupload.FileItem;
import org.apache.tomcat.util.http.fileupload.RequestContext;
import org.apache.tomcat.util.http.fileupload.disk.DiskFileItemFactory;
import org.apache.tomcat.util.http.fileupload.servlet.ServletFileUpload;
*/
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;

public class UploadServlet1 extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String savaPath = this.getServletContext().getRealPath("/WEB-INF/upload");//得到文件的保存目录,而这个目录一般不允许被人随意访问,所以应该放在WEB-INF下
        //request.setCharacterEncoding("UTF-8");//这种方式不能解决提交的数据的乱码问题.表单为文件上传设置Request编码无效,只能手工设置
        try{
            DiskFileItemFactory factory = new DiskFileItemFactory();
            //缓冲区默认是10k,下面我们设置如果上传文件超过10k则使用一个临时文件保存
            factory.setRepository(new File(this.getServletContext().getRealPath("/WEB-INF/temp")));
            
            ServletFileUpload upload = new ServletFileUpload(factory);
            upload.setProgressListener(//这里我们定义一个监听器,可以看到文件上传的进度,之后可以使用ajax生成一个进度条,同时注意定义的位置,这里是在解析器定义之后解析完文件之前
                    new ProgressListener() {
                        
                        @Override
                        public void update(long pBytesRead, long pContentLength, int arg2) {
                            System.out.println("文件大小为:" + pContentLength + ",当前已处理:" + pBytesRead);
                            
                        }
            });
            
            upload.setHeaderEncoding("UTF-8");//解决上传文件名在中文乱码问题
            
            if(!upload.isMultipartContent(request)){//如果不是文件上传
                //按照传统方式获取数据
                return ;
            }
            /*upload.setFileSizeMax(1024);//上传的文件不能超过1k
            upload.setSizeMax(1024*10);//上传文件的总大小不能超过10k
*/          List<FileItem> list = upload.parseRequest(request);
            for(FileItem item : list){
                if(item.isFormField()){
                    //fileItem中封装的是普通输入项的数据
                    String name = item.getFieldName();
                    
                    //String value = item.getString();
                    //value = new String(value.getBytes("iso8859-1"),"UTF-8");//由于对Request设置编码无效,这里我们只能手工设置
                    String value = item.getString("UTF-8");//也可以用这种方式制定码表
                    System.out.println(name + "=" + value);
                    
                }else{//提交的表单是上传文件,不是普通输入项
                    //request.getParameter("username");//这种方式是获取不到数据的,因为表单提交时以multipart/form-data形式提交的
                    String filename = item.getName();//注意:不同浏览器提交文件的方式是不一样的,有的是完整路径,但是有的是直接提交1.txt,所以这里我们需要作相应的截取工作
                    if(filename == null || filename.trim().equals("")){
                        continue;//如果某个框中没有选择上传的文件则跳出本次循环,继续下一次循环
                    }
                    
                    filename = filename.substring(filename.lastIndexOf("\\") + 1);
                    InputStream in = item.getInputStream();
                    String saveFileName = makeFileName(filename);//得到文件保存的名称
                    
                    String realSavaPath = makePath(saveFileName, savaPath);//产生目录
                    FileOutputStream out = new FileOutputStream(realSavaPath + "\\" + saveFileName);
                    byte buffer[] = new byte[1024];
                    int len = 0;
                    while((len = in.read(buffer)) > 0){
                        out.write(buffer, 0, len);
                    } 
                    in.close();
                    out.close();
                    item.delete();//删除临时文件,而且这行代码必须在上面流关闭的后面
                }
            }
        }catch(FileUploadBase.FileSizeLimitExceededException e){
            e.printStackTrace();
            request.setAttribute("message", "文件超出最大值");
            request.getRequestDispatcher("/message.jsp").forward(request, response);
            return ;
        }catch(Exception e){
            e.printStackTrace();
        }
    }
    //产生唯一的文件名
    public String makeFileName(String filename){

        return UUID.randomUUID().toString() + "_" + filename;
    }
    //产生一个目录
    public String makePath(String filename, String savePath){
        int hashCode = filename.hashCode();//得到一个文件名的哈希编码,即文件在内存中的地址
        int dir1 = hashCode & 0xf;//得到一个0-15的数,表示一级目录最多有16个目录
        int dir2 = (hashCode & 0xf0) >> 4;
        
        String dir = savePath + "\\" + dir1 + "\\" + dir2;//完整路径就是upload\2\3...
        File file = new File(dir);
        if(!file.exists()){
            file.mkdirs();//因为这里会有多级目录,则用此方法。若是只有一级目录,那使用mkdir方法
        }
        return dir;
    }

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

上传页面:upload.jsp

<body>
    <form action="${pageContext.request.contextPath }/servlet/UploadServlet1" 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>
  </body>

说明:

  • 1.文件上传的整体过程就是首先我们得到一个DiskFileItemFactory工厂,然后设置好临时保存文件。
  • 2.使用工厂得到ServletFileUpload解析器,解析器设置好监听器和文件名编码之后就可以进行文件解析了。
  • 3.文件解析首先判断是不是文件上传(根据请求头判断),如果不是,则直接返回;如果是,再次判断是普通输入项数据还是文件数据,是普通输入项数据则按普通方式进行解析;是文件数据则进行文件解析。

注意:

  • 1.上传文件名的中文乱码和上传数据的中文乱码问题

    • 1.1对于上传的文件名的中文乱码问题我们可以使用upload.setHeaderEncoding("UTF-8");解决
    • 1.2对于上传的数据的中文乱码问题我们可以使用
      value = new String(value.getBytes("iso8859-1"),"UTF-8");直接手工转换
      String value = item.getString("UTF-8");在获得数据的时候制定码表
  • 2.为保证服务器安全,上传文件应该放在外界无法直接访问的目录,比如 WEB-INF目录下。

  • 3.对于多次上传中出现相同的文件名问题我们需要解决:
    String saveFileName = makeFileName(filename);//得到文件保存的名称,使用UUID产生一个唯一的文件名
    FileOutputStream out = new FileOutputStream(savaPath + "\\" + saveFileName);

  • 4.为防止一个目录下出现太多文件,要使用hash算法打散存储

  • 5.要限制上传文件大小的最大值,可以通过
    upload.setFileSizeMax(1024);//上传的文件不能超过1k。实现并通过捕获FileUploadBase.FileSizeLimitExceededException异常给用户一个友好提示

  • 6.我们还可以设置当上传文件超出限制时(默认上传文件总大小是10k),使用临时文件保存

factory.setRepository(new File(this.getServletContext().getRealPath("/WEB-INF/temp")));

但是一般来说,文件会先保存在临时文件中,之后才会从临时文件复制到真正的存储目录中,而且这个临时文件一般不会删除,这里我们需要设置让其删除,如下。

  • 7.要想确保临时文件被删除一定要在处理上传文件后关闭流之后调用item.delete方法。

  • 8.限制上传文件的类型
    在收到上传文件名之后判断后缀名是否合法

  • 9.监听文件上传进度
    upload.setProgressListener

  • 10.当两个上传框中只有一个选择了上传文件而直接点击了提交:

if(filename == null || filename.trim().equals("")){
        continue;//如果某个框中没有选择上传的文件则跳出本次循环,继续下一次循环
}
  • 11.在web页面中动态添加上传输入项upload2.jsp
    有时候我们希望在上传文件的时候可以自己添加或删除上传文件的个数,这里可以对jsp进行修改:
<body>
  <!-- 如果想提交则在下面套一个form表单 ,同时如果是上传文件的话一定要注意其enctype="mutlipart/form-data"-->
  <!-- <form action="" enctype="mutlipart/form-data"> -->
    <table>
        <tr>
            <td>上传用户:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>上传文件:</td>
            <td><input type="button" value="添加上传文件" onclick="addInput()"></td>
        </tr>
        <tr>
            <td>    </td>
            <td>
                <div id="file">
                    
                </div>
            </td>
        </tr>
    </table>
    <script type="text/javascript">
        function addInput(){
            var div = document.getElementById("file");
            var input = document.createElement("input");
            input.type = "file";
            input.name = "filename";
            
            var del = document.createElement("input");
            del.type = "button";
            del.value = "删除";
            del.onclick = function d(){
                this.parentNode.parentNode.removeChild(this.parentNode);
            
            }
            var innerdiv = document.createElement("div");
            
            innerdiv.appendChild(input);
            innerdiv.appendChild(del);
            div.appendChild(innerdiv);
        }
    </script>
  </body>

三、文件下载

3.1 概述

  • web应用中实现文件下载的两种方式:

    • 1.超链接直接指向下载的资源;
    • 2.程序实现下载需要设置两个响应头:
      <1>设置Content-Type 的值为:application/x-msdownload。Web 服务器需要告诉浏览器其所输出的内容的类型不是普通的文本文件或 HTML 文件,而是一个要保存到本地的下载文件。
      <2>Web 服务器希望浏览器不直接处理相应的实体内容,而是由用户选择将
      相应的实体内容保存到一个文件中,这需要设置Content-Disposition 报头。该报头指定了接收程序处理数据内容的方式,在 HTTP 应用中只有attachment是标准方式,attachment表示要求用户干预。在 attachment 后面还可以指定 filename 参数,该参数是服务器建议浏览器将实体内容保存到文件中的文件名称。在设置 Content-Dispostion之前一定要指定Content-Type.
  • 因为要下载的文件可以是各种类型的文件,所以要将文件传送给客户端,其相应的内容应该被当作二进制来处理,所以应该调用response.getOutputStream方法返回ServletOutputStream对象来向客户端写入文件内容。

3.2 示例

列出网站所有文件:ListFileServlet.java

package cn.itcast.web.servlet;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
//列出网站所有的下载文件
public class ListFileServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String filepath = this.getServletContext().getRealPath("/WEB-INF/upload");
        Map map = new HashMap();
        listFile(new File(filepath), map);
        request.setAttribute("map", map);
        request.getRequestDispatcher("/listfile.jsp").forward(request, response);
    }
    
    public void listFile(File file, Map map){
        
        //为了让迭代出来的文件进行显示,这里我们一般传递一个map进行保存
        if(!file.isFile()){//如果是目录(不是文件)
            File files[] = file.listFiles();//得到所有文件或目录
            for(File f : files){
                listFile(f, map);
            }
        }else{//将迭代出的文件在jsp页面中进行显示
            String realname = file.getName().substring(file.getName().indexOf("_") + 1);
            map.put(file.getName(), realname);//强文件在硬盘中的名称作为key,而真实名称作为value
        }
    }

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

下载界面:listfile.jsp

<c:forEach var="me" items="${map }"><%-- filename=${me.key} 这里不能这样做,因为key中包含了中文--%>
        <c:url value="/servlet/DownLoadServlet" var="downurl">
            <c:param name="filename" value="${me.key}"></c:param>
        </c:url>
        ${me.value } <a href="${downurl} ">下载</a><br/>
</c:forEach>

DownLoadServlet.java

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

public class DownLoadServlet extends HttpServlet {

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String filename = request.getParameter("filename");
        //filename = new String(filename.getBytes("iso8859-1"),"UTF-8");???我这里不需要设置编码
        
        String path = makePath(filename, this.getServletContext().getRealPath("/WEB-INF/upload"));
        
        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"));
        //这里注意:URLEncoder.encode对IE是有效的,但是对火狐无效
        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 String makePath(String filename, String savePath){
        int hashCode = filename.hashCode();//得到一个文件名的哈希编码,即文件在内存中的地址
        int dir1 = hashCode & 0xf;//得到一个0-15的数,表示一级目录最多有16个目录
        int dir2 = (hashCode & 0xf0) >> 4;
            
        String dir = savePath + "\\" + dir1 + "\\" + dir2;//完整路径就是upload\2\3...
        File file = new File(dir);
        if(!file.exists()){
            file.mkdirs();//因为这里会有多级目录,则用此方法。若是只有一级目录,那使用mkdir方法
        }
        return dir;
    }
    public void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        doGet(request, response);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349

推荐阅读更多精彩内容