From 9c30cb1de555234a83a9f62c5b03847344cdef76 Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Sat, 3 Jan 2026 12:25:53 +0700 Subject: [PATCH 1/2] Fix Kotlin example in `Multipart Content` Signed-off-by: Tran Ngoc Nhan --- .../ann-methods/multipart-forms.adoc | 80 ++----------------- .../partevent/PartEventController.java | 60 ++++++++++++++ .../partevent/PartEventController.kt | 59 ++++++++++++++ 3 files changed, 124 insertions(+), 75 deletions(-) create mode 100644 framework-docs/src/main/java/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.java create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.kt diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/multipart-forms.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/multipart-forms.adoc index 9e5ac96d7ab7..9c17af58ee7d 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/multipart-forms.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/multipart-forms.adoc @@ -103,8 +103,8 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") - fun handle(@RequestPart("meta-data") Part metadata, // <1> - @RequestPart("file-data") FilePart file): String { // <2> + fun handle(@RequestPart("meta-data") metadata: MetaData, // <1> + @RequestPart("file-data") file: MultipartFile): String { // <2> // ... } ---- @@ -169,7 +169,7 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") - fun handle(@Valid @RequestPart("meta-data") metadata: MetaData): String { + fun handle(@Valid @RequestPart("meta-data") metadata: Mono): String { // ... } ---- @@ -202,7 +202,7 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") - fun handle(@RequestBody parts: MultiValueMap): String { // <1> + fun handle(@RequestBody parts: Mono>): String { // <1> // ... } ---- @@ -227,76 +227,7 @@ when uploading. If the file is large enough to be split across multiple buffers, For example: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes"] ----- - @PostMapping("/") - public void handle(@RequestBody Flux 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 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:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes"] ----- - @PostMapping("/") - fun handle(@RequestBody allPartsEvents: Flux) = { // <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 = partEvents.map(PartEvent::content); <6> - // handle file upload - } else { - return Mono.error(RuntimeException("Unexpected event: " + event)); - } - } else { - return partEvents; // either complete or error signal - } - } - } -} ----- +include-code::./PartEventController[tag=snippet,indent=0] <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. @@ -307,7 +238,6 @@ 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. -====== Received part events can also be relayed to another service by using the `WebClient`. See xref:web/webflux-webclient/client-body.adoc#webflux-client-body-multipart[Multipart Data]. diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.java b/framework-docs/src/main/java/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.java new file mode 100644 index 000000000000..0f13c24ab3a9 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.java @@ -0,0 +1,60 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webflux.controller.annmethods.partevent; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.codec.multipart.FilePartEvent; +import org.springframework.http.codec.multipart.FormPartEvent; +import org.springframework.http.codec.multipart.PartEvent; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@RestController +public class PartEventController { + + // tag::snippet[] + @PostMapping("/") + public void handle(@RequestBody Flux 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 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 + } + return Mono.empty(); + })); + } + // end::snippet[] + +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.kt new file mode 100644 index 000000000000..e3197ecd4454 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.docs.web.webflux.controller.annmethods.partevent + +import org.springframework.core.io.buffer.DataBuffer +import org.springframework.http.codec.multipart.FilePartEvent +import org.springframework.http.codec.multipart.FormPartEvent +import org.springframework.http.codec.multipart.PartEvent +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono + +@RestController +class PartEventController { + + // tag::snippet[] + @PostMapping("/") + fun handle(@RequestBody allPartsEvents: Flux) { // <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 = partEvents.map(PartEvent::content) // <6> + // handle file upload + } else { + return@switchOnFirst Mono.error(RuntimeException("Unexpected event: $event")) + } + } else { + return@switchOnFirst partEvents // either complete or error signal + } + Mono.empty() + } + } + } + // end::snippet[] + +} \ No newline at end of file From 455b999f7f1f1e6cac8e217ab5f5e2fcfd7a003d Mon Sep 17 00:00:00 2001 From: Tran Ngoc Nhan Date: Mon, 5 Jan 2026 22:05:03 +0700 Subject: [PATCH 2/2] Move references to comments Signed-off-by: Tran Ngoc Nhan --- .../ann-methods/multipart-forms.adoc | 14 ++-------- .../partevent/PartEventController.java | 26 +++++++++++------ .../partevent/PartEventController.kt | 28 +++++++++++++------ 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/multipart-forms.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/multipart-forms.adoc index 9c17af58ee7d..9b6b76fa7e23 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/multipart-forms.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-methods/multipart-forms.adoc @@ -103,8 +103,8 @@ Kotlin:: [source,kotlin,indent=0,subs="verbatim,quotes"] ---- @PostMapping("/") - fun handle(@RequestPart("meta-data") metadata: MetaData, // <1> - @RequestPart("file-data") file: MultipartFile): String { // <2> + fun handle(@RequestPart("meta-data") metadata: Part, // <1> + @RequestPart("file-data") file: FilePart): String { // <2> // ... } ---- @@ -228,16 +228,6 @@ when uploading. If the file is large enough to be split across multiple buffers, For example: include-code::./PartEventController[tag=snippet,indent=0] -<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. Received part events can also be relayed to another service by using the `WebClient`. See xref:web/webflux-webclient/client-body.adoc#webflux-client-body-multipart[Multipart Data]. diff --git a/framework-docs/src/main/java/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.java b/framework-docs/src/main/java/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.java index 0f13c24ab3a9..8d11a49dc31c 100644 --- a/framework-docs/src/main/java/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.java +++ b/framework-docs/src/main/java/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.java @@ -31,19 +31,29 @@ public class PartEventController { // tag::snippet[] @PostMapping("/") - public void handle(@RequestBody Flux allPartsEvents) { // <1> - allPartsEvents.windowUntil(PartEvent::isLast) // <2> - .concatMap(p -> p.switchOnFirst((signal, partEvents) -> { // <3> + public void handle(@RequestBody Flux allPartsEvents) { // Using @RequestBody. + + // 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. + allPartsEvents.windowUntil(PartEvent::isLast) + + // The Flux::switchOnFirst operator allows you to see whether you are handling + // a form field or file upload. + .concatMap(p -> p.switchOnFirst((signal, partEvents) -> { if (signal.hasValue()) { PartEvent event = signal.get(); - if (event instanceof FormPartEvent formEvent) { // <4> + if (event instanceof FormPartEvent formEvent) { String value = formEvent.value(); - // handle form field + // Handling the form field. } - else if (event instanceof FilePartEvent fileEvent) { // <5> + else if (event instanceof FilePartEvent fileEvent) { String filename = fileEvent.filename(); - Flux contents = partEvents.map(PartEvent::content); // <6> - // handle file upload + + // The body contents must be completely consumed, relayed, or released to avoid memory leaks. + Flux contents = partEvents.map(PartEvent::content); + // Handling the file upload. } else { return Mono.error(new RuntimeException("Unexpected event: " + event)); diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.kt index e3197ecd4454..0a79bf860f61 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webflux/controller/annmethods/partevent/PartEventController.kt @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -31,19 +31,29 @@ class PartEventController { // tag::snippet[] @PostMapping("/") - fun handle(@RequestBody allPartsEvents: Flux) { // <1> - allPartsEvents.windowUntil(PartEvent::isLast) // <2> + fun handle(@RequestBody allPartsEvents: Flux) { // Using @RequestBody. + + // 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. + allPartsEvents.windowUntil(PartEvent::isLast) .concatMap { - it.switchOnFirst { signal, partEvents -> // <3> + + // The Flux::switchOnFirst operator allows you to see whether you are handling + // a form field or file upload. + it.switchOnFirst { signal, partEvents -> if (signal.hasValue()) { val event = signal.get() - if (event is FormPartEvent) { // <4> + if (event is FormPartEvent) { val value: String = event.value() - // handle form field - } else if (event is FilePartEvent) { // <5> + // Handling the form field. + } else if (event is FilePartEvent) { val filename: String = event.filename() - val contents: Flux = partEvents.map(PartEvent::content) // <6> - // handle file upload + + // The body contents must be completely consumed, relayed, or released to avoid memory leaks. + val contents: Flux = partEvents.map(PartEvent::content) + // Handling the file upload. } else { return@switchOnFirst Mono.error(RuntimeException("Unexpected event: $event")) }