Multipart Content

  1. 使用命令对象和数据绑定来绑定文件上传表单数据,方便访问。

  2. 对于 RESTful 服务,使用 @RequestPart 或 @RequestBody 访问部件。

  3. 可使用 @Validated 对部件进行验证,以获取异常和错误详细信息。

  4. @RequestBody 可用于访问部件数据作为 MultiValueMap 或流式访问。

  5. 注意 isLast() 属性以识别部件事件的结束。

  6. 使用适当的方法处理表单字段和文件上传,并释放内容以避免内存泄漏。

正如 Multipart Data 中所述,ServerWebExchange 可以访问多部分内容。在控制器中处理文件上传表单(例如,来自浏览器)的最佳方式是通过数据与 command object 绑定,如下面的示例所示:

  • Java

  • Kotlin

class MyForm {

	private String name;

	private MultipartFile file;

	// ...

}

@Controller
public class FileUploadController {

	@PostMapping("/form")
	public String handleFormUpload(MyForm form, BindingResult errors) {
		// ...
	}

}
class MyForm(
		val name: String,
		val file: MultipartFile)

@Controller
class FileUploadController {

	@PostMapping("/form")
	fun handleFormUpload(form: MyForm, errors: BindingResult): String {
		// ...
	}

}

您还可以在 RESTful 服务场景中从非浏览器客户端提交多部分请求。以下示例使用 JSON 及一个文件:

POST /someUrl Content-Type: multipart/mixed

--edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp Content-Disposition: form-data; name="meta-data" Content-Type: application/json; charset=UTF-8 Content-Transfer-Encoding: 8bit

{ "name": "value" } --edt7Tfrdusa7r3lNQc79vXuhIIMlatb7PQg7Vp Content-Disposition: form-data; name="file-data"; filename="file.properties" Content-Type: text/xml Content-Transfer-Encoding: 8bit …​ File Data …​ 您可以使用 @RequestPart 访问各个部分,如下面的示例所示:

Java
@PostMapping("/")
public String handle(@RequestPart("meta-data") Part metadata, (1)
		@RequestPart("file-data") FilePart file) { (2)
	// ...
}
1 使用 `@RequestPart`获取元数据。
2 使用 `@RequestPart`获取文件。
Kotlin
@PostMapping("/")
fun handle(@RequestPart("meta-data") Part metadata, (1)
		@RequestPart("file-data") FilePart file): String { (2)
	// ...
}
3 使用 `@RequestPart`获取元数据。
4 使用 `@RequestPart`获取文件。

若要反序列化原始部分内容(例如,反序列化为 JSON — 类似于 @RequestBody),可以声明一个具体目标 Object 而不是 Part,如下面的示例所示:

Java
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { (1)
	// ...
}
1 使用 `@RequestPart`获取元数据。
Kotlin
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData): String { (1)
	// ...
}
2 使用 `@RequestPart`获取元数据。

您可以将 @RequestPartjakarta.validation.Valid 或 Spring 的 @Validated 注解结合使用,这会导致应用标准 Bean 验证。验证错误会导致 WebExchangeBindException,从而导致 400 (BAD_REQUEST) 响应。异常包含一个包含错误详细信息的 BindingResult,还可以在控制器方法中处理,方法是用异步包装器声明参数,然后使用与错误相关的运算符:

  • Java

  • Kotlin

@PostMapping("/")
public String handle(@Valid @RequestPart("meta-data") Mono<MetaData> metadata) {
	// use one of the onError* operators...
}
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData): String {
	// ...
}

如果方法验证适用,因为其他参数有 @Constraint 注解,那么就引发 HandlerMethodValidationException。请参阅 Validation 部分。 要访问作为 MultiValueMap 的所有多部分数据,可以使用 @RequestBody,如下例所示:

Java
@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { (1)
	// ...
}
1 Using @RequestBody.
Kotlin
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { (1)
	// ...
}
2 Using @RequestBody.

PartEvent

要以流方式顺序访问多部分数据,可以使用带有 Flux<PartEvent>(或 Kotlin 中的 Flow<PartEvent>)的 @RequestBody。多部分 HTTP 消息中的每个部分都将产生至少一个 PartEvent,其中包含标题和一个缓冲区,其中包含该部分的内容。

  • 表单域将生成一个包含域值的*single*FormPartEvent

  • 文件上传将生成 one or more`FilePartEvent`对象,其中包含用于上传的文件名。如果文件大到足以分割到多个缓冲区中,第一个`FilePartEvent`后面会跟着后续事件。

例如:

Java
@PostMapping("/")
public void handle(@RequestBody Flux<PartEvent> allPartsEvents) { 1
    allPartsEvents.windowUntil(PartEvent::isLast) 2
            .concatMap(p -> p.switchOnFirst((signal, partEvents) -> { 3
                if (signal.hasValue()) {
                    PartEvent event = signal.get();
                    if (event instanceof FormPartEvent formEvent) { 4
                        String value = formEvent.value();
                        // handle form field
                    }
                    else if (event instanceof FilePartEvent fileEvent) { 5
                        String filename = fileEvent.filename();
                        Flux<DataBuffer> contents = partEvents.map(PartEvent::content); 6
                        // handle file upload
                    }
                    else {
                        return Mono.error(new RuntimeException("Unexpected event: " + event));
                    }
                }
                else {
                    return partEvents; // either complete or error signal
                }
            }));
}
1 Using @RequestBody.
2 某个特定部分的最终`PartEvent`将会有`isLast()设置为`true,并且可能会有后续部分的其他事件紧随其后。这使得`isLast`属性适用于`Flux::windowUntil`运算符,以将所有部分的事件拆分为各自属于一个部分的窗口。
3 `Flux::switchOnFirst`操作符能够让你了解你是在处理表单字段还是文件上传。
4 Handling the form field.
5 Handling the file upload.
6 主体内容必须被完全消费、中继或释放,以避免内存泄漏。
Kotlin
	@PostMapping("/")
	fun handle(@RequestBody allPartsEvents: Flux<PartEvent>) = { (1)
      allPartsEvents.windowUntil(PartEvent::isLast) 2
          .concatMap {
              it.switchOnFirst { signal, partEvents -> 3
                  if (signal.hasValue()) {
                      val event = signal.get()
                      if (event is FormPartEvent) { 4
                          val value: String = event.value();
                          // handle form field
                      } else if (event is FilePartEvent) { 5
                          val filename: String = event.filename();
                          val contents: Flux<DataBuffer> = partEvents.map(PartEvent::content); 6
                          // handle file upload
                      } else {
                          return Mono.error(RuntimeException("Unexpected event: " + event));
                      }
                  } else {
                      return partEvents; // either complete or error signal
                  }
              }
          }
}
7 Using @RequestBody.
8 某个特定部分的最终`PartEvent`将会有`isLast()设置为`true,并且可能会有后续部分的其他事件紧随其后。这使得`isLast`属性适用于`Flux::windowUntil`运算符,以将所有部分的事件拆分为各自属于一个部分的窗口。
9 `Flux::switchOnFirst`操作符能够让你了解你是在处理表单字段还是文件上传。
10 Handling the form field.
11 Handling the file upload.
12 主体内容必须被完全消费、中继或释放,以避免内存泄漏。

接收到的部分事件还可以通过使用 WebClient 传递到另一个服务。请参见 Multipart Data