Add RequestAttributeArgumentResolver

Closes gh-28458
This commit is contained in:
rstoyanchev 2022-05-17 14:02:23 +01:00
parent 495507e5d4
commit 496c1dcae1
7 changed files with 199 additions and 20 deletions

View File

@ -70,6 +70,8 @@ public final class HttpRequestValues {
private final MultiValueMap<String, String> cookies; private final MultiValueMap<String, String> cookies;
private final Map<String, Object> attributes;
@Nullable @Nullable
private final Object bodyValue; private final Object bodyValue;
@ -82,7 +84,7 @@ public final class HttpRequestValues {
private HttpRequestValues(HttpMethod httpMethod, private HttpRequestValues(HttpMethod httpMethod,
@Nullable URI uri, @Nullable String uriTemplate, Map<String, String> uriVariables, @Nullable URI uri, @Nullable String uriTemplate, Map<String, String> uriVariables,
HttpHeaders headers, MultiValueMap<String, String> cookies, HttpHeaders headers, MultiValueMap<String, String> cookies, Map<String, Object> attributes,
@Nullable Object bodyValue, @Nullable Object bodyValue,
@Nullable Publisher<?> body, @Nullable ParameterizedTypeReference<?> bodyElementType) { @Nullable Publisher<?> body, @Nullable ParameterizedTypeReference<?> bodyElementType) {
@ -94,6 +96,7 @@ public final class HttpRequestValues {
this.uriVariables = uriVariables; this.uriVariables = uriVariables;
this.headers = headers; this.headers = headers;
this.cookies = cookies; this.cookies = cookies;
this.attributes = attributes;
this.bodyValue = bodyValue; this.bodyValue = bodyValue;
this.body = body; this.body = body;
this.bodyElementType = bodyElementType; 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<String, String> getCookies() { public MultiValueMap<String, String> getCookies() {
return this.cookies; return this.cookies;
} }
/**
* Return the attributes associated with the request, or an empty map.
*/
public Map<String, Object> getAttributes() {
return this.attributes;
}
/** /**
* Return the request body as a value to be serialized, if set. * Return the request body as a value to be serialized, if set.
* <p>This is mutually exclusive with {@link #getBody()}. * <p>This is mutually exclusive with {@link #getBody()}.
@ -209,6 +219,9 @@ public final class HttpRequestValues {
@Nullable @Nullable
private MultiValueMap<String, String> requestParams; private MultiValueMap<String, String> requestParams;
@Nullable
private Map<String, Object> attributes;
@Nullable @Nullable
private Object bodyValue; private Object bodyValue;
@ -325,6 +338,17 @@ public final class HttpRequestValues {
return this; 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. * Set the request body as a concrete value to be serialized.
* <p>This is mutually exclusive with, and resets any previously set * <p>This is mutually exclusive with, and resets any previously set
@ -388,9 +412,12 @@ public final class HttpRequestValues {
MultiValueMap<String, String> cookies = (this.cookies != null ? MultiValueMap<String, String> cookies = (this.cookies != null ?
new LinkedMultiValueMap<>(this.cookies) : EMPTY_COOKIES_MAP); new LinkedMultiValueMap<>(this.cookies) : EMPTY_COOKIES_MAP);
Map<String, Object> attributes = (this.attributes != null ?
new HashMap<>(this.attributes) : Collections.emptyMap());
return new HttpRequestValues( return new HttpRequestValues(
this.httpMethod, uri, uriTemplate, uriVars, headers, cookies, bodyValue, this.httpMethod, uri, uriTemplate, uriVars, headers, cookies, attributes,
this.body, this.bodyElementType); bodyValue, this.body, this.bodyElementType);
} }
private String appendQueryParams( private String appendQueryParams(

View File

@ -185,14 +185,21 @@ public final class HttpServiceProxyFactory {
} }
private List<HttpServiceArgumentResolver> initArgumentResolvers(ConversionService conversionService) { private List<HttpServiceArgumentResolver> initArgumentResolvers(ConversionService conversionService) {
List<HttpServiceArgumentResolver> resolvers = new ArrayList<>(this.customResolvers); List<HttpServiceArgumentResolver> resolvers = new ArrayList<>(this.customResolvers);
// Annotation-based
resolvers.add(new RequestHeaderArgumentResolver(conversionService)); resolvers.add(new RequestHeaderArgumentResolver(conversionService));
resolvers.add(new RequestBodyArgumentResolver(this.reactiveAdapterRegistry)); resolvers.add(new RequestBodyArgumentResolver(this.reactiveAdapterRegistry));
resolvers.add(new PathVariableArgumentResolver(conversionService)); resolvers.add(new PathVariableArgumentResolver(conversionService));
resolvers.add(new RequestParamArgumentResolver(conversionService)); resolvers.add(new RequestParamArgumentResolver(conversionService));
resolvers.add(new CookieValueArgumentResolver(conversionService)); resolvers.add(new CookieValueArgumentResolver(conversionService));
resolvers.add(new RequestAttributeArgumentResolver());
// Specific type
resolvers.add(new UrlArgumentResolver()); resolvers.add(new UrlArgumentResolver());
resolvers.add(new HttpMethodArgumentResolver()); resolvers.add(new HttpMethodArgumentResolver());
return resolvers; return resolvers;
} }

View File

@ -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.
*
* <p>The argument may be a single variable value or a {@code Map} with multiple
* variables and values.
*
* <p>If the value is required but {@code null}, {@link IllegalArgumentException}
* is raised. The value is not required if:
* <ul>
* <li>{@link RequestAttribute#required()} is set to {@code false}
* <li>The argument is declared as {@link java.util.Optional}
* </ul>
*
* @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);
}
}

View File

@ -48,8 +48,7 @@ class PathVariableArgumentResolverTests {
@SuppressWarnings("SameParameterValue") @SuppressWarnings("SameParameterValue")
private void assertPathVariable(String name, @Nullable String expectedValue) { private void assertPathVariable(String name, @Nullable String expectedValue) {
assertThat(this.client.getRequestValues().getUriVariables().get(name)) assertThat(this.client.getRequestValues().getUriVariables().get(name)).isEqualTo(expectedValue);
.isEqualTo(expectedValue);
} }

View File

@ -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}.
* <p>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);
}
}

View File

@ -103,6 +103,7 @@ public class WebClientAdapter implements HttpClientAdapter {
bodySpec.headers(headers -> headers.putAll(requestValues.getHeaders())); bodySpec.headers(headers -> headers.putAll(requestValues.getHeaders()));
bodySpec.cookies(cookies -> cookies.putAll(requestValues.getCookies())); bodySpec.cookies(cookies -> cookies.putAll(requestValues.getCookies()));
bodySpec.attributes(attributes -> attributes.putAll(requestValues.getAttributes()));
if (requestValues.getBodyValue() != null) { if (requestValues.getBodyValue() != null) {
bodySpec.bodyValue(requestValues.getBodyValue()); bodySpec.bodyValue(requestValues.getBodyValue());

View File

@ -19,6 +19,8 @@ package org.springframework.web.reactive.function.client.support;
import java.io.IOException; import java.io.IOException;
import java.time.Duration; import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import okhttp3.mockwebserver.MockResponse; import okhttp3.mockwebserver.MockResponse;
@ -29,11 +31,13 @@ import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import reactor.test.StepVerifier; 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.reactive.function.client.WebClient;
import org.springframework.web.service.annotation.GetExchange; import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.invoker.HttpServiceProxyFactory; import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import static org.assertj.core.api.Assertions.assertThat;
/** /**
* Integration tests for {@link HttpServiceProxyFactory HTTP Service proxy} * Integration tests for {@link HttpServiceProxyFactory HTTP Service proxy}
@ -45,22 +49,10 @@ public class WebClientHttpServiceProxyTests {
private MockWebServer server; private MockWebServer server;
private TestHttpService httpService;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
this.server = new MockWebServer(); 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") @SuppressWarnings("ConstantConditions")
@ -78,12 +70,47 @@ public class WebClientHttpServiceProxyTests {
prepareResponse(response -> prepareResponse(response ->
response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!")); response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
StepVerifier.create(this.httpService.getGreeting()) StepVerifier.create(initHttpService().getGreeting())
.expectNext("Hello Spring!") .expectNext("Hello Spring!")
.expectComplete() .expectComplete()
.verify(Duration.ofSeconds(5)); .verify(Duration.ofSeconds(5));
} }
@Test
void greetingWithRequestAttribute() {
Map<String, Object> 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<MockResponse> consumer) { private void prepareResponse(Consumer<MockResponse> consumer) {
MockResponse response = new MockResponse(); MockResponse response = new MockResponse();
consumer.accept(response); consumer.accept(response);
@ -96,6 +123,9 @@ public class WebClientHttpServiceProxyTests {
@GetExchange("/greeting") @GetExchange("/greeting")
Mono<String> getGreeting(); Mono<String> getGreeting();
@GetExchange("/greeting")
Mono<String> getGreetingWithAttribute(@RequestAttribute String myAttribute);
} }