# 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。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://potoyang.gitbook.io/spring-in-action-v4/di-7-zhang-spring-mvc-de-gao-ji-ji-shu/untitled-4/untitled.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
