第 16 章 文件上传和下载

文件的上传和下载是项目开发中最常用的功能,例如图片的上传与下载、 邮件附件的上传与下载等。 接下来,本章将对 Spring MVC 环境中文件的上传和下载进行详细的讲解。

文件上传
  • 文件上传概述

多数文件上传都是通过表单形式提交给后台服务器的,因此,要实现文件上传功能,就需要提供一个文件上传的表单,而该表单必须满足以下 3 个条件。

  • form 表单的 method 属性设置为 post。
  • form 表单的 enctype 属性设置为 multipart/form-data。
  • 提供<input type="file" name="filename" />的文件上传输入框。

文件上传表单的示例代码如下。

  <form action="uploadUrl" method="post" enctype="multipart/form-data">
      <input type="file" name="filename">
      <input type="submit" value="文件上传">
  </form>

上述代码中,除了满足上传表单所必须的 3 个条件外,在<ínput>元素中还增加了一个 multiple 属性。 该属性是 HTML5 中的新属性,如果使用了该属性,则可以同时选择多个文件进行上传,即可实现多文件上传。
当客户端 form 表单的 enctype 属性为 multipart/form-data 时,浏览器就会采用二进制流的方式来处理表单数据,服务器端就会对文件上传的请求进行解析处理。 Spring MVC 为文件上传提供了直接的支持,这种支持是通过 MultipartResolver (多部件解析器)对象实现的 。 MultipartResolver 是一个接口对象,需要通过它的实现类 CommonsMultipartResolver 来完成文 件上传工作。 在 Spring MVC 中使用 MultipartResolver 对象非常简单,只需要在配置文件中定义 MultipartResolver 接口的 Bean 即可,其具体配置方式如下。

  <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
  <!-- 设置请求编码格式,必须与JSP中的pageEncoding属性一致 ,默认为ISO-8859-1 -->
  <property name="defaultEncoding" value="UTF-8" />
  <!-- 设置允许上传文件的最大值(2MB),单位为字节  -->
  <property name="maxUploadSize" value="2097152" />
  ...
  </bean>

在上述配置代码中,除配置了 CommonsMultipartResolver 类外,还通过<property>元素配置了编码格式以及允许上传文件的大小。 通过<property>元素可以对文件解析器类 CommonsMultipartResolver 的如下属性进行配置。

  • maxUploadSize: 上传文件最大长度(以字节为单位)。
  • maxlnMemorySize: 缓存中的最大尺寸。
  • defaultEncoding: 默认编码格式。
  • resolveLazily: 推迟文件解析,以便在 Controller 中捕获文件大小异常。

注意:因为 MultipartResolver 接口的实现类 CommonsMultipartResolver 内部是引用 multipartResolver 字符串获取该实现类对象并完成文件解析的,所以在配置 CommonsMultipartResolver 时,必须指定该 Bean 的 id 为MultipartResolver 。
由于 CommonsMultipartResolver 是 Spring MVC 内部通过 Apache Commons FileUpload 技术实现的,所以 Spring MVC 的文件上传还需要依赖 Apache Commons FileUpload 的组件, 即需要导入支持文件上传的相关 JAR 包,具体如下。
• commons-fileupload-1.3.2.jar
• commons-io-2.6.jar
以上两个 JAR 包大家可以通过 Apache 官网地址 " http://commons.apache.org/" 下载(进入该网址后,在 Apache Commons Proper 下方列表 的 Components 列中找到 FileUplod 和 iO ,单击链接后,即可在打开页面找到下载链接)。
当完成页面表单和文件上传解析器的配置后,在 Controller 中编写文件上传的方法即可实现 文件上传。 在 Spring MVC 中,文件上传的方法编写十分简单,其代码如下所示。

@Controller
public class FileUploadController {
  @RequestMapping("/fileUpload") 
  public String handleFormUpload(@RequestParam("name") String name,@RequestParam("filename")  MultipartFile file,...){
      if(!file.isEmpty()){
          //具体的执行办法
          ...
          return "uploadSuccess";
      }
      return "uploadFailure";
  }
}

在上述代码中,包含一个 MultipartFile 接口类型的参数 file ,上传到程序中的文件就是被封装在该参数中的。 org.springframework.web.multipart.MultipartFile 接口中提供了获取上传文件、 文件名称等方法,这些方法及其说明如表所示。

方法 说明
byte[] getBytes() 以字节数组的形式返回文件的内容
String getContentType() 返回文件的内窑类型
InputStream getlnputStream() 读取文内容,返回一个 InputStream 流
String getName() 获取多部件 form 表单的参数名称
String getOriginalFilename() 获取上传文件的初始化名
ong getSize() 获取上传文件的大小,单位是字节
boolean isEmpty() 判断上传的文件是否为空
void transferTo(File file) 将上传文件保存到目标目录下
  • 应用案例一一文件上传

通过上一小节的学习,相信大家对 Spring MVC 中实现文件上传的步骤和配置已经有了一个 大致的了解。 接下来,本小节就通过一个具体的案例来演示文件上传功能的实现,其具体步骤如下。
( 1 )在 Eclipse 中创建一个名为 springmvc06 的 Web 项目,将 Spring MVC 相关 JAR 包以及支持文件上传下载的 JAR 包添加到项目的 lib 目录中,并发布到类路径下。 添加后的 lib 目录如图所示。



( 2 )在 web.xml 文件中,配置 Spring MVC 的前端控制器等信息。
( 3 )在 src 目录下,创建并编写 Spring MVC 的核心配置文件 springmvc-config.xml ,如文件所示。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:mvc="http://www.springframework.org/schema/mvc"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
   http://www.springframework.org/schema/mvc
   http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd
   http://www.springframework.org/schema/context 
   http://www.springframework.org/schema/context/spring-context-4.3.xsd">
  <!-- 定义组件扫描器,指定需要扫描的包  -->
  <context:component-scan base-package="com.neuedu.controller" />
  <!-- 配置注解驱动  -->
  <mvc:annotation-driven />
  <!-- 定义视图解析器  -->
  <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
      <!-- 设置前缀  -->
      <property name="prefix" value="/WEB-INF/jsp/" /> 
      <!-- 设置后缀  -->
      <property name="suffix" value=".jsp" /> 
  </bean>
  <!-- 配置文件上传解析器   -->
  <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
      <!-- 设置请求编码格式  -->
      <property name="defaultEncoding" value="UTF-8" />
  </bean>     
</beans>

在上述文件中 ,除配置了 Spring MVC 环境需要的组件扫描器、注解驱动和视图解析器外, 还增加了支持文件上传的解析器 CommonsMultipartResolver 的配置。
( 4 ) 在 WebContent 目录下 , 创建一个用于上传文件的页面 fileUpload.jsp ,编辑后文件如下所示。

<%@ page language="java" contentType="text/html; charset=utf-8"
   pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>文件上传</title>
<script>
//判断是否填写上传人并已经选择上传文件
function check() {
  var name = document.getElementById("name").value;
  var file = document.getElementById("file").value;
  if(name == ""){
      alert("请填写上传人!");
      return false;
  }
  if(file.length==0||file==""){
      alert("请选择上传文件!");
      return false;
  }
  return true;
}
</script>
</head>
<body>
  
  <form action="fileUpload" method="post" enctype="multipart/form-data" onsubmit="return check()">
      上传人:<input type="text" id="name" name="name"><br />
      请选择文件:<input type="file" id="file" name="uploadfile"><br>
      <input type="submit" value="上传">
  </form>
</body>
</html>

在上述文件中,编写了一个用于文件上传的 form 表单,该表单可以填写上传人并上传文件。 当单击"上传"按钮时,会先执行 check() 方法来检查上传人文本框和文件选择框中的内容是否为空。 只有填写了上传人并选择了需要上传的文件后,才能正常提交表单;否则表单将不会提交,并给出相应提示信息。 提交表单后,会以 POST 方式提交到一个以 "/fileUpload" 结尾的请求中。
( 5 )在 WEB-INF 目录下,创建 JSP 文件夹,并在文件夹中创建 success.jsp 和 error.jsp 文件,分别在两个文件的<body>元素内编写显示上传成功的信息(如"文件上传成功!" )和显示 上传失败的信息(如"文件上传失败,请重新上传!" )。
( 6 )在 src 目录下,创建一个 com.neuedu.controller 包,在该包下创建一个用于文件上传的控制器类 FileUploadController,编辑后文件如下所示。

package com.neuedu.controller;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import com.sun.org.apache.bcel.internal.classfile.Field;
/**
* 文件上传
*/
@Controller
public class FileUploadController {
  /**
   * 执行文件上传
   */
  @RequestMapping("/fileUpload")
  public String handleFormUpload(@RequestParam("name") String name,@RequestParam("uploadfile") List<MultipartFile> uploadfile,HttpServletRequest request){
      //判断所上传文件是否存在
      if(!uploadfile.isEmpty() && uploadfile.size() > 0){
          //循环输出上传的文件
          for (MultipartFile file : uploadfile) {
              //获取上传文件的原始名称
              String originalFilename = file.getOriginalFilename();
              //设置上传文件的保存地址目录
              String dirPath = request.getServletContext().getRealPath("/upload/");
              File filePath = new File(dirPath);
              //如果保存文件的地址不存在,就先创建目录
              if(!filePath.exists()){
                  filePath.mkdirs();
              }
              //使用 UUID 重新命名上传的文件名称(上传人 _uuid_ 原始文件名称)
              String newFilename = name + "_"+UUID.randomUUID() + "_" + originalFilename;
              try {
                  file.transferTo(new File(dirPath + newFilename));
              } catch (Exception e) {
                  e.printStackTrace();
                  return "error";
              }
          }
          //跳转到成功页面
          return "success";
      }else{
          return "error";
      }       
  }
}

在文件中,使用注解方式定义了一个控制器类,并在类中定义了执行文件上传的方法 handleFormUpload()。 在 handleFormUpload() 方法参数中使用了List<MultipartFile>集合类型来接收用户上传的文件,然后判断所上传的文件是否存在。 如果存在,则继续执行上传操作,在通过 MultipartFile 接口的 transferTo() 方法将上传文件保存到用户指定的目录位置后,会跳转到 success.jsp 页面;如果文件不存在或者上传失败,则跳转到 error.jsp 页面。
( 7 )将项目发布到 Tomcat 服务器中并启动,在浏览器中访问地址 http://localhost8080/springmvc06/fileUpload.jsp ,其显示效果如图 16-2 所示。


在图的文件上传页面中,填写上传人并选择所要上传的文件,单击"上传"按钮后就可向后台发送上传请求信息。 这里填写上传人为"小韩",然后选择两张图片后,浏览器的显示效果如图所示。

单击"上传"按钮,程序在正确执行后浏览器就会跳转到 success.Jsp 页面,此时查看项目的发布目录,即可发现在 springmvc06 项目中多了一个 upload 文件夹,该文件夹中的内容如图所示。

从图中可以看出,已经成功上传了两张图片,并且图片的名称为"上传人uuid原始文件名称"的形式。 需要注意的是 , upload 文件夹是在项目的发布路径中,而不是创建的项目所在目录。 如果未更改项目发布路径,则要去工作空间的 metadata 目录中寻找项目发布目录(路径为: workspace.metadata.plugins\org.eclipse.WSt.server.core\tmpl\wtpwebapps\springmvc06\upload );如果将项目的发布路径已更改到 Tomcat 中,则需要在 Tomcat 的 webapps 目录中寻找项目 。

文件下载
  • 实现文件下载

文件下载就是将文件服务器中的文件下载到本机上。 在 Spring MVC 环境中,实现文件下载大致可分为如下两个步骤。
( 1 )在客户端页面使用一个文件下载的超链接,该链接的 href 属性要指定后台文件下载的方法以及文件名(需要先在文件下载目录中添加了一个名称为 "1 .jpg" 的文件),具体代码示例如下。

  <a href="${pageContext.request.contextPath}/download?filename=1.jpg">
      文件下载
  </a>

( 2 )在后台 Controller 类中,使用 Spring MVC 提供的文件下载方法进行文件下载。 Spring MVC 提供了一个 ResponseEntity 类型的对象,使用它可以很方便地定义返回的 HttpHeaders 对象和 HttpStatus 对象,通过对这两个对象的设置,即可完成下载文件时所需的配置信息。 文件下载的示例代码如下所示。

  @RequestMapping("/download") 
  public ResponseEntity<byte[]> fileDownload(HttpServletRequest request,String filename) throws IOException{
      //指定要下载的文件所在路径
      String path = request.getServletContext().getRealPath("/upload/");
      //创建该文件对象
      File file = new File(path + File.separator + filename);
      //设置响应头
      HttpHeaders headers = new HttpHeaders();
      //通知浏览器以下载的方式打开文件
      headers.setContentDispositionFormData("attachment", filename);
      //定义以流的形式下载返回文件数据
      headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
      //使用Spring MVC框架的ResponseEntity对象封装返回下载数据
      return new ResponseEntity<byte[]>(FileUtils.readFileToByteArray(file),headers,HttpStatus.OK);
  }

在 fileDownload() 方法中,首先根据文件路径和需要下载的文件名来创建文件对象,然后对响应头中文件下载时的打开方式以及下载方式进行了设置,最后返回 ResponseEntity 封装的下载结果对象。
ResponseEntity 对象有些类似前面章节介绍的@ResponseBody 注解,它用于直接返回结果对象。 上面示例中,设置响应头信息中的 MediaType 代表的是 Interner Media Type (即互联网媒体类型),也叫作 MIME 类型, MediaType.APPLlCATION_OCTET_STREAM 的值为 application/octet -stream ,即表示以二进制流的形式下载数据; HttpStatus 类型代表的是 Http 协议中的状态,示例中的 HttpStatus.OK 表示 200 ,即服务器已成功处理了请求。
在 Eclipse 的 WebContent 目录下,创建一个页面文件 download.jsp ,将上述的第 ( 1 ) 步的页面代码编写到 download.jsp 中,然后将第( 2 )步的 fileDownload() 方法编写在 FileUploadController 类中 。 发布项目并启动 Tomcat 服务器,在浏览器中访问地址 http://localhost:8080/springmvc06/download.jsp ,其显示效果如图所示。


单击图中的"文件下载"链接后,会出现下载提示弹窗,如图所示(这里以火狐浏览器为例进行演示)。

选择图中的"保存文件"并单击"确定"按钮后,即可下载该文件。

  • 中文名称的文件下载

虽然在前面小节中,通过 Spring MVC 实现了文件下载功能,但此案例代码只适用于非中文名称的文件下载。 当对中文名称的文件进行下载时,因为各个浏览器内部转码机制的不同,就会出现不同的乱码以及解析异常问题。 例如在文件下载目录中添加一个名称为"壁纸.jpg" 的文件,当通过浏览器下载该文件时,下载弹出窗口的显示如图所示。



从图中可以看出,所要下载的文件名称并不是"壁纸.jpg" ,而是"_.jpg" ,这就表示中文文件名称出现了乱码。 那么我们要如何解决这种乱码问题呢? 为了解决浏览器中文件下载时中文名称的乱码问题,可以在前端页面发送请求前先对中文名进行统一编码,然后在后台控制器类中对文件名称进行相应的转码,其具体实现步骤如下。
( 1 )在下载页面中对中文文件名编码。 可以使用 Servlet API 中提供的 URLEncoder 类中的 encoder(String s, String enc)方法将中文转为 UTF-8 编码。 该方法中第一个参数表示需要转码的字符串,第二个参数表示编码格式,其具体实现方式如文件所示。

<%@ page language="java" contentType="text/html; charset=utf-8"
   pageEncoding="utf-8"%>
<%@ page import="java.net.URLEncoder"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" >"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>下载页面</title>
</head>
<body>
  <a href="${pageContext.request.contextPath}/download?filename=<%=URLEncoder.encode("壁纸.jpg","UTF-8")%>">
      中文名称文件下载
  </a>
</body>
</html>

( 2 )修改控制器类 FileUploadController 中的 fileDownload() 方法,并增加对文件名进行编码的方法,其代码如下所示。

  @RequestMapping("/download") 
  public ResponseEntity<byte[]> fileDownload(HttpServletRequest request,String filename) throws IOException{
      //指定要下载的文件所在路径
      String path = request.getServletContext().getRealPath("/upload/");
      //创建该文件对象
      File file = new File(path + File.separator + filename);
      //对文件名编码,防止中文文件乱码
      filename = this.getFilename(request,filename);
      //设置响应头
      HttpHeaders headers = new HttpHeaders();
      //通知浏览器以下载的方式打开文件
      headers.setContentDispositionFormData("attachment", filename);
      //定义以流的形式下载返回文件数据
      headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
      //使用Spring MVC框架的ResponseEntity对象封装返回下载数据
      return new ResponseEntity<byte[]>(FileUtils.readFileToByteArray(file),headers,HttpStatus.OK);
  }
  /**
   * 根据浏览器的不同进行编码设置,返回编码后的文件名
   */
  private String getFilename(HttpServletRequest request, String filename) throws UnsupportedEncodingException {
      //IE不同版本的User-Agent中出现的关键词
      String[] IEBrowserKeyWords = {"MS1E", "Trident", "Edge"}; 
      //获取请求头代理信息
      String userAgent = request.getHeader("User-Agent"); 
      for (String keyWord : IEBrowserKeyWords) {
          if(userAgent.contains(keyWord)){
              //IE内核浏览器,统一为UTF-8编码显示
              return URLEncoder.encode(filename, "UTF-8") ; 
          }
      }
      //火狐等其他浏览器统一为ISO-8859-1编码显示
      return new String(filename.getBytes("UTF-8"), "ISO-8859-1"); 
  }

在方法 getFilename() 中,由于 IE 浏览器在文件编码上与其他浏览器的方式不同,所以在中文编码设置上 IE 浏览器设置为 UTF-8 编码,而火狐等其他浏览器设置为 ISO-8859-1 编码。 另外由于不同版本的 IE 浏览器,请求代理 User-Agent 中的关键字也略有不同,所以在判断 IE 浏览器时,需要特别注意 User-Agent 中的关键字。
再次进行中文名的文件下载测试,并在 IE 和火狐浏览器中分别单击文件下载链接后,两个浏览器的显示效果如图所示。




从图中的显示效果可以看出,所下载的文件已在两个浏览器中正确显示出了中文名称。

本章小结

本章主要对 Spring MVC 环境下的文件上传和下载进行了详细讲解。 首先讲解了如何实现文件上传,并通过一个应用案例来演示文件上传功能的实现;然后讲解了非中文名称文件下载的实现过程,以及中文名称文件下载的实现过程。 通过本章的学习,大家可以学会如何在 Spring MVC 环境下进行文件上传和下载,并能够掌握中文名称文件下载时乱码的解决方案。

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

推荐阅读更多精彩内容