Spring Boot REST API 教程,第 3 部分:上传和下载文件
在 Spring Boot REST API 教程,第 2 部分:添加 GET 和 DELETE 端点 中,API 使用照片标识符和文件名称。 它可以描述照片,但不保存图像。 在本部分中,添加一个用于上传文件的页面,将文件字节存储在服务中,并通过下载端点返回这些字节。
创建用于文件上传的 HTML 页面
要上传图像,浏览器需要一种选择文件并将其发送到 API 的方式。 此页面提供文件选择器和上传按钮,然后在警报中显示服务器响应。
在 src/main/resources/static 中,创建 upload.html ,内容如下:
<!DOCTYPE html> <html lang="en"> <head> <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。 上传端点使用同一名称在请求中查找文件。 那里的不匹配是上传看起来没有任何反应的常见原因。
将文件数据添加到模型
现有的 Photo 类包含文件名称,但没有地方保存图像。 添加字节和内容类型,例如 image/jpeg ,这样下载端点稍后就可以告诉浏览器它返回的是哪种文件。
将内容类型和文件字节的字段添加到
Photo:private String contentType; @JsonIgnore private byte[] data;照片列表只需要元数据。 包含字节会在 JSON 响应中的每张照片里添加一条很长的 base64 字符串。
@JsonIgnore会告知 Jackson JSON 转换器跳过此字段,只保留标识符、文件名称和内容类型。 字节仍会被存储,以便下载端点单独返回。为这两个新字段生成访问器:按 Alt+Insert ,选择 Getter 和 Setter ,然后选择
contentType和data。从
PhotosService中移除添加hello.jpg的构造函数:public PhotosService() { db.put("1", new Photo("1", "hello.jpg")); }实际上传会替换该示例,而该示例没有图像数据。 此外,从
Photo中移除含两个实参的构造函数:public Photo(String id, String fileName) { this.id = id; this.fileName = fileName; }保留无实参构造函数。
处理 multipart 文件上传
在
PhotosService中,添加一个根据文件数据构建并存储照片的方法: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。
在
PhotosController中,添加上传端点:@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,该方法对此进行了声明。
提高上传大小限制
默认文件大小限制为 1 MB。 如果上传更大的文件,请求会失败, 运行 工具窗口会报告 MaxUploadSizeExceededException。 在上传更大的照片之前提高限制。 此设置应位于 application.properties 中,可在其中配置应用程序行为,而无需更改 Java 编码。
打开 src/main/resources/application.properties 并开始输入
spring.servlet.multipart。 IntelliJ IDEA 会补全配置密钥并显示其描述。 设置这两个限制:spring.servlet.multipart.max-file-size=100MB spring.servlet.multipart.max-request-size=100MBmax-file-size限制每个文件;max-request-size限制整个请求,包括 multipart 开销。
添加文件下载端点
API 现在可以存储图像,但其 JSON 响应会有意省略字节。 添加一个单独的下载控制器。 此端点会返回图像字节并设置页眉,告知浏览器收到的是哪种文件以及应将其称作什么。
在 web 软件包中,创建名称为
DownloadController的类并 inject 服务:@RestController public class DownloadController { private final PhotosService photosService; public DownloadController(PhotosService photosService) { this.photosService = photosService; } }两个控制器都会接收同一个服务实例,因此它们使用相同的已存储照片。
在
DownloadController中,添加一个将照片字节作为文件下载返回的方法:@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会要求浏览器以其原始名称下载文件。
测试文件上传和下载
重新启动应用程序,打开
http://localhost:8080/upload.html,选择小于 100 MB 的图像,然后点击 上传。警报会显示标识符、文件名称和内容类型。 从警报中复制标识符。
打开
http://localhost:8080/photos。 JSON 会列出标识符、文件名称和内容类型,不包含图像字节。打开
http://localhost:8080/download/<id>,将<id>替换为复制的标识符。 浏览器会以图像的原始名称下载该图像。
请求一个不存在的标识符。 下载端点会返回 404,与 JSON 端点相同,因为两者都会询问服务,并且都以相同方式处理
null。