美文网首页Java 杂谈程序员服务端开发实战
一文读懂SpringMVC中的文件上传与下载

一文读懂SpringMVC中的文件上传与下载

作者: Wizey | 来源:发表于2018-09-22 20:40 被阅读16次

    这两天研究了一下 SpringMVC 中文件上传与下载,也遇到了一些坑,这里做个总结。

    1、文件上传下载的原理

    Web 中文件上传下载是和 HTTP 协议分不开的,想要更加深入的理解文件上传和下载,必须要对 HTTP 协议有充分认识。

    1.1 文件上传

    在 TCP/IP 中,最早出现的文件上传机制是 FTP,这是将文件由客户端发送到服务器的标准机制。而在 Web 开发中,使用应用层协议 HTTP,通过在请求头中设置传输的内容类型 Content-Type 为 multipart/form-data; boundary=流分隔符值 来上传文件,这个流分隔符用来区分一个文件上传的开始和结束,下面的是我在火狐浏览器中截取的多个文件上传时的消息头和参数。

    文件上传消息头.jpg
    文件上传参数.jpg

    对应在 HTML 中就是为 form 元素设置 Method = "post" enctype="`multipart/form-data" 属性,为 input 元素设置 type = "file" 以及多个文件上传时设置 "multiple" 属性,代码示例如下。

    <form action="" method="post" enctype="multipart/form-data" onsubmit="return check()">
        <input type="file" name="file" id="file" multiple="multiple"><br>
        <input type="submit" value="上传">
    </form>
    

    对表单中的 enctype 属性做个详细的说明:

    • application/x-www=form-urlencoded:默认方式,只处理表单域中的 value 属性值,采用这种编码方式的表单会将表单域中的值处理成 URL 编码方式。
    • multipart/form-data:这种编码方式会以二进制流的方式来处理表单数据,这种编码方式会把文件域指定文件的内容也封装到请求参数中,不会对字符编码。
    • text/plain:除了把空格转换为 "+" 号外,其他字符都不做编码处理,这种方式适用直接通过表单发送邮件。

    1.2 文件下载

    通过在响应消息头中设置 Content-Disposition 和 Content-Type 使得浏览器无法使用某种方式或者激活某个程序来处理 MIME 类型的文件,来让浏览器提示是否保存文件,也就是文件的下载。Content-Disposition 的值为 attachment;filename=文件名,Content-Type 的值为 application/octet-stream 或者 application/x-msdownload。文件中的中文注意编码问题,不同浏览器之间是有差异的。

    文件下载.jpg

    2、SpringMVC中的文件上传与下载

    本文涉及的所以代码,都可以在我的 GitHub 上找到,传送门

    2.1 文件上传

    文件在上传时注意前后端最好都做下检查,如文件的大小,文件的类型等等,我这里就只做了后端的验证。

    文件上传页面代码:

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <!DOCTYPE html>
    <html lang="zh-cn">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport"
              content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>文件上传与下载</title>
        <noscript>
            <style>
                #main {
                    display: none !important;
                }
            </style>
            <p align="center">您的浏览器禁止了JS,请先启动脚本</p>
        </noscript>
        <script>
            function check() {
                var file = document.getElementById("file");
                if (file.value == "") {
                    alert("上传的文件为空")
                    return false;
                }
                return true;
            }
        </script>
    </head>
    <body>
        <div id="main" style="width:500px; margin: 0 auto;">
            <span style="color:red;">${msg}</span>
            <form action="" method="post" enctype="multipart/form-data" onsubmit="return check()">
                <input type="file" name="file" id="file" multiple="multiple"><br>
                <input type="submit" value="上传">
            </form>
        </div>
    </body>
    </html>
    

    在做限制文件上传的大小时,注意不要在 SpringMVC 中直接限制,尤其是大文件(2M以上的),否则在上传时 Tomcat 会关闭接收流,浏览器会失去响应。这个地方困扰的不止我一个人,这个 BUG 和 SpringMVC 无关,和 Tomcat 的一个属性有关系,请看下图,网上有人说 Tomcat7 就没有这个问题,但这不是推荐的解决问题方式。

    Tomcat文件上传大小限制.jpg

    经过一些研究,我的方案是用拦截器来做文件上传的大小限制。当拦截器拦截文件超过设置的值时就抛出异常,在 Controller 中处理异常,这里要在配置中延迟异常的解析时间。在拦截器的配置中,对拦截器的属性做限制,在拦截器中获取这个配置值,不要在拦截器中直接写死。Controller 中捕获这个异常,提示上传文件超过了限制。

    SpringMVC 中的配置:

    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
        <!--设置请求编码-->
        <property name="defaultEncoding" value="UTF-8"/>
        <property name="uploadTempDir" value="WEB-INF/tmp"/>
        <!--设置允许单个上传文件的最大值,不要在这里配置-->
        <!--<property name="maxUploadSizePerFile" value="31457280"/>-->
        <!--延迟解析,在Controller中抛出异常-->
        <property name="resolveLazily" value="true"/>
    </bean>
    <mvc:interceptors>
        <mvc:interceptor>
            <mvc:mapping path="/*upload*"/>
            <bean class="com.wenshixin.interceptor.FileUploadInterceptor">
                <property name="maxSize" value="31457280"/>
            </bean>
        </mvc:interceptor>
    </mvc:interceptors>
    

    拦截器:

    public class FileUploadInterceptor implements HandlerInterceptor {
        private long maxSize;
    
        @Override
        public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
            if (httpServletRequest != null && ServletFileUpload.isMultipartContent(httpServletRequest)) {
                ServletRequestContext servletRequestContext = new ServletRequestContext(httpServletRequest);
                long requestSize = servletRequestContext.contentLength();
                if (requestSize > maxSize) {
                    // 抛出异常
                    throw new MaxUploadSizeExceededException(maxSize);
                }
            }
            return true;
        }
    
        @Override
        public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
    
        }
    
        @Override
        public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
    
        }
    
        public void setMaxSize(long maxSize) {
            this.maxSize = maxSize;
        }
    }
    

    Controller 中的异常处理方法:

    @ExceptionHandler(MaxUploadSizeExceededException.class)
    public String handException(MaxUploadSizeExceededException e, HttpServletRequest request) {
        request.setAttribute("msg", "文件超过了指定大小,上传失败!");
        return "fileupload";
    }
    

    SpringMVC 中使用 MultipartFile 对象来接收上传的文件,通过这个对象可以得到文件的文件名和文件类型,通过 transferTo() 方法将文件写入到磁盘上。文件上传时,给文件重命名来防止上传文件重名产生覆盖,我这里采取是 UUID值 + 文件名,中间用下划线隔开。

    Controller 中的文件上传方法:

    @PostMapping(value = "/fileupload")
    public String fileUpload(@RequestParam(value = "file") List<MultipartFile> files, HttpServletRequest request) {
        String msg = "";
        // 判断文件是否上传
        if (!files.isEmpty()) {
            // 设置上传文件的保存目录
            String basePath = request.getServletContext().getRealPath("/upload/");
            // 判断文件目录是否存在
            File uploadFile = new File(basePath);
            if (!uploadFile.exists()) {
                uploadFile.mkdirs();
            }
            for (MultipartFile file : files) {
                String originalFilename = file.getOriginalFilename();
                if (originalFilename != null && !originalFilename.equals("")) {
                    try {
                        // 对文件名做加UUID值处理
                        originalFilename = UUID.randomUUID() + "_" + originalFilename;
                        file.transferTo(new File(basePath + originalFilename));
                    } catch (IOException e) {
                        e.printStackTrace();
                        msg = "文件上传失败!";
                    }
                } else {
                    msg = "上传的文件为空!";
                }
            }
            msg = "文件上传成功!";
        } else {
            msg = "没有文件被上传!";
        }
        request.setAttribute("msg", msg);
        return "fileupload";
    }
    

    文件上传的效果图:

    文件下载效果图.gif

    2.2 文件下载

    下载页面我使用了 Jquery 动态生成下载列表对 url 提前做了编码处理,防止文件名中 # 号等特殊字符的干扰,并对显示的文件名做了去除 UUID 值的处理,对 IE 浏览器也做了特殊的中文处理。

    下载页面:

    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <!DOCTYPE html>
    <html lang="zh-cn">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport"
              content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>文件上传与下载</title>
        <script src="${pageContext.request.contextPath}/js/jquery-1.12.4.min.js"></script>
        <script>
            $(function(){
                var targer = $("#main")
                $.ajax({
                    url: "fileList",
                    dataType: "json",
                    success: function (data) {
                        data = JSON.parse(data)
                        for (var i in data) {
                            var a = $("<a></a><br>").text(data[i].substring(data[i].indexOf("_")+1))
                            a.attr("href", "${pageContext.request.contextPath}/download?filename="+encodeURIComponent(data[i]))
                            targer.append(a)
                        }
                    }
                })
            })
        </script>
    </head>
    <body>
        <div id="main" style="width:500px; margin: 0 auto;">
        </div>
    </body>
    </html>
    

    Controller 中的下载方法:

    @RequestMapping(value = "/download")
    public ResponseEntity<byte[]> fileDownload(String filename, HttpServletRequest request) throws IOException {
        String path = request.getServletContext().getRealPath("/upload/");
        File file = new File(path + filename);
        //        System.out.println("转码前" + filename);
        filename = this.getFilename(request, filename);
        //        System.out.println("转码后" + filename);
        // 设置响应头通知浏览器下载
        HttpHeaders headers = new HttpHeaders();
        // 将对文件做的特殊处理还原
        filename = filename.substring(filename.indexOf("_") + 1);
        headers.setContentDispositionFormData("attachment", filename);
        headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
        return new ResponseEntity<byte[]>(FileUtils.readFileToByteArray(file), headers, HttpStatus.OK);
    }
    
    // 根据不同的浏览器进行编码设置,返回编码后的文件名
    public String getFilename(HttpServletRequest request, String filename) throws UnsupportedEncodingException {
        String[] IEBrowerKeyWords = {"MSIE", "Trident", "Edge"};
        String userAgent = request.getHeader("User-Agent");
        for (String keyword : IEBrowerKeyWords) {
            if (userAgent.contains(keyword)) {
                return URLEncoder.encode(filename, "UTF-8");
            }
        }
        return new String(filename.getBytes("UTF-8"), "ISO-8859-1");
    }
    

    下载文件的效果图(谷歌、火狐、IE、360浏览器):

    文件上传效果图.gif

    文件上传下载是 Web 开发中很常见的功能,但是要想做好也并不容易,浏览器的兼容性要考虑,如果追求用户体验,还可以在上传文件时给出进度条、AJAX实现页面无刷新上传,深感自己的前端水平还需要提高 😭,不说了学习去了。

    欢迎关注下方的微信公众号哦,里面有各种学习资料免费分享哦!

    编程心路

    相关文章

      网友评论

        本文标题:一文读懂SpringMVC中的文件上传与下载

        本文链接:https://www.haomeiwen.com/subject/apbxoftx.html