From 4869521230df80571bba5845f577338044bb96fc Mon Sep 17 00:00:00 2001 From: Hyunwoo Jung Date: Fri, 14 Aug 2026 05:46:14 +0900 Subject: [PATCH] Fix RestClient API usage in documentation Signed-off-by: Hyunwoo Jung --- .../ROOT/pages/integration/rest-clients.adoc | 130 +--------------- .../RestClientMessageConversion.java | 144 ++++++++++++++++++ .../create/RestClientCreation.java | 75 +++++++++ .../RestClientMessageConversion.kt | 116 ++++++++++++++ .../create/RestClientCreation.kt | 71 +++++++++ 5 files changed, 412 insertions(+), 124 deletions(-) create mode 100644 framework-docs/src/main/java/org/springframework/docs/integration/restmessageconversion/RestClientMessageConversion.java create mode 100644 framework-docs/src/main/java/org/springframework/docs/integration/restrestclient/create/RestClientCreation.java create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/integration/restmessageconversion/RestClientMessageConversion.kt create mode 100644 framework-docs/src/main/kotlin/org/springframework/docs/integration/restrestclient/create/RestClientCreation.kt diff --git a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc index 4813980bbf4d..f058e449dd7f 100644 --- a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc +++ b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc @@ -15,6 +15,7 @@ The Spring Framework provides the following choices for making calls to REST end `RestClient` is a synchronous HTTP client that provides a fluent API to perform requests. It serves as an abstraction over HTTP libraries, and handles conversion of HTTP request and response content to and from higher level Java objects. +[[rest-restclient.create]] === Create a `RestClient` `RestClient` has static `create` shortcut methods. @@ -32,48 +33,7 @@ Once created, a `RestClient` is safe to use in multiple threads. The below shows how to create or build a `RestClient`: -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim"] ----- - RestClient defaultClient = RestClient.create(); - - RestClient customClient = RestClient.builder() - .requestFactory(new HttpComponentsClientHttpRequestFactory()) - .messageConverters(converters -> converters.add(new MyCustomMessageConverter())) - .baseUrl("https://example.com") - .defaultUriVariables(Map.of("variable", "foo")) - .defaultHeader("My-Header", "Foo") - .defaultCookie("My-Cookie", "Bar") - .defaultVersion("1.2") - .apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build()) - .requestInterceptor(myCustomInterceptor) - .requestInitializer(myCustomInitializer) - .build(); ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim"] ----- - val defaultClient = RestClient.create() - - val customClient = RestClient.builder() - .requestFactory(HttpComponentsClientHttpRequestFactory()) - .messageConverters { converters -> converters.add(MyCustomMessageConverter()) } - .baseUrl("https://example.com") - .defaultUriVariables(mapOf("variable" to "foo")) - .defaultHeader("My-Header", "Foo") - .defaultCookie("My-Cookie", "Bar") - .defaultVersion("1.2") - .apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build()) - .requestInterceptor(myCustomInterceptor) - .requestInitializer(myCustomInitializer) - .build() ----- -====== +include-code::./RestClientCreation[tag=snippet,indent=0] === Use the `RestClient` @@ -390,17 +350,7 @@ xref:web/webmvc/message-converters.adoc#message-converters[See the supported HTT To serialize only a subset of the object properties, you can specify a {baeldung-blog}/jackson-json-view-annotation[Jackson JSON View], as the following example shows: -[source,java,indent=0,subs="verbatim"] ----- - MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23")); - value.setSerializationView(User.WithoutPasswordView.class); - - ResponseEntity response = restClient.post() // or RestTemplate.postForEntity - .contentType(APPLICATION_JSON) - .body(value) - .retrieve() - .toBodilessEntity(); ----- +include-code::./../restmessageconversion/RestClientMessageConversion[tag=jsonview,indent=0] ==== URL encoded Forms @@ -410,17 +360,7 @@ or a target type. For example: -[source,java,indent=0,subs="verbatim"] ----- - MultiValueMap form = new LinkedMultiValueMap<>(); - form.add("project", "Spring Framework"); - form.add("module", "spring-web"); - ResponseEntity response = this.restClient.post() - .contentType(MediaType.APPLICATION_FORM_URLENCODED) - .body(form) - .retrieve() - .toBodilessEntity(); ----- +include-code::./../restmessageconversion/RestClientMessageConversion[tag=urlencodedform,indent=0] ==== Multipart @@ -428,24 +368,7 @@ For example: To send multipart data, you need to provide a `MultiValueMap` whose values may be an `Object` for part content, a `Resource` for a file part, or an `HttpEntity` for part content with headers. For example: -[source,java,indent=0,subs="verbatim"] ----- - MultiValueMap parts = new LinkedMultiValueMap<>(); - - parts.add("fieldPart", "fieldValue"); - parts.add("filePart", new FileSystemResource("...logo.png")); - parts.add("jsonPart", new Person("Jason")); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_XML); - parts.add("xmlPart", new HttpEntity<>(myBean, headers)); - - ResponseEntity response = this.restClient.post() - .contentType(MediaType.MULTIPART_FORM_DATA) - .body(parts) - .retrieve() - .toBodilessEntity(); ----- +include-code::./../restmessageconversion/RestClientMessageConversion[tag=multipartrequest,indent=0] In most cases, you do not have to specify the `Content-Type` for each part. The content type is determined automatically based on the `HttpMessageConverter` chosen to serialize it or, in the case of a `Resource`, based on the file extension. @@ -461,48 +384,7 @@ To decode a multipart response body, use a `ParameterizedTypeReference result = this.restClient.get() - .uri("https://example.com/upload") - .accept(MediaType.MULTIPART_FORM_DATA) - .retrieve() - .body(new ParameterizedTypeReference<>() {}); - - Part field = result.getFirst("fieldPart"); - if (field instanceof FormFieldPart formField) { - String fieldValue = formField.value(); - } - Part file = result.getFirst("filePart"); - if (file instanceof FilePart filePart) { - filePart.transferTo(Path.of("/tmp/" + filePart.filename())); - } ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim"] ----- - val result = this.restClient.get() - .uri("https://example.com/upload") - .accept(MediaType.MULTIPART_FORM_DATA) - .retrieve() - .body(object : ParameterizedTypeReference>() {}) - - val field = result?.getFirst("fieldPart") - if (field is FormFieldPart) { - val fieldValue = field.value() - } - val file = result?.getFirst("filePart") - if (file is FilePart) { - file.transferTo(Path.of("/tmp/" + file.filename())) - } ----- -====== +include-code::./../restmessageconversion/RestClientMessageConversion[tag=multipartresponse,indent=0] [[rest-request-factories]] diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/restmessageconversion/RestClientMessageConversion.java b/framework-docs/src/main/java/org/springframework/docs/integration/restmessageconversion/RestClientMessageConversion.java new file mode 100644 index 000000000000..a6d1157b7ecc --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/restmessageconversion/RestClientMessageConversion.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-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.integration.restmessageconversion; + +import java.io.IOException; +import java.nio.file.Path; + +import com.fasterxml.jackson.annotation.JsonView; + +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.multipart.FilePart; +import org.springframework.http.converter.multipart.FormFieldPart; +import org.springframework.http.converter.multipart.Part; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClient; + +import static org.springframework.http.MediaType.APPLICATION_JSON; + +public class RestClientMessageConversion { + + private final RestClient restClient = RestClient.create(); + + private final Object myBean = new Object(); + + void useJsonView() { + // tag::jsonview[] + User user = new User("eric", "7!jd#h23"); + + ResponseEntity response = this.restClient.post() + .contentType(APPLICATION_JSON) + .body(user) + .hint(JsonView.class.getName(), User.WithoutPasswordView.class) + .retrieve() + .toBodilessEntity(); + // end::jsonview[] + } + + void sendUrlEncodedForm() { + // tag::urlencodedform[] + MultiValueMap form = new LinkedMultiValueMap<>(); + form.add("project", "Spring Framework"); + form.add("module", "spring-web"); + ResponseEntity response = this.restClient.post() + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(form) + .retrieve() + .toBodilessEntity(); + // end::urlencodedform[] + } + + void sendMultipartData() { + // tag::multipartrequest[] + MultiValueMap parts = new LinkedMultiValueMap<>(); + + parts.add("fieldPart", "fieldValue"); + parts.add("filePart", new FileSystemResource("...logo.png")); + parts.add("jsonPart", new Person("Jason")); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_XML); + parts.add("xmlPart", new HttpEntity<>(this.myBean, headers)); + + ResponseEntity response = this.restClient.post() + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(parts) + .retrieve() + .toBodilessEntity(); + // end::multipartrequest[] + } + + void receiveMultipartData() throws IOException { + // tag::multipartresponse[] + MultiValueMap result = this.restClient.get() + .uri("https://example.com/upload") + .accept(MediaType.MULTIPART_FORM_DATA) + .retrieve() + .body(new ParameterizedTypeReference<>() {}); + + Part field = result.getFirst("fieldPart"); + if (field instanceof FormFieldPart formField) { + String fieldValue = formField.value(); + } + Part file = result.getFirst("filePart"); + if (file instanceof FilePart filePart) { + filePart.transferTo(Path.of("/tmp/" + filePart.filename())); + } + // end::multipartresponse[] + } + + public static class User { + + private String username; + private String password; + + public User() { + } + + public User(String username, String password) { + this.username = username; + this.password = password; + } + + @JsonView(WithoutPasswordView.class) + public String getUsername() { + return this.username; + } + + @JsonView(WithPasswordView.class) + public String getPassword() { + return this.password; + } + + public interface WithoutPasswordView { + } + + public interface WithPasswordView extends WithoutPasswordView { + } + } + + private record Person(String name) { + + } + +} diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/restrestclient/create/RestClientCreation.java b/framework-docs/src/main/java/org/springframework/docs/integration/restrestclient/create/RestClientCreation.java new file mode 100644 index 000000000000..f9f264d10d60 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/restrestclient/create/RestClientCreation.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-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.integration.restrestclient.create; + +import java.io.IOException; +import java.util.Map; + +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInitializer; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.web.client.ApiVersionInserter; +import org.springframework.web.client.RestClient; + +public class RestClientCreation { + + void createRestClient() { + // tag::snippet[] + RestClient defaultClient = RestClient.create(); + + RestClient customClient = RestClient.builder() + .requestFactory(new HttpComponentsClientHttpRequestFactory()) + .configureMessageConverters(converters -> converters.addCustomConverter(new MyCustomMessageConverter())) + .baseUrl("https://example.com") + .defaultUriVariables(Map.of("variable", "foo")) + .defaultHeader("My-Header", "Foo") + .defaultCookie("My-Cookie", "Bar") + .defaultApiVersion("1.2") + .apiVersionInserter(ApiVersionInserter.useHeader("API-Version")) + .requestInterceptor(new MyCustomInterceptor()) + .requestInitializer(new MyCustomInitializer()) + .build(); + // end::snippet[] + } + + private static class MyCustomMessageConverter extends StringHttpMessageConverter { + } + + private static class MyCustomInterceptor implements ClientHttpRequestInterceptor { + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, + ClientHttpRequestExecution execution) throws IOException { + + return execution.execute(request, body); + } + } + + private static class MyCustomInitializer implements ClientHttpRequestInitializer { + + @Override + public void initialize(ClientHttpRequest request) { + request.getHeaders().add("My-Header", "My-Value"); + } + } + +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/restmessageconversion/RestClientMessageConversion.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/restmessageconversion/RestClientMessageConversion.kt new file mode 100644 index 000000000000..937da1a67520 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/restmessageconversion/RestClientMessageConversion.kt @@ -0,0 +1,116 @@ +/* + * Copyright 2002-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.integration.restmessageconversion + +import com.fasterxml.jackson.annotation.JsonView +import org.springframework.core.ParameterizedTypeReference +import org.springframework.core.io.FileSystemResource +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.MediaType +import org.springframework.http.MediaType.APPLICATION_JSON +import org.springframework.http.ResponseEntity +import org.springframework.http.converter.multipart.FilePart +import org.springframework.http.converter.multipart.FormFieldPart +import org.springframework.http.converter.multipart.Part +import org.springframework.util.LinkedMultiValueMap +import org.springframework.util.MultiValueMap +import org.springframework.web.client.RestClient +import java.nio.file.Path + +class RestClientMessageConversion { + + private val restClient = RestClient.create() + + private val myBean = Any() + + fun useJsonView() { + // tag::jsonview[] + val user = User("eric", "7!jd#h23") + + val response: ResponseEntity = restClient.post() + .contentType(APPLICATION_JSON) + .body(user) + .hint(JsonView::class.java.name, User.WithoutPasswordView::class.java) + .retrieve() + .toBodilessEntity() + // end::jsonview[] + } + + fun sendUrlEncodedForm() { + // tag::urlencodedform[] + val form: MultiValueMap = LinkedMultiValueMap() + form.add("project", "Spring Framework") + form.add("module", "spring-web") + val response: ResponseEntity = this.restClient.post() + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(form) + .retrieve() + .toBodilessEntity() + // end::urlencodedform[] + } + + fun sendMultipartData() { + // tag::multipartrequest[] + val parts: MultiValueMap = LinkedMultiValueMap() + + parts.add("fieldPart", "fieldValue") + parts.add("filePart", FileSystemResource("...logo.png")) + parts.add("jsonPart", Person("Jason")) + + val headers = HttpHeaders() + headers.contentType = MediaType.APPLICATION_XML + parts.add("xmlPart", HttpEntity(myBean, headers)) + + val response: ResponseEntity = this.restClient.post() + .contentType(MediaType.MULTIPART_FORM_DATA) + .body(parts) + .retrieve() + .toBodilessEntity() + // end::multipartrequest[] + } + + fun receiveMultipartData() { + // tag::multipartresponse[] + val result = this.restClient.get() + .uri("https://example.com/upload") + .accept(MediaType.MULTIPART_FORM_DATA) + .retrieve() + .body(object : ParameterizedTypeReference>() {}) + + val field = result?.getFirst("fieldPart") + if (field is FormFieldPart) { + val fieldValue = field.value() + } + val file = result?.getFirst("filePart") + if (file is FilePart) { + file.transferTo(Path.of("/tmp/" + file.filename())) + } + // end::multipartresponse[] + } + + class User( + @JsonView(WithoutPasswordView::class) val username: String, + @JsonView(WithPasswordView::class) val password: String) { + + interface WithoutPasswordView + interface WithPasswordView : WithoutPasswordView + } + + data class Person(val name: String) + +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/restrestclient/create/RestClientCreation.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/restrestclient/create/RestClientCreation.kt new file mode 100644 index 000000000000..53b751a091a9 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/restrestclient/create/RestClientCreation.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2002-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.integration.restrestclient.create + +import org.springframework.http.HttpRequest +import org.springframework.http.client.ClientHttpRequest +import org.springframework.http.client.ClientHttpRequestExecution +import org.springframework.http.client.ClientHttpRequestInitializer +import org.springframework.http.client.ClientHttpRequestInterceptor +import org.springframework.http.client.ClientHttpResponse +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory +import org.springframework.http.converter.StringHttpMessageConverter +import org.springframework.web.client.ApiVersionInserter +import org.springframework.web.client.RestClient + +class RestClientCreation { + + fun createRestClient() { + // tag::snippet[] + val defaultClient = RestClient.create() + + val customClient = RestClient.builder() + .requestFactory(HttpComponentsClientHttpRequestFactory()) + .configureMessageConverters { converters -> converters.addCustomConverter(MyCustomMessageConverter()) } + .baseUrl("https://example.com") + .defaultUriVariables(mapOf("variable" to "foo")) + .defaultHeader("My-Header", "Foo") + .defaultCookie("My-Cookie", "Bar") + .defaultApiVersion("1.2") + .apiVersionInserter(ApiVersionInserter.useHeader("API-Version")) + .requestInterceptor(MyCustomInterceptor()) + .requestInitializer(MyCustomInitializer()) + .build() + // end::snippet[] + } + + private class MyCustomMessageConverter : StringHttpMessageConverter() + + private class MyCustomInterceptor : ClientHttpRequestInterceptor { + + override fun intercept( + request: HttpRequest, + body: ByteArray, + execution: ClientHttpRequestExecution + ): ClientHttpResponse { + return execution.execute(request, body) + } + } + + private class MyCustomInitializer : ClientHttpRequestInitializer { + + override fun initialize(request: ClientHttpRequest) { + request.headers.add("My-Header", "My-Value") + } + } + +}