多部分內容

多部分資料中所述,ServerWebExchange 提供多部分內容的存取權。在控制器中處理檔案上傳表單(例如,來自瀏覽器)的最佳方式是透過資料繫結至命令物件,如下列範例所示

  • 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)
	// ...
}
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 取得中繼資料。

您可以將 @RequestPartjakarta.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)
	// ...
}
1 使用 @RequestBody
@PostMapping("/")
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { (1)
	// ...
}
1 使用 @RequestBody

PartEvent

若要以串流方式循序存取多部分資料,您可以將 @RequestBodyFlux<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 必須完整取用、轉送或釋放 Body 內容,以避免記憶體外洩。
	@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 必須完整取用、轉送或釋放 Body 內容,以避免記憶體外洩。

接收到的部分事件也可以使用 WebClient 轉送到另一個服務。請參閱多部分資料