# Spring Boot REST API 教程，第 3 部分：上传和下载文件

> For the complete documentation index, see [llms.txt](https://www.jetbrains.com.cn/help/idea/llms.txt).

* [1：创建并运行 Spring Boot 项目](spring-boot-tutorial-create-project.html)

* [2：添加 REST 端点](spring-boot-tutorial-rest-endpoints.html)

* [3：上传和下载文件]()

* [4：在 H2 中存储数据并创建 JAR 软件包](spring-boot-tutorial-h2-database-jar.html)

在 [Spring Boot REST API 教程，第 2 部分：添加 GET 和 DELETE 端点](spring-boot-tutorial-rest-endpoints.html) 中，API 使用照片标识符和文件名称。 它可以描述照片，但不保存图像。 在本部分中，添加一个用于上传文件的页面，将文件字节存储在服务中，并通过下载端点返回这些字节。

Procedure: 创建用于文件上传的 HTML 页面

要上传图像，浏览器需要一种选择文件并将其发送到 API 的方式。 此页面提供文件选择器和上传按钮，然后在警报中显示服务器响应。

* 在  `src/main/resources/static`  中，创建  `upload.html` ，内容如下：

```HTML
<!DOCTYPE html>
<html lang="en">
<head><link rel="canonical" href="https://www.jetbrains.com.cn/help/idea/spring-boot-tutorial-file-upload-download.html.md/" data-react-helmet="true"/>
    <meta charset="UTF-8">
    <title>Photo Upload</title>
</head>
<body>

<h2>Photo Upload</h2>

<input id="fileupload" type="file" name="fileupload"/>
<button id="upload-button" onclick="uploadFile()">Upload</button>

<script>
    async function uploadFile() {
        let formData = new FormData();
        formData.append("data", fileupload.files[0]);
        await fetch("http://localhost:8080/photos", {
            method: "POST",
            body: formData
        }).then(result => result.text())
            .then(text => alert(text));
    }
</script>
</body>
</html>

```

Spring Boot 会直接提供此目录中的文件，因此无需控制器即可通过  `/upload.html`  访问该页面。

`FormData`  会让浏览器发送 multipart 请求，该请求会同时携带文件字节、文件名称和内容类型。 每个部分都有一个名称：这里是  `data` 。 上传端点使用同一名称在请求中查找文件。 那里的不匹配是上传看起来没有任何反应的常见原因。

Procedure: 将文件数据添加到模型

现有的  `Photo`  类包含文件名称，但没有地方保存图像。 添加字节和内容类型，例如  `image/jpeg` ，这样下载端点稍后就可以告诉浏览器它返回的是哪种文件。

1. 将内容类型和文件字节的字段添加到  `Photo` ：

```JAVA
private String contentType;

@JsonIgnore
private byte[] data;

```

照片列表只需要元数据。 包含字节会在 JSON 响应中的每张照片里添加一条很长的 base64 字符串。  `@JsonIgnore`  会告知 Jackson JSON 转换器跳过此字段，只保留标识符、文件名称和内容类型。 字节仍会被存储，以便下载端点单独返回。

2. 为这两个新字段生成访问器：按 `Alt+Insert` (Windows), `⌘ N` (macOS), `⌃ N` (IntelliJ IDEA Classic (macOS)), `⌘ N` (macOS System Shortcuts), `Alt+Insert` (XWin), `Alt+Insert` (GNOME), `Alt+Insert` (KDE), `Alt+Insert` (Emacs), `Alt+Insert` (Sublime Text), `⌘ N` (Sublime Text (macOS)), `Alt+Insert` (Visual Studio), `⌘ ⌃ N` (Visual Studio (macOS)), `Alt+Insert` (NetBeans), `Alt+Insert` (Eclipse), `⌘ N` (Eclipse (macOS))，选择   Getter 和 Setter  ，然后选择  `contentType`  和  `data` 。

3. 从  `PhotosService`  中移除添加  `hello.jpg`  的构造函数：

```JAVA
public PhotosService() {
    db.put("1", new Photo("1", "hello.jpg"));
}

```

实际上传会替换该示例，而该示例没有图像数据。 此外，从  `Photo`  中移除含两个实参的构造函数：

```JAVA
public Photo(String id, String fileName) {
    this.id = id;
    this.fileName = fileName;
}

```

保留无实参构造函数。

Procedure: 处理 multipart 文件上传

1. 在  `PhotosService`  中，添加一个根据文件数据构建并存储照片的方法：

```JAVA
public Photo save(String fileName, String contentType, byte[] data) {
    Photo photo = new Photo();
    photo.setId(UUID.randomUUID().toString());
    photo.setFileName(fileName);
    photo.setContentType(contentType);
    photo.setData(data);
    db.put(photo.getId(), photo);
    return photo;
}

```

服务器会分配一个 UUID 来标识照片，并返回存储的对象，以便客户端接收该标识符用于后续请求。 照片会保留在内存中，直到应用程序停止；第 4 部分会用数据库替换该 map。

2. 在  `PhotosController`  中，添加上传端点：

```JAVA
@PostMapping("/photos")
public Photo create(@RequestPart("data") MultipartFile file) throws IOException {
    return photosService.save(file.getOriginalFilename(), file.getContentType(), file.getBytes());
}

```

`@RequestPart("data")`  会选择页面发送的文件。  `MultipartFile`  会提供其原始名称、内容类型和字节。

控制器会提取这些值，因此服务不依赖于 HTTP 特定的  `MultipartFile`  类型。 同一个服务方法可以从其他来源接受文件名称、内容类型和字节，而无需 HTTP 请求。 读取字节可能会抛出  `IOException` ，该方法对此进行了声明。

Procedure: 提高上传大小限制

默认文件大小限制为 1 MB。 如果上传更大的文件，请求会失败， [运行](run-tool-window.html) 工具窗口会报告  `MaxUploadSizeExceededException` 。 在上传更大的照片之前提高限制。 此设置应位于  `application.properties`  中，可在其中配置应用程序行为，而无需更改 Java 编码。

* 打开  `src/main/resources/application.properties`  并开始输入  `spring.servlet.multipart` 。 IntelliJ IDEA 会补全配置密钥并显示其描述。 设置这两个限制：

```PROPERTIES
spring.servlet.multipart.max-file-size=100MB
spring.servlet.multipart.max-request-size=100MB

```

`max-file-size`  限制每个文件；  `max-request-size`  限制整个请求，包括 multipart 开销。

Procedure: 添加文件下载端点

API 现在可以存储图像，但其 JSON 响应会有意省略字节。 添加一个单独的下载控制器。 此端点会返回图像字节并设置页眉，告知浏览器收到的是哪种文件以及应将其称作什么。

1. 在  `web`  软件包中，创建名称为  `DownloadController`  的类并 inject 服务：

```JAVA
@RestController
public class DownloadController {

    private final PhotosService photosService;

    public DownloadController(PhotosService photosService) {
        this.photosService = photosService;
    }

}

```

两个控制器都会接收同一个服务实例，因此它们使用相同的已存储照片。

2. 在  `DownloadController`  中，添加一个将照片字节作为文件下载返回的方法：

```JAVA
@GetMapping("/download/{id}")
public ResponseEntity<byte[]> download(@PathVariable String id) {
    Photo photo = photosService.get(id);
    if (photo == null) {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND);
    }

    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.valueOf(photo.getContentType()));
    headers.setContentDisposition(
            ContentDisposition.builder("attachment")
                    .filename(photo.getFileName())
                    .build());

    return new ResponseEntity<>(photo.getData(), headers, HttpStatus.OK);
}

```

`ResponseEntity`  会组合字节、页眉和状态。  `Content-Type`  会标识文件格式，而  `Content-Disposition`  会要求浏览器以其原始名称下载文件。

Procedure: 测试文件上传和下载

1. 重新启动应用程序，打开  `http://localhost:8080/upload.html` ，选择小于 100 MB 的图像，然后点击   上传  。

警报会显示标识符、文件名称和内容类型。 从警报中复制标识符。

2. 打开  `http://localhost:8080/photos` 。 JSON 会列出标识符、文件名称和内容类型，不包含图像字节。

3. 打开  `http://localhost:8080/download/<id>` ，将  `<id>`  替换为复制的标识符。 浏览器会以图像的原始名称下载该图像。

![浏览器下载历史记录显示 ij.png，在使用照片标识符打开 /download 端点后，该文件已按其原始名称保存。](https://resources.jetbrains.com.cn/help/img/idea/2026.3/sb_downloaded_image.png)

4. 请求一个不存在的标识符。 下载端点会返回 404，与 JSON 端点相同，因为两者都会询问服务，并且都以相同方式处理  `null` 。

[下一部分](spring-boot-tutorial-h2-database-jar.html)

