文件上传的实现(详解)

文件上传

前言

如果要选择一个文件并上传到服务器, 你需要在 <form> 中添加 <input type=“file”> 字段. 根据 HTML规范, 你需要为 form 使用 POST 方法进行提交, 并需要将编码方式 enctype 设置为 multipart/form-data:

<form action="upload" method="post" enctype="multipart/form-data">

  <input type="text" name="description" placeholder="文件描述" />

  <input type="file" name="file" />

  <input type="submit" />

</form>

这样的表单提交的时候, 会在请求体(request body)中保存二进制的 multipart 数据, 这跟不设置 enctype 是完全不同的。

PS: multipart 编码的数据大致是这个样子的:

------WebKitFormBoundaryrGKCBY7qhFd3TrwA

Content-Disposition: form-data; name="title"

harttle

------WebKitFormBoundaryrGKCBY7qhFd3TrwA

Content-Disposition: form-data; name="avatar"; filename="harttle.png"

Content-Type: image/png

... content of harttle.png ...

------WebKitFormBoundaryrGKCBY7qhFd3TrwA--

服务端

在 Servlet 3.0 以前, Servlet API 本身不支持 multipart/form-data, 它只支持表单默认的 application/x-www-form-urlencoded 编码. 当提交 multipart 类型数据的时候, 在服务端, 调用 HttpServletRequest#getParameter() 会得到 null 值. 特别不方便.

这就是 Apache Commons FileUpload 出现的原因。

1. 不要手动解析!

理论上,你可以自己通过 ServletRequest#getInputStream() 手动解析数据, 但这是个细致并且繁琐的任务, 需要你对 HTML 规范有一定了解. 千万别浪费太多时间在这上面, 不仅没有意义, 而且你的解析可能包含很多错误. 最好的方式, 是选择一个成熟靠谱的实现.

2. 如果你使用的是 Servlet 3.0 或以后的版本,使用内置的 MultiPart API !!!

如果你使用 Servlet 3.0 以上(Tomcat 7.0), 就可以使用标准 API 中提供的 HttpServletRequest#getPart() 获取文件数据. 同时, 可以使用 getParameter() 获取同一表单中其他的 非文件 字段的值。

要接收上传的数据, 总共分两步:

【首先】 在你的 Servlet 上面标注 @MultipartConfig, 使你的 Servlet 有处理 multipart/form-data 的能力, 并使 getPart() 方法生效:

@WebServlet("/upload")

@MultipartConfig

public class UploadServlet extends HttpServlet {

    // ...

}

你也可以根据需要为 @MultipartConfig 增加参数, 这样可以配置上传的一些细节, 这些参数也可以放到 web.xml 中全局使用。

【然后】 你需要实现自己的 doPost() 方法:

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

    // 获取 <input type="text" name="description"> 的值

    String description = request.getParameter("description");

    // 获取 <input type="file" name="file"> 的值

    Part part = request.getPart("file");


    // 常用属性

    System.out.printf(

                      "Part 对象属性方法:\n> Name: %s\n> Size: %d\n> ContentType: %s\n> getSubmittedFileName: %s\n> HeaderNames: %s\n> disposition: %s\n",

                      part.getName(),

                      part.getSize(),

                      part.getContentType(),

                      part.getSubmittedFileName(),

                      part.getHeaderNames(),

                      part.getHeader("content-disposition"));

    // 得到文件名

    String fileName = part.getSubmittedFileName();

    // 处理文件

    part.write(你的保存路径);


    // 其他操作 ...

}

如果你在 form 表单中指定了 multiple: <input type=file multiple />, 表示允许一次上传多个文件, 那么, 在服务端, 你可以使用 HttpServletRequest#getParts() 获取所有文件的列表, 然后根据需要逐个处理。

Servlet 3.1 新增了 Part#getSubmittedFileName() 方法, 它用来得到文件的原始名字, 如果是之前的版本, 文件的名字需要从 head 参数里手动获取, 比如:

String fileName = part.getHeader("content-disposition").split("filename=")[1].replace("\"", "");

3. 如果你使用的是 Servlet 3.0 以前的版本, 请使用 Apache Commons FileUpload !

这是当前最流行, 也是最成熟的第三方库, 你需要将两个 jar 包放到你的 lib 包下:

commons-fileupload.jar

commons-io.jar

它的使用也比较简单, 示例如下:

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

    try {

        // 初始化 uploader 对象, 需要一个 FileItemFactory 参数, 用来配置对上传文件的限制

        ServletFileUpload uploader = new ServletFileUpload(new DiskFileItemFactory());

        // 调用 parseRequest 方法, 将 multipart 所有的数据封装到 list 中

        List<FileItem> items = uploader.parseRequest(request);

        // 循环处理

        for (FileItem item : items) {

            if (item.isFormField()) { // 处理普通字段 (input type="text|radio|checkbox|etc", select, 等).

                String fieldName = item.getFieldName();

                String fieldValue = item.getString();

                // ... (其他操作)

            } else { // 处理文件数据 (input type="file").

                String fieldName = item.getFieldName();

                String fileName = FilenameUtils.getName(item.getName());

                item.write(我的保存路径);

                // ... (其他操作)

            }

        }

    } catch (FileUploadException e) {

        throw new ServletException("解析文件出错.", e);

    }

    // ...

}

就这么简单。

客户端

不管在 IE/Firefox 还是 Chrome 浏览器上, 上传按钮的样式都很丑, 所以, 我们需要:

1. 自定义上传按钮的样式!

怎么搞呢? 一般的手段是: 通过 css 将上传按钮变透明(opacity), 并放到其他元素上面(position).

在 html5 中,也可以使用 label 配合 input 的 display:none 实现:

<label style="我的样式">

  选择图片

  <input type="file" style="display:none" />

</label>

除了样式要好看, 另外一个重要的用户体验是:

2. 选择文件后给我一个预览图吧!:

在图片上传中, 如果选中后, 能够预览图片, 那是极好的啊! – by 牛顿

可怎么实现呢? 方法很多, 但这样是不行的:

$('#preview').attrib('src', $(':file').val())

获取到的 file 字段的值是类似 C:\FakePath\xxxx 的形式, 因为浏览器为了安全方面的考虑, 并不会允许 js 能获取到真正的文件路径.

怎么办呢? 使用 html5 的 URL#createObjectURL() 是一种选择, 也可以使用 FileReader 进行更复杂的处理.

3. 下面是一个样式+预览的示例:

<body>

  <!-- 设置样式 -->

  <style>

    .filewrapper {

        padding: 8px 12px;

        font-size: 1.2em;

        background: #333;

        color: goldenrod;

        border-radius: 5px;

        cursor: hand;

    }

    .filewrapper:hover {

        background: #000;

    }

  </style>

  <!-- 将 input 隐藏,给 label 一个美丽的外观。点击 label 的时候,会激活相对应的 input -->

  <label class="filewrapper">

    点击选择图片

    <input id="b" name="b" style="display:none" type="file" />

  </label>

  <!-- 选择图片后的预览图 -->

  <div>

    <img id="preview" src="" style="width: 200px; height: 200px; margin: 20px auto;">

  </div>

  <!-- 选择文件后, 在 preview 区域显示图片预览 -->

  <!-- 使用了 html5 的 FileReader 对象 -->

  <script>

    $("#b").change(function (event) {

        var file = $("#b")[0].files[0];

        var reader = new FileReader();

        reader.onload = function (e) {

            $("#preview").attr("src", this.result);

        };

        reader.readAsDataURL(file);

    });

  </script>

</body>

效果图为:

assets/image/howdoudo-fileupload/5233708_2017-07-06_00-21-36_2017-07-10_13-40-26.png

4. 另一个重点, 是实现异步上传:

话不多说,代码在此:

// 表单提交,交给 submitForm 函数处理

$('form').on('submit', submitForm);

// 通过 jQuery 进行异步提交

function submitForm() {

    // 使用 html5 的 FormData 封装表单数据

    let formData = new FormData($('form')[0]);

    $.ajax({

        url : '/upload',

        method : 'POST',

        data : formData,

        cache : false,

        processData : false,  // jQuery 啊,你不要修改我上传的数据

        contentType : false,  // jQuery 啊,你不要私自设置 Content-Type

        xhr: function () {    // 如果需要进度条的话,可以为 xhr 对象的 upload 绑定 progress 事件;如果不需要进度条,这里可省略

            let xhr = new window.XMLHttpRequest();

            xhr.upload.addEventListener("progress", processHandler, false);

            return xhr;

        }

    }).done(function(data) {

        console.log(data);

        alert(data);

    });

    return false;

}

// 进度监听函数,可以自定义进度条变化等效果

function processHandler(event) {

    if (event.lengthComputable) {

        // 获取进度

        var percent = parseInt(100 * event.loaded / event.total);

        // 根据进度更新显示

        console.log(percent);

        // 完成之后...

        if (percent === 100) {}

    }

}

z. 最后, 我们可以选择一些上传插件, 为项目快速增加上传功能.

jQuery-File-Upload

Dropzone JS

其他

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容

  • 本文包括:1、文件上传概述2、利用 Commons-fileupload 组件实现文件上传3、核心API——Dis...
    廖少少阅读 12,546评论 5 91
  • 一、文件上传概述 实现web开发中的文件上传功能,需完成如下二步操作在web页面中添加上传输入项在servlet中...
    yjaal阅读 2,892评论 0 22
  • Servlet 实现文件上传 所谓文件上传就是将本地的文件发送到服务器中保存。例如我们向百度网盘中上传本地的资源或...
    Lucky_Light阅读 6,589评论 2 23
  • Servlet 3.0 新特性概述 Servlet 3.0 作为 Java EE 6 规范体系中一员,随着 Jav...
    lovePython阅读 552评论 0 5
  • 多年后,依然无法忘记小的时候那个稚嫩却对这个世界充满好奇的我,小的时候老师叫我参演定军山,换戏服时裤子里却夹着...
    尘曦月下阅读 174评论 0 0