多部分表单内容

多部分数据(Multipart Data)中所述,ServerWebExchange 提供了对 multipart 内容的访问。在控制器中处理文件上传表单(例如,来自浏览器的表单)的最佳方式是通过数据绑定到一个命令对象(command object),如下例所示:spring-doc.cadn.net.cn

class MyForm {

	private String name;

	private FilePart file;

	// ...

}

@Controller
public class FileUploadController {

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

}
class MyForm(
		private val name: String,
		private val file: FilePart)

@Controller
class FileUploadController {

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

}

你也可以在 RESTful 服务场景中从非浏览器客户端提交 multipart 请求。以下示例使用了一个文件以及 JSON:spring-doc.cadn.net.cn

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 访问各个部分,如下例所示:spring-doc.cadn.net.cn

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

要反序列化原始部分的内容(例如,将其转换为 JSON,类似于 @RequestBody), 您可以声明一个具体的目标准 Object,而不是 Part,如下例所示:spring-doc.cadn.net.cn

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

你可以将 @RequestPartjakarta.validation.Valid 或 Spring 的 @Validated 注解结合使用,从而触发标准的 Bean Validation 验证。验证错误会引发一个 WebExchangeBindException,导致返回 400(BAD_REQUEST)响应。该异常包含一个带有错误详情的 BindingResult,也可以通过在控制器方法中声明一个异步包装器类型的参数,然后使用与错误相关的操作符来处理该异常:spring-doc.cadn.net.cn

@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: Mono<MetaData>): String {
	// use one of the onError* operators...
}

如果由于其他参数带有 @Constraint 注解而应用了方法验证, 则会抛出 HandlerMethodValidationException 异常。请参阅 验证 章节。spring-doc.cadn.net.cn

要将所有多部分(multipart)数据作为 MultiValueMap 访问,您可以使用 @RequestBody,如下例所示:spring-doc.cadn.net.cn

@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { (1)
	// ...
}
1 使用 @RequestBody
@PostMapping("/")
fun handle(@RequestBody parts: Mono<MultiValueMap<String, Part>>): String { (1)
	// ...
}
1 使用 @RequestBody

PartEvent

要以流式方式顺序访问多部分(multipart)数据,您可以将 @RequestBodyFlux<PartEvent>(在 Kotlin 中为 Flow<PartEvent>)一起使用。 HTTP 多部分消息中的每个部分都会生成至少一个 PartEvent,其中包含该部分的头部信息和内容缓冲区。spring-doc.cadn.net.cn

  • 表单字段将生成一个单独的FormPartEvent,其中包含该字段的值。spring-doc.cadn.net.cn

  • 文件上传将生成一个或多个FilePartEvent对象,其中包含上传时使用的文件名。如果文件足够大,需要拆分到多个缓冲区中,则第一个FilePartEvent之后将跟随后续的事件。spring-doc.cadn.net.cn

@PostMapping("/")
public void handle(@RequestBody Flux<PartEvent> allPartEvents) {

	//	The final PartEvent for a particular part will have isLast() set to true, and can be
	//	followed by additional events belonging to subsequent parts.
	//	This makes the isLast property suitable as a predicate for the Flux::windowUntil operator, to
	//	split events from all parts into windows that each belong to a single part.
	allPartEvents.windowUntil(PartEvent::isLast)
			//	The Flux::switchOnFirst operator allows you to see whether you are handling
			//	a form field or file upload
			.concatMap(p -> p.switchOnFirst((signal, partEvents) -> {
					if (signal.hasValue()) {
							PartEvent event = signal.get();
							if (event instanceof FormPartEvent formEvent) {
									String value = formEvent.value();
									// Handling of the form field
							}
							else if (event instanceof FilePartEvent fileEvent) {
									String filename = fileEvent.filename();

									// The body contents must be completely consumed, relayed, or released to avoid memory leaks
									Flux<DataBuffer> contents = partEvents.map(PartEvent::content);
									// Handling of the file upload
					}
					else {
						return Mono.error(new RuntimeException("Unexpected event: " + event));
					}
				}
				else {
					return partEvents; // either complete or error signal
				}
				return Mono.empty();
			}));
}
@PostMapping("/")
fun handle(@RequestBody allPartEvents: Flux<PartEvent>) {

	//	The final PartEvent for a particular part will have isLast() set to true, and can be
	//	followed by additional events belonging to subsequent parts.
	//	This makes the isLast property suitable as a predicate for the Flux::windowUntil operator, to
	//	split events from all parts into windows that each belong to a single part.
	allPartEvents.windowUntil(PartEvent::isLast)
			.concatMap {

				//	The Flux::switchOnFirst operator allows you to see whether you are handling
				//	a form field or file upload
				it.switchOnFirst { signal, partEvents ->
					if (signal.hasValue()) {
						val event = signal.get()
						if (event is FormPartEvent) {
							val value: String = event.value()
							// Handling of the form field
						} else if (event is FilePartEvent) {
							val filename: String = event.filename()

							// The body contents must be completely consumed, relayed, or released to avoid memory leaks
							val contents: Flux<DataBuffer> = partEvents.map(PartEvent::content)
							// Handling of the file upload
						} else {
							return@switchOnFirst Mono.error(RuntimeException("Unexpected event: $event"))
						}
					} else {
						return@switchOnFirst partEvents // either complete or error signal
					}
					Mono.empty()
				}
			}
}

接收到的部件事件也可以通过使用 WebClient 转发到另一个服务。 参见多部分数据spring-doc.cadn.net.cn