多部分内容
正如在 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 服务场景中从非浏览器客户端提交多部分请求。以下示例结合了文件和 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
// ...
}
使用
@RequestPart
获取元数据。使用
@RequestPart
获取文件。
@PostMapping("/")
fun handle(@RequestPart("meta-data") Part metadata, 1
@RequestPart("file-data") FilePart file): String { 2
// ...
}
使用
@RequestPart
获取元数据。使用
@RequestPart
获取文件。
要将原始部分内容反序列化(例如,反序列化为 JSON — 类似于 @RequestBody
),你可以声明一个具体的目标 Object
,而不是 Part
,如下例所示:
- Java
- Kotlin
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { 1
// ...
}
使用
@RequestPart
获取元数据。
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData): String { 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
异常。请参阅 验证 部分。
要访问所有的多部分数据作为 MultiValueMap
,你可以使用 @RequestBody
,如下例所示:
- Java
- Kotlin
@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { 1
// ...
}
使用
@RequestBody
。
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { 1
// ...
}
使用
@RequestBody
。
PartEvent
要以流式方式顺序访问多部分数据,可以使用 @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();
// 处理表单字段
}
else if (event instanceof FilePartEvent fileEvent) { 5
String filename = fileEvent.filename();
Flux<DataBuffer> contents = partEvents.map(PartEvent::content); 6
// 处理文件上传
}
else {
return Mono.error(new RuntimeException("意外事件: " + event));
}
}
else {
return partEvents; // 完成或错误信号
}
}));
}
使用
@RequestBody
。特定部分的最后一个
PartEvent
会将isLast()
设置为true
,并且可能会跟随属于后续部分的其他事件。这使得isLast
属性适合作为Flux::windowUntil
操作符的谓词,将来自所有部分的事件拆分为每个属于单个部分的窗口。Flux::switchOnFirst
操作符允许您查看是处理表单字段还是文件上传。处理表单字段。
处理文件上传。
必须完全消费、中继或释放主体内容,以避免内存泄漏。
@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();
// 处理表单字段
} else if (event is FilePartEvent) { 5
val filename: String = event.filename();
val contents: Flux<DataBuffer> = partEvents.map(PartEvent::content); 6
// 处理文件上传
} else {
return Mono.error(RuntimeException("意外事件: " + event));
}
} else {
return partEvents; // 完成或错误信号
}
}
}
}
使用
@RequestBody
。特定部分的最后一个
PartEvent
会将isLast()
设置为true
,并且可能会跟随属于后续部分的其他事件。这使得isLast
属性适合作为Flux::windowUntil
操作符的谓词,将来自所有部分的事件拆分为每个属于单个部分的窗口。Flux::switchOnFirst
操作符允许您查看是处理表单字段还是文件上传。处理表单字段。
处理文件上传。
必须完全消费、中继或释放主体内容,以避免内存泄漏。
接收到的部分事件也可以通过使用 WebClient
中继到另一个服务。请参阅 Multipart 数据。