Multipart Content

  1. 使用命令对象和数据绑定来绑定文件上传表单数据,方便访问。

  2. 对于 RESTful 服务,使用 @RequestPart 或 @RequestBody 访问部件。

  3. 可使用 @Validated 对部件进行验证,以获取异常和错误详细信息。

  4. @RequestBody 可用于访问部件数据作为 MultiValueMap 或流式访问。

  5. 注意 isLast() 属性以识别部件事件的结束。

  6. 使用适当的方法处理表单字段和文件上传,并释放内容以避免内存泄漏。

正如 Multipart Data 中所述,ServerWebExchange 可以访问多部分内容。在控制器中处理文件上传表单(例如,来自浏览器)的最佳方式是通过数据与 command object 绑定,如下面的示例所示:

As explained in Multipart Data, ServerWebExchange provides access to multipart content. The best way to handle a file upload form (for example, from a browser) in a controller is through data binding to a command object, as the following example shows:

  • 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 及一个文件:

You can also submit multipart requests from non-browser clients in a RESTful service scenario. The following example uses a file along with 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 访问各个部分,如下面的示例所示:

You can access individual parts with @RequestPart, as the following example shows:

Java
@PostMapping("/")
public String handle(@RequestPart("meta-data") Part metadata, (1)
		@RequestPart("file-data") FilePart file) { (2)
	// ...
}
1 Using @RequestPart to get the metadata.
2 Using @RequestPart to get the file.
Kotlin
@PostMapping("/")
fun handle(@RequestPart("meta-data") Part metadata, (1)
		@RequestPart("file-data") FilePart file): String { (2)
	// ...
}
3 Using @RequestPart to get the metadata.
4 Using @RequestPart to get the file.

若要反序列化原始部分内容(例如,反序列化为 JSON — 类似于 @RequestBody),可以声明一个具体目标 Object 而不是 Part,如下面的示例所示:

To deserialize the raw part content (for example, to JSON — similar to @RequestBody), you can declare a concrete target Object, instead of Part, as the following example shows:

Java
@PostMapping("/")
public String handle(@RequestPart("meta-data") MetaData metadata) { (1)
	// ...
}
1 Using @RequestPart to get the metadata.
Kotlin
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: MetaData): String { (1)
	// ...
}
2 Using @RequestPart to get the metadata.

您可以将 @RequestPartjakarta.validation.Valid 或 Spring 的 @Validated 注解结合使用,这会导致应用标准 Bean 验证。验证错误会导致 WebExchangeBindException,从而导致 400 (BAD_REQUEST) 响应。异常包含一个包含错误详细信息的 BindingResult,还可以在控制器方法中处理,方法是用异步包装器声明参数,然后使用与错误相关的运算符:

You can use @RequestPart in combination with jakarta.validation.Valid or Spring’s @Validated annotation, which causes Standard Bean Validation to be applied. Validation errors lead to a WebExchangeBindException that results in a 400 (BAD_REQUEST) response. The exception contains a BindingResult with the error details and can also be handled in the controller method by declaring the argument with an async wrapper and then using error related operators:

  • 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。请参阅 Validation 部分。

If method validation applies because other parameters have @Constraint annotations, then HandlerMethodValidationException is raised instead. See the section on Validation.

要访问作为 MultiValueMap 的所有多部分数据,可以使用 @RequestBody,如下例所示:

To access all multipart data as a MultiValueMap, you can use @RequestBody, as the following example shows:

Java
@PostMapping("/")
public String handle(@RequestBody Mono<MultiValueMap<String, Part>> parts) { (1)
	// ...
}
1 Using @RequestBody.
Kotlin
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { (1)
	// ...
}
2 Using @RequestBody.

PartEvent

要以流方式顺序访问多部分数据,可以使用带有 Flux<PartEvent>(或 Kotlin 中的 Flow<PartEvent>)的 @RequestBody。多部分 HTTP 消息中的每个部分都将产生至少一个 PartEvent,其中包含标题和一个缓冲区,其中包含该部分的内容。

To access multipart data sequentially, in a streaming fashion, you can use @RequestBody with Flux<PartEvent> (or Flow<PartEvent> in Kotlin). Each part in a multipart HTTP message will produce at least one PartEvent containing both headers and a buffer with the contents of the part.

  • Form fields will produce a single FormPartEvent, containing the value of the field.

  • File uploads will produce one or more FilePartEvent objects, containing the filename used when uploading. If the file is large enough to be split across multiple buffers, the first FilePartEvent will be followed by subsequent events.

例如:

For example:

Java
@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 Using @RequestBody.
2 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.
3 The Flux::switchOnFirst operator allows you to see whether you are handling a form field or file upload.
4 Handling the form field.
5 Handling the file upload.
6 The body contents must be completely consumed, relayed, or released to avoid memory leaks.
Kotlin
	@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
                  }
              }
          }
}
7 Using @RequestBody.
8 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.
9 The Flux::switchOnFirst operator allows you to see whether you are handling a form field or file upload.
10 Handling the form field.
11 Handling the file upload.
12 The body contents must be completely consumed, relayed, or released to avoid memory leaks.

接收到的部分事件还可以通过使用 WebClient 传递到另一个服务。请参见 Multipart Data

Received part events can also be relayed to another service by using the WebClient. See Multipart Data.