Multipart 内容
如 Multipart 数据 中所述,ServerWebExchange
提供对 multipart
内容的访问。在控制器中处理文件上传表单(例如,来自浏览器)的最佳方式
是通过数据绑定到 命令对象,
如以下示例所示:
-
Java
-
Kotlin
class MyForm {
private String name;
private FilePart file;
// ...
}
@Controller
public class FileUploadController {
@PostMapping("/form")
public String handleFormUpload(MyForm form, BindingResult errors) {
// ...
}
}
class MyForm(
val name: String,
val file: FilePart)
@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
… 文件数据 …
你可以使用 @RequestPart
访问单个部分,如以下示例所示:
- Java
-
@PostMapping("/") public String handle(@RequestPart("meta-data") Part metadata, [id="CO1-1"][id="CO1-1"][id="CO1-1"](1) @RequestPart("file-data") FilePart file) { [id="CO1-2"][id="CO1-2"][id="CO1-2"](2) // ... }
<1> 使用 `@RequestPart` 获取元数据。 <1> 使用 `@RequestPart` 获取文件。
- Kotlin
-
@PostMapping("/") fun handle(@RequestPart("meta-data") Part metadata, [id="CO2-1"][id="CO1-3"][id="CO2-1"](1) @RequestPart("file-data") FilePart file): String { [id="CO2-2"][id="CO1-4"][id="CO2-2"](2) // ... }
<1> 使用 `@RequestPart` 获取元数据。 <1> 使用 `@RequestPart` 获取文件。
要反序列化原始部分内容(例如,到 JSON——类似于 @RequestBody
),
你可以声明一个具体的 Object
目标,而不是 Part
,如以下示例所示:
- Java
-
@PostMapping("/") public String handle(@RequestPart("meta-data") MetaData metadata) { [id="CO3-1"][id="CO1-5"][id="CO3-1"](1) // ... }
<1> 使用 `@RequestPart` 获取元数据。
- Kotlin
-
@PostMapping("/") fun handle(@RequestPart("meta-data") metadata: MetaData): String { [id="CO4-1"][id="CO1-6"][id="CO4-1"](1) // ... }
<1> 使用 `@RequestPart` 获取元数据。
你可以将 @RequestPart
与 jakarta.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
。请参阅
验证 部分。
要将所有 multipart 数据作为 MultiValueMap
访问,你可以使用 @RequestBody
,
如以下示例所示:
- Java
-
@PostMapping("/") public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { [id="CO5-1"][id="CO1-7"][id="CO5-1"](1) // ... }
<1> 使用 `@RequestBody`。
- Kotlin
-
@PostMapping("/") fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { [id="CO6-1"][id="CO1-8"][id="CO6-1"](1) // ... }
<1> 使用 `@RequestBody`。
PartEvent
要以流式方式按顺序访问 multipart 数据,你可以使用 @RequestBody
和
Flux<PartEvent>
(或 Kotlin 中的 Flow<PartEvent>
)。
multipart HTTP 消息中的每个部分将生成
至少一个 PartEvent
,其中包含头和包含部分内容的缓冲区。
-
表单字段将生成一个 单个
FormPartEvent
,其中包含字段的值。 -
文件上传将生成 一个或多个
FilePartEvent
对象,其中包含上传时使用的文件名。 如果文件足够大,需要拆分成多个缓冲区,则第一个FilePartEvent
之后将跟随后续事件。
例如:
-
Java
-
Kotlin
@PostMapping("/")
public void handle(@RequestBody Flux<PartEvent> allPartsEvents) { [id="CO7-1"]1
allPartsEvents.windowUntil(PartEvent::isLast) [id="CO7-2"]2
.concatMap(p -> p.switchOnFirst((signal, partEvents) -> { [id="CO7-3"]3
if (signal.hasValue()) {
PartEvent event = signal.get();
if (event instanceof FormPartEvent formEvent) { [id="CO7-4"]4
String value = formEvent.value();
// handle form field
}
else if (event instanceof FilePartEvent fileEvent) { [id="CO7-5"]5
String filename = fileEvent.filename();
Flux<DataBuffer> contents = partEvents.map(PartEvent::content); [id="CO7-6"]6
// handle file upload
}
else {
return Mono.error(new RuntimeException("Unexpected event: " + event));
}
}
else {
return partEvents; // either complete or error signal
}
}));
}
<1> 使用 @RequestBody
。
<1> 特定部分的最终 PartEvent
将 isLast()
设置为 true
,并且可以
后跟属于后续部分的额外事件。
这使得 isLast
属性适合作为 Flux::windowUntil
操作符的谓词,以
将所有部分的事件拆分为每个属于单个部分的窗口。
<1> Flux::switchOnFirst
操作符允许你查看你正在处理的是表单字段还是
文件上传。
<1> 处理表单字段。
<1> 处理文件上传。
<1> 必须完全消费、转发或释放主体内容,以避免内存泄漏。
@PostMapping("/")
fun handle(@RequestBody allPartsEvents: Flux<PartEvent>) = { [id="CO8-1"][id="CO1-9"][id="CO7-1"](1)
allPartsEvents.windowUntil(PartEvent::isLast) [id="CO8-2"]2
.concatMap {
it.switchOnFirst { signal, partEvents -> [id="CO8-3"]3
if (signal.hasValue()) {
val event = signal.get()
if (event is FormPartEvent) { [id="CO8-4"]4
val value: String = event.value();
// handle form field
} else if (event is FilePartEvent) { [id="CO8-5"]5
val filename: String = event.filename();
val contents: Flux<DataBuffer> = partEvents.map(PartEvent::content); [id="CO8-6"]6
// handle file upload
} else {
return Mono.error(RuntimeException("Unexpected event: " + event));
}
} else {
return partEvents; // either complete or error signal
}
}
}
}
<1> 使用 @RequestBody
。
<1> 特定部分的最终 PartEvent
将 isLast()
设置为 true
,并且可以
后跟属于后续部分的额外事件。
这使得 isLast
属性适合作为 Flux::windowUntil
操作符的谓词,以
将所有部分的事件拆分为每个属于单个部分的窗口。
<1> Flux::switchOnFirst
操作符允许你查看你正在处理的是表单字段还是
文件上传。
<1> 处理表单字段。
<1> 处理文件上传。
<1> 必须完全消费、转发或释放主体内容,以避免内存泄漏。
收到的部分事件也可以使用 WebClient
转发到另一个服务。
请参阅 Multipart 数据。