diff --git a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc index ec4fd12a012..4b4d4385520 100644 --- a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc +++ b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc @@ -988,6 +988,27 @@ parameter annotation) is set to `false`, or the parameter is marked optional as +[[rest-http-interface.custom-resolver]] +=== Custom argument resolver + +For more complex cases, HTTP interfaces do not support `RequestEntity` types as method parameters. +This would take over the entire HTTP request and not improve the semantics of the interface. +Instead of adding many method parameters, developers can combine them into a custom type +and configure a dedicated `HttpServiceArgumentResolver` implementation. + +In the following HTTP interface, we are using a custom `Search` type as a parameter: + +include-code::./CustomHttpServiceArgumentResolver[tag=httpinterface,indent=0] + +We can implement our own `HttpServiceArgumentResolver` that supports our custom `Search` type +and writes its data in the outgoing HTTP request. + +include-code::./CustomHttpServiceArgumentResolver[tag=argumentresolver,indent=0] + +Finally, we can use this argument resolver during the setup and use our HTTP interface. + +include-code::./CustomHttpServiceArgumentResolver[tag=usage,indent=0] + [[rest-http-interface-return-values]] === Return Values diff --git a/framework-docs/src/main/java/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.java b/framework-docs/src/main/java/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.java new file mode 100644 index 00000000000..779654bbad0 --- /dev/null +++ b/framework-docs/src/main/java/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2025 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.resthttpinterface.customresolver; + +import java.util.List; + +import org.springframework.core.MethodParameter; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.support.RestClientAdapter; +import org.springframework.web.service.annotation.GetExchange; +import org.springframework.web.service.invoker.HttpRequestValues; +import org.springframework.web.service.invoker.HttpServiceArgumentResolver; +import org.springframework.web.service.invoker.HttpServiceProxyFactory; + +public class CustomHttpServiceArgumentResolver { + + // tag::httpinterface[] + interface RepositoryService { + + @GetExchange("/repos/search") + List searchRepository(Search search); + + } + // end::httpinterface[] + + class Sample { + + void sample() { + // tag::usage[] + RestClient restClient = RestClient.builder().baseUrl("https://api.github.com/").build(); + RestClientAdapter adapter = RestClientAdapter.create(restClient); + HttpServiceProxyFactory factory = HttpServiceProxyFactory + .builderFor(adapter) + .customArgumentResolver(new SearchQueryArgumentResolver()) + .build(); + RepositoryService repositoryService = factory.createClient(RepositoryService.class); + + Search search = Search.create() + .owner("spring-projects") + .language("java") + .query("rest") + .build(); + List repositories = repositoryService.searchRepository(search); + // end::usage[] + } + + } + + // tag::argumentresolver[] + static class SearchQueryArgumentResolver implements HttpServiceArgumentResolver { + @Override + public boolean resolve(Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) { + if (parameter.getParameterType().equals(Search.class)) { + Search search = (Search) argument; + requestValues.addRequestParameter("owner", search.owner()); + requestValues.addRequestParameter("language", search.language()); + requestValues.addRequestParameter("query", search.query()); + return true; + } + return false; + } + } + // end::argumentresolver[] + + + record Search (String query, String owner, String language) { + + static Builder create() { + return new Builder(); + } + + static class Builder { + + Builder query(String query) { return this;} + + Builder owner(String owner) { return this;} + + Builder language(String language) { return this;} + + Search build() { + return new Search(null, null, null); + } + } + + } + + record Repository(String name) { + + } + +} diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.kt new file mode 100644 index 00000000000..86e4b5566b3 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/resthttpinterface/customresolver/CustomHttpServiceArgumentResolver.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2025 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.resthttpinterface.customresolver + +import org.springframework.core.MethodParameter +import org.springframework.web.client.RestClient +import org.springframework.web.client.support.RestClientAdapter +import org.springframework.web.service.annotation.GetExchange +import org.springframework.web.service.invoker.HttpRequestValues +import org.springframework.web.service.invoker.HttpServiceArgumentResolver +import org.springframework.web.service.invoker.HttpServiceProxyFactory + +class CustomHttpServiceArgumentResolver { + + // tag::httpinterface[] + interface RepositoryService { + + @GetExchange("/repos/search") + fun searchRepository(search: Search): List + + } + // end::httpinterface[] + + class Sample { + fun sample() { + // tag::usage[] + val restClient = RestClient.builder().baseUrl("https://api.github.com/").build() + val adapter = RestClientAdapter.create(restClient) + val factory = HttpServiceProxyFactory + .builderFor(adapter) + .customArgumentResolver(SearchQueryArgumentResolver()) + .build() + val repositoryService = factory.createClient(RepositoryService::class.java) + + val search = Search(owner = "spring-projects", language = "java", query = "rest") + val repositories = repositoryService.searchRepository(search) + // end::usage[] + } + } + + // tag::argumentresolver[] + class SearchQueryArgumentResolver : HttpServiceArgumentResolver { + override fun resolve( + argument: Any?, + parameter: MethodParameter, + requestValues: HttpRequestValues.Builder + ): Boolean { + if (parameter.getParameterType() == Search::class.java) { + val search = argument as Search + requestValues.addRequestParameter("owner", search.owner) + .addRequestParameter("language", search.language) + .addRequestParameter("query", search.query) + return true + } + return false + } + } + // end::argumentresolver[] + + data class Search(val query: String, val owner: String, val language: String) + + data class Repository(val name: String) +} \ No newline at end of file