> For the complete documentation index, see [llms.txt](https://potoyang.gitbook.io/spring-in-action-v4/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://potoyang.gitbook.io/spring-in-action-v4/di-7-zhang-spring-mvc-de-gao-ji-ji-shu/untitled-4/untitled.md).

# 7.2.2　处理 multipart 请求

现在已经在 Spring 中（或 Servlet 容器中）配置好了对 mutipart 请求的处理，那么接下来我们就可以编写控制器方法来接收上传的文件。要实现这一点，最常见的方式就是在某个控制器方法参数上添加 @RequestPart 注解。

假设我们允许用户在注册 Spittr 应用的时候上传一张图片，那么我们需要修改表单，以允许用户选择要上传的图片，同时还需要修改 SpitterController 中的 processRegistration() 方法来接收上传的图片。如下的代码片段来源于 Thymeleaf 注册表单视图（registrationForm.html），着重强调了表单所需的修改：

```markup
<form method="POST" th:object="${spitter}" enctype="multipart/form-data">
  
  ...
  <label>Profile Picture</label>:
  <input type="file" name="profilePicture" accept="image/jpeg,image/png,image/gif" /><br/>
  ...
  
</form>
```

\<form> 标签现在将 enctype 属性设置为 multipart/formdata，这会告诉浏览器以 multipart 数据的形式提交表单，而不是以表单数据的形式进行提交。在 multipart 中，每个输入域都会对应一个 part。

除了注册表单中已有的输入域，我们还添加了一个新的域，其 type 为 file。这能够让用户选择要上传的图片文件。accept 属性用来将文件类型限制为 JPEG、PNG 以及 GIF 图片。根据其 name 属性，图片数据将会发送到 multipart 请求中的 profilePicture part 之中。

现在，我们需要修改 processRegistration() 方法，使其能够接受上传的图片。其中一种方式是添加 byte 数组参数，并为其添加 @RequestPart 注解。如下为示例：

```java
@RequestMapping(value="/register", method=POST)
public String processRegistration(
    @RequestPart("profilePicture") byte[]  profilePicture,
    @Valid Spittr spittr,
    Errors errors) {
  ...
} 
```

当注册表单提交的时候，profilePicture 属性将会给定一个 byte 数组，这个数组中包含了请求中对应 part 的数据（通过 @RequestPart 指定）。如果用户提交表单的时候没有选择文件，那么这个数组会是空（而不是 null）。获取到图片数据后，processRegistration() 方法剩下的任务就是将文件保存到某个位置。

我们将会稍后讨论如何保存文件。但首先，想一下，对于提交的图片数据我们都了解哪些信息呢。或者，更为重要的是，我们还不知道些什么呢？尽管我们已经得到了 byte 数组形式的图片数据，并且根据它能够得到图片的大小，但是对于其他内容我们就一无所知了。我们不知道文件的类型是什么，甚至不知道原始的文件名是什么。你需要判断如何将 byte 数组转换为可存储的文件。

**接受 MultipartFile**

使用上传文件的原始 byte 比较简单但是功能有限。因此，Spring 还提供了 MultipartFile 接口，它为处理 multipart 数据提供了内容更为丰富的对象。如下的程序清单展现了 MultipartFile 接口的概况。

```java
package org.springframework.web.multipart

import java.io.File;
import java.io.IOException;
import java.io.InputStream;

public interface MultipartFile {
  String getName();
  String getOriginalFilename();
  String getContentType();
  boolean isEmpty();
  long getSize();
  byte[] getBytes() throws IOException;
  InputStream getInputStream() throws IOException;
  void transferTo(File dest) throws IOException;
}
```

我们可以看到，MultipartFile 提供了获取上传文件 byte 的方式，但是它所提供的功能并不仅限于此，还能获得原始的文件名、大小以及内容类型。它还提供了一个 InputStream，用来将文件数据以流的方式进行读取。

除此之外，MultipartFile 还提供了一个便利的 transferTo() 方 法，它能够帮助我们将上传的文件写入到文件系统中。作为样例，我们可以在 process-Registration() 方法中添加如下的几行代码，从而将上传的图片文件写入到文件系统中：

```java
profilePicture.tranferTo(new File("/data/spittr/" + profilePicture.getOriginalFilename()));
```

将文件保存到本地文件系统中是非常简单的，但是这需要我们对这些文件进行管理。我们需要确保有足够的空间，确保当出现硬件故障时，文件进行了备份，还需要在集群的多个服务器之间处理这些图片文件的同步。

**将文件保存到 Amazon S3 中**

另外一种方案就是让别人来负责处理这些事情。多加几行代码，我们就能将图片保存到云端。例如，如下的程序清单所展现的 saveImage() 方法能够将上传的文件保存到 Amazon S3 中，我们在 processRegistration() 中可以调用该方法。

```java
private void saveImage(MultipartFile image) throws ImageUploadException {
  try {
    AWSCredentials awsCredentials = new AWSCredentials(s3AccessKey, s2SecretKey);
    S3Service s3 = new ResetS3Service(awsCredentials);
    
    S3Bucket bucket = s3.getBucket("spittrImages");
    S3Object imageObject = new S3Object(image.getOriginalFilename);
    
    imageObject.setDataInputStream(image.getInputStream());
    imageObject.setContentLength(image.getSize());
    imageObject.setContentType(image.getContentType());
    
    AccessControlList acl = new AccessControlList();
    acl.setOwner(bucket.getOwner());
    acl.grantPermission(GroupGrants.ALL_USERS, Permission.PERMISSION_READ);
    imageObject.setAcl(acl);
    
    s3.putObject(bucket, imageObject);
  } catch (Exception e) {
    throw new ImageUploadException("Unable to save image", e);
  }
}
```

程序清单 7.6 将 MultipartFile 保存到 Amazon S3 中 saveImage() 方法所做的第一件事就是构建 Amazon Web Service（AWS）凭证。为了完成这一点，你需要有一个 S3 Access Key 和 S3 Secret Access Key。当注册 S3 服务的时候，Amazon 会将其提供给你。它们会通过值注入的方式提供给 SpitterController。

AWS 凭证准备好后，saveImage() 方法创建了一个 JetS3t 的 RestS3Service 实例，可以通过它来操作 S3 文件系统。它获取 spitterImages bucket 的引用并创建用来包含图片的 S3Object 对象，接下来将图片数据填充到 S3Object。

在调用 putObject() 方法将图片数据写到 S3 之前，saveImage() 方法设置了 S3Object 的权限，从而允许所有的用户查看它。这是很重要的 —— 如果没有它的话，这些图片对我们应用程序的用户就是不可见的。最后，如果出现任何问题的话，将会抛出 ImageUploadException 异常。

**以 Part 的形式接受上传的文件**

如果你需要将应用部署到 Servlet 3.0 的容器中，那么会有 MultipartFile 的一个替代方案。Spring MVC 也能接受 javax.servlet.http.Part 作为控制器方法的参数。如果使用 Part 来替换 MultipartFile 的话，那么 processRegistration() 的方法签名将会变成如下的形式：

```java
@RequestMapping(value="/register", method=POST)
public String processRegistration(
    @RequestPart("profilePicture") Part profilePicture,
    @Valid Spittr spittr,
    Errors errors) {
  ...
} 
```

就主体来言（不开玩笑地说），Part 接口与 MultipartFile 并没有太大的差别。在如下的程序清单中，我们可以看到 Part 接口的有一些方法其实是与 MultipartFile 相对应的。

{% code title="程序清单 7.7　Part 接口：Spring MultipartFile 的替代方案" %}

```java
package javax.servlet.http;

import java.io.*;
import java.util.*;

public interface Part {
  InputStream getInputStream() throws IOException;
  String getContentType();
  String getName();
  String getSubmittedFileName();
  long getSize();
  void write(String fileName) throws IOException;
  void delete() throws IOException;
  String getHeader(String name);
  Collection<String> getHeaders(String name);
  Collection<String> getHeaderNames();
}
```

{% endcode %}

在很多情况下，Part 方法的名称与 MultipartFile 方法的名称是完全相同的。有一些比较类似，但是稍有差异，比如 getSubmittedFileName() 对应于 getOriginalFilename()。类似地，write() 对应于 transferTo()，借助该方法我们能够将上传的文件写入文件系统中：

```java
profilePicture.write("/data/spittr/" + profilePicture.getOriginalFilename());
```

值得一提的是，如果在编写控制器方法的时候，通过 Part 参数的形式接受文件上传，那么就没有必要配置 MultipartResolver 了。只有使用 MultipartFile 的时候，我们才需要 MultipartResolver。
