|
对于最新稳定版本,请使用 Spring Framework 7.0.6! |
多部分表单内容
如多部分数据(Multipart Data)中所述,ServerWebExchange 提供了对 multipart 内容的访问。在控制器中处理文件上传表单(例如,来自浏览器的表单)的最佳方式是通过数据绑定到一个命令对象(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 服务场景中从非浏览器客户端提交 multipart 请求。以下示例使用了一个文件以及 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
-
Kotlin
@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") Part metadata, (1)
@RequestPart("file-data") FilePart file): String { (2)
// ...
}
| 1 | 使用 @RequestPart 获取元数据。 |
| 2 | 使用 @RequestPart 获取文件。 |
要反序列化原始部分的内容(例如,将其转换为 JSON,类似于 @RequestBody),
您可以声明一个具体的目标准 Object,而不是 Part,如下例所示:
-
Java
-
Kotlin
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { (1)
// ...
}
| 1 | 使用 @RequestPart 获取元数据。 |
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData): String { (1)
// ...
}
| 1 | 使用 @RequestPart 获取元数据。 |
你可以将 @RequestPart 与 jakarta.validation.Valid 或 Spring 的 @Validated 注解结合使用,从而触发标准的 Bean Validation 验证。验证错误会引发一个 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 {
// ...
}
要将所有多部分(multipart)数据作为 MultiValueMap 访问,您可以使用 @RequestBody,如下例所示:
-
Java
-
Kotlin
@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { (1)
// ...
}
| 1 | 使用 @RequestBody。 |
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { (1)
// ...
}
| 1 | 使用 @RequestBody。 |
PartEvent
要以流式方式顺序访问多部分(multipart)数据,您可以将 @RequestBody 与 Flux<PartEvent>(在 Kotlin 中为 Flow<PartEvent>)一起使用。
HTTP 多部分消息中的每个部分都会生成至少一个 PartEvent,其中包含该部分的头部信息和内容缓冲区。
-
表单字段将生成一个单独的
FormPartEvent,其中包含该字段的值。 -
文件上传将生成一个或多个
FilePartEvent对象,其中包含上传时使用的文件名。如果文件足够大,需要拆分到多个缓冲区中,则第一个FilePartEvent之后将跟随后续的事件。
例如:
-
Java
-
Kotlin
@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 | 使用 @RequestBody。 |
| 2 | 某个特定部分的最后一个 PartEvent 的 isLast() 方法将返回 true,其后可能跟随属于后续部分的其他事件。
因此,isLast 属性适合作为 Flux::windowUntil 操作符的谓词,
用于将所有部分的事件拆分为多个窗口,每个窗口仅包含单个部分的事件。 |
| 3 | Flux::switchOnFirst 操作符允许你判断正在处理的是表单字段还是文件上传。 |
| 4 | 处理表单字段。 |
| 5 | 处理文件上传。 |
| 6 | 必须完全消费、转发或释放请求体内容,以避免内存泄漏。 |
@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
}
}
}
}
| 1 | 使用 @RequestBody。 |
| 2 | 某个特定部分的最后一个 PartEvent 的 isLast() 方法将返回 true,其后可能跟随属于后续部分的其他事件。
因此,isLast 属性适合作为 Flux::windowUntil 操作符的谓词,
用于将所有部分的事件拆分为多个窗口,每个窗口仅包含单个部分的事件。 |
| 3 | Flux::switchOnFirst 操作符允许你判断正在处理的是表单字段还是文件上传。 |
| 4 | 处理表单字段。 |
| 5 | 处理文件上传。 |
| 6 | 必须完全消费、转发或释放请求体内容,以避免内存泄漏。 |
接收到的部件事件也可以通过使用 WebClient 转发到另一个服务。
参见多部分数据。