此版本仍在开发中,尚不被认为是稳定的。对于最新的稳定版本,请使用 Spring Framework 6.2.10spring-doc.cadn.net.cn

多部分内容

Multipart Data 中所述,ServerWebExchange提供对 Multipart 的访问 内容。在控制器中处理文件上传表单(例如,从浏览器)的最佳方式 是通过与命令对象的数据绑定, 如以下示例所示: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(
		val name: String,
		val file: FilePart)

@Controller
class FileUploadController {

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

}

您还可以在 RESTful 服务中提交来自非浏览器客户端的多部分请求 场景。以下示例将文件与 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") Part metadata, (1)
		@RequestPart("file-data") FilePart file): 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以获取元数据。

您可以使用@RequestPart结合使用jakarta.validation.Valid或 Spring 的@Validated注释,这会导致应用标准 Bean 验证。验证 错误导致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: MetaData): String {
	// ...
}

如果方法验证适用,因为其他参数具有@Constraint附注 然后HandlerMethodValidationException而是被提高。请参阅验证部分spring-doc.cadn.net.cn

要将所有多部分数据作为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: MultiValueMap<String, Part>): String { (1)
	// ...
}
1 @RequestBody.

PartEvent

要以流式方式按顺序访问多部分数据,您可以使用@RequestBodyFlux<PartEvent>(或Flow<PartEvent>在 Kotlin 中)。 多部分 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> 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. 请参阅多部分数据spring-doc.cadn.net.cn