diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java index 20469a977da..05c465deeb6 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java @@ -70,6 +70,8 @@ public final class HttpRequestValues { private final MultiValueMap cookies; + private final Map attributes; + @Nullable private final Object bodyValue; @@ -82,7 +84,7 @@ public final class HttpRequestValues { private HttpRequestValues(HttpMethod httpMethod, @Nullable URI uri, @Nullable String uriTemplate, Map uriVariables, - HttpHeaders headers, MultiValueMap cookies, + HttpHeaders headers, MultiValueMap cookies, Map attributes, @Nullable Object bodyValue, @Nullable Publisher body, @Nullable ParameterizedTypeReference bodyElementType) { @@ -94,6 +96,7 @@ public final class HttpRequestValues { this.uriVariables = uriVariables; this.headers = headers; this.cookies = cookies; + this.attributes = attributes; this.bodyValue = bodyValue; this.body = body; this.bodyElementType = bodyElementType; @@ -142,12 +145,19 @@ public final class HttpRequestValues { } /** - * Return the cookies for the request, if any. + * Return the cookies for the request, or an empty map. */ public MultiValueMap getCookies() { return this.cookies; } + /** + * Return the attributes associated with the request, or an empty map. + */ + public Map getAttributes() { + return this.attributes; + } + /** * Return the request body as a value to be serialized, if set. *

This is mutually exclusive with {@link #getBody()}. @@ -209,6 +219,9 @@ public final class HttpRequestValues { @Nullable private MultiValueMap requestParams; + @Nullable + private Map attributes; + @Nullable private Object bodyValue; @@ -325,6 +338,17 @@ public final class HttpRequestValues { return this; } + /** + * Configure an attribute to associate with the request. + * @param name the attribute name + * @param value the attribute value + */ + public Builder addAttribute(String name, Object value) { + this.attributes = (this.attributes != null ? this.attributes : new HashMap<>()); + this.attributes.put(name, value); + return this; + } + /** * Set the request body as a concrete value to be serialized. *

This is mutually exclusive with, and resets any previously set @@ -388,9 +412,12 @@ public final class HttpRequestValues { MultiValueMap cookies = (this.cookies != null ? new LinkedMultiValueMap<>(this.cookies) : EMPTY_COOKIES_MAP); + Map attributes = (this.attributes != null ? + new HashMap<>(this.attributes) : Collections.emptyMap()); + return new HttpRequestValues( - this.httpMethod, uri, uriTemplate, uriVars, headers, cookies, bodyValue, - this.body, this.bodyElementType); + this.httpMethod, uri, uriTemplate, uriVars, headers, cookies, attributes, + bodyValue, this.body, this.bodyElementType); } private String appendQueryParams( diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java index 55334b534fb..0ec408e185a 100644 --- a/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java @@ -185,14 +185,21 @@ public final class HttpServiceProxyFactory { } private List initArgumentResolvers(ConversionService conversionService) { + List resolvers = new ArrayList<>(this.customResolvers); + + // Annotation-based resolvers.add(new RequestHeaderArgumentResolver(conversionService)); resolvers.add(new RequestBodyArgumentResolver(this.reactiveAdapterRegistry)); resolvers.add(new PathVariableArgumentResolver(conversionService)); resolvers.add(new RequestParamArgumentResolver(conversionService)); resolvers.add(new CookieValueArgumentResolver(conversionService)); + resolvers.add(new RequestAttributeArgumentResolver()); + + // Specific type resolvers.add(new UrlArgumentResolver()); resolvers.add(new HttpMethodArgumentResolver()); + return resolvers; } diff --git a/spring-web/src/main/java/org/springframework/web/service/invoker/RequestAttributeArgumentResolver.java b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestAttributeArgumentResolver.java new file mode 100644 index 00000000000..c7a9f9f0ef6 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/service/invoker/RequestAttributeArgumentResolver.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2022 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.web.service.invoker; + +import org.springframework.core.MethodParameter; +import org.springframework.web.bind.annotation.RequestAttribute; + +/** + * {@link HttpServiceArgumentResolver} for {@link RequestAttribute @RequestAttribute} + * annotated arguments. + * + *

The argument may be a single variable value or a {@code Map} with multiple + * variables and values. + * + *

If the value is required but {@code null}, {@link IllegalArgumentException} + * is raised. The value is not required if: + *

    + *
  • {@link RequestAttribute#required()} is set to {@code false} + *
  • The argument is declared as {@link java.util.Optional} + *
+ * + * @author Rossen Stoyanchev + * @since 6.0 + */ +public class RequestAttributeArgumentResolver extends AbstractNamedValueArgumentResolver { + + + @Override + protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { + RequestAttribute annot = parameter.getParameterAnnotation(RequestAttribute.class); + return (annot == null ? null : + new NamedValueInfo(annot.name(), annot.required(), null, "request attribute", false)); + } + + @Override + protected void addRequestValue(String name, Object value, HttpRequestValues.Builder requestValues) { + requestValues.addAttribute(name, value); + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java index ddd0f067386..0f29856b8b0 100644 --- a/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java @@ -48,8 +48,7 @@ class PathVariableArgumentResolverTests { @SuppressWarnings("SameParameterValue") private void assertPathVariable(String name, @Nullable String expectedValue) { - assertThat(this.client.getRequestValues().getUriVariables().get(name)) - .isEqualTo(expectedValue); + assertThat(this.client.getRequestValues().getUriVariables().get(name)).isEqualTo(expectedValue); } diff --git a/spring-web/src/test/java/org/springframework/web/service/invoker/RequestAttributeArgumentResolverTests.java b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestAttributeArgumentResolverTests.java new file mode 100644 index 00000000000..e020b2bf6ad --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/service/invoker/RequestAttributeArgumentResolverTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2022 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.web.service.invoker; + +import org.junit.jupiter.api.Test; + +import org.springframework.lang.Nullable; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.service.annotation.GetExchange; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Unit tests for {@link RequestAttributeArgumentResolver}. + *

For base class functionality, see {@link NamedValueArgumentResolverTests}. + * + * @author Rossen Stoyanchev + */ +class RequestAttributeArgumentResolverTests { + + private final TestHttpClientAdapter client = new TestHttpClientAdapter(); + + private final Service service = HttpServiceProxyFactory.builder(this.client).build().createClient(Service.class); + + + // Base class functionality should be tested in NamedValueArgumentResolverTests. + + @Test + void cookieValue() { + this.service.execute("test"); + assertAttribute("attribute", "test"); + } + + @SuppressWarnings("SameParameterValue") + private void assertAttribute(String name, @Nullable String expectedValue) { + assertThat(this.client.getRequestValues().getAttributes().get(name)).isEqualTo(expectedValue); + } + + + private interface Service { + + @GetExchange + void execute(@RequestAttribute String attribute); + + } + +} diff --git a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java index e4685b67318..ecb9dfa8504 100644 --- a/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java +++ b/spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java @@ -103,6 +103,7 @@ public class WebClientAdapter implements HttpClientAdapter { bodySpec.headers(headers -> headers.putAll(requestValues.getHeaders())); bodySpec.cookies(cookies -> cookies.putAll(requestValues.getCookies())); + bodySpec.attributes(attributes -> attributes.putAll(requestValues.getAttributes())); if (requestValues.getBodyValue() != null) { bodySpec.bodyValue(requestValues.getBodyValue()); diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyTests.java index 8010bb4ac61..93ee2d4af18 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyTests.java @@ -19,6 +19,8 @@ package org.springframework.web.reactive.function.client.support; import java.io.IOException; import java.time.Duration; +import java.util.HashMap; +import java.util.Map; import java.util.function.Consumer; import okhttp3.mockwebserver.MockResponse; @@ -29,11 +31,13 @@ import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.bind.annotation.RequestAttribute; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.service.annotation.GetExchange; import org.springframework.web.service.invoker.HttpServiceProxyFactory; +import static org.assertj.core.api.Assertions.assertThat; + /** * Integration tests for {@link HttpServiceProxyFactory HTTP Service proxy} @@ -45,22 +49,10 @@ public class WebClientHttpServiceProxyTests { private MockWebServer server; - private TestHttpService httpService; - @BeforeEach void setUp() { this.server = new MockWebServer(); - WebClient webClient = WebClient - .builder() - .clientConnector(new ReactorClientHttpConnector()) - .baseUrl(this.server.url("/").toString()) - .build(); - - WebClientAdapter clientAdapter = new WebClientAdapter(webClient); - HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(clientAdapter).build(); - - this.httpService = proxyFactory.createClient(TestHttpService.class); } @SuppressWarnings("ConstantConditions") @@ -78,12 +70,47 @@ public class WebClientHttpServiceProxyTests { prepareResponse(response -> response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); - StepVerifier.create(this.httpService.getGreeting()) + StepVerifier.create(initHttpService().getGreeting()) .expectNext("Hello Spring!") .expectComplete() .verify(Duration.ofSeconds(5)); } + @Test + void greetingWithRequestAttribute() { + + Map attributes = new HashMap<>(); + + WebClient webClient = WebClient.builder() + .baseUrl(this.server.url("/").toString()) + .filter((request, next) -> { + attributes.putAll(request.attributes()); + return next.exchange(request); + }) + .build(); + + prepareResponse(response -> + response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); + + StepVerifier.create(initHttpService(webClient).getGreetingWithAttribute("myAttributeValue")) + .expectNext("Hello Spring!") + .expectComplete() + .verify(Duration.ofSeconds(5)); + + assertThat(attributes).containsEntry("myAttribute", "myAttributeValue"); + } + + private TestHttpService initHttpService() { + WebClient webClient = WebClient.builder().baseUrl(this.server.url("/").toString()).build(); + return initHttpService(webClient); + } + + private TestHttpService initHttpService(WebClient webClient) { + WebClientAdapter clientAdapter = new WebClientAdapter(webClient); + HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(clientAdapter).build(); + return proxyFactory.createClient(TestHttpService.class); + } + private void prepareResponse(Consumer consumer) { MockResponse response = new MockResponse(); consumer.accept(response); @@ -96,6 +123,9 @@ public class WebClientHttpServiceProxyTests { @GetExchange("/greeting") Mono getGreeting(); + @GetExchange("/greeting") + Mono getGreetingWithAttribute(@RequestAttribute String myAttribute); + }