Skip to content

Commit 496c1dc

Browse files
committed
Add RequestAttributeArgumentResolver
Closes gh-28458
1 parent 495507e commit 496c1dc

File tree

7 files changed

+199
-20
lines changed

7 files changed

+199
-20
lines changed

spring-web/src/main/java/org/springframework/web/service/invoker/HttpRequestValues.java

+31-4
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ public final class HttpRequestValues {
7070

7171
private final MultiValueMap<String, String> cookies;
7272

73+
private final Map<String, Object> attributes;
74+
7375
@Nullable
7476
private final Object bodyValue;
7577

@@ -82,7 +84,7 @@ public final class HttpRequestValues {
8284

8385
private HttpRequestValues(HttpMethod httpMethod,
8486
@Nullable URI uri, @Nullable String uriTemplate, Map<String, String> uriVariables,
85-
HttpHeaders headers, MultiValueMap<String, String> cookies,
87+
HttpHeaders headers, MultiValueMap<String, String> cookies, Map<String, Object> attributes,
8688
@Nullable Object bodyValue,
8789
@Nullable Publisher<?> body, @Nullable ParameterizedTypeReference<?> bodyElementType) {
8890

@@ -94,6 +96,7 @@ private HttpRequestValues(HttpMethod httpMethod,
9496
this.uriVariables = uriVariables;
9597
this.headers = headers;
9698
this.cookies = cookies;
99+
this.attributes = attributes;
97100
this.bodyValue = bodyValue;
98101
this.body = body;
99102
this.bodyElementType = bodyElementType;
@@ -142,12 +145,19 @@ public HttpHeaders getHeaders() {
142145
}
143146

144147
/**
145-
* Return the cookies for the request, if any.
148+
* Return the cookies for the request, or an empty map.
146149
*/
147150
public MultiValueMap<String, String> getCookies() {
148151
return this.cookies;
149152
}
150153

154+
/**
155+
* Return the attributes associated with the request, or an empty map.
156+
*/
157+
public Map<String, Object> getAttributes() {
158+
return this.attributes;
159+
}
160+
151161
/**
152162
* Return the request body as a value to be serialized, if set.
153163
* <p>This is mutually exclusive with {@link #getBody()}.
@@ -209,6 +219,9 @@ public final static class Builder {
209219
@Nullable
210220
private MultiValueMap<String, String> requestParams;
211221

222+
@Nullable
223+
private Map<String, Object> attributes;
224+
212225
@Nullable
213226
private Object bodyValue;
214227

@@ -325,6 +338,17 @@ public Builder addRequestParameter(String name, String... values) {
325338
return this;
326339
}
327340

341+
/**
342+
* Configure an attribute to associate with the request.
343+
* @param name the attribute name
344+
* @param value the attribute value
345+
*/
346+
public Builder addAttribute(String name, Object value) {
347+
this.attributes = (this.attributes != null ? this.attributes : new HashMap<>());
348+
this.attributes.put(name, value);
349+
return this;
350+
}
351+
328352
/**
329353
* Set the request body as a concrete value to be serialized.
330354
* <p>This is mutually exclusive with, and resets any previously set
@@ -388,9 +412,12 @@ else if (uri != null) {
388412
MultiValueMap<String, String> cookies = (this.cookies != null ?
389413
new LinkedMultiValueMap<>(this.cookies) : EMPTY_COOKIES_MAP);
390414

415+
Map<String, Object> attributes = (this.attributes != null ?
416+
new HashMap<>(this.attributes) : Collections.emptyMap());
417+
391418
return new HttpRequestValues(
392-
this.httpMethod, uri, uriTemplate, uriVars, headers, cookies, bodyValue,
393-
this.body, this.bodyElementType);
419+
this.httpMethod, uri, uriTemplate, uriVars, headers, cookies, attributes,
420+
bodyValue, this.body, this.bodyElementType);
394421
}
395422

396423
private String appendQueryParams(

spring-web/src/main/java/org/springframework/web/service/invoker/HttpServiceProxyFactory.java

+7
Original file line numberDiff line numberDiff line change
@@ -185,14 +185,21 @@ private ConversionService initConversionService() {
185185
}
186186

187187
private List<HttpServiceArgumentResolver> initArgumentResolvers(ConversionService conversionService) {
188+
188189
List<HttpServiceArgumentResolver> resolvers = new ArrayList<>(this.customResolvers);
190+
191+
// Annotation-based
189192
resolvers.add(new RequestHeaderArgumentResolver(conversionService));
190193
resolvers.add(new RequestBodyArgumentResolver(this.reactiveAdapterRegistry));
191194
resolvers.add(new PathVariableArgumentResolver(conversionService));
192195
resolvers.add(new RequestParamArgumentResolver(conversionService));
193196
resolvers.add(new CookieValueArgumentResolver(conversionService));
197+
resolvers.add(new RequestAttributeArgumentResolver());
198+
199+
// Specific type
194200
resolvers.add(new UrlArgumentResolver());
195201
resolvers.add(new HttpMethodArgumentResolver());
202+
196203
return resolvers;
197204
}
198205

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.service.invoker;
18+
19+
import org.springframework.core.MethodParameter;
20+
import org.springframework.web.bind.annotation.RequestAttribute;
21+
22+
/**
23+
* {@link HttpServiceArgumentResolver} for {@link RequestAttribute @RequestAttribute}
24+
* annotated arguments.
25+
*
26+
* <p>The argument may be a single variable value or a {@code Map} with multiple
27+
* variables and values.
28+
*
29+
* <p>If the value is required but {@code null}, {@link IllegalArgumentException}
30+
* is raised. The value is not required if:
31+
* <ul>
32+
* <li>{@link RequestAttribute#required()} is set to {@code false}
33+
* <li>The argument is declared as {@link java.util.Optional}
34+
* </ul>
35+
*
36+
* @author Rossen Stoyanchev
37+
* @since 6.0
38+
*/
39+
public class RequestAttributeArgumentResolver extends AbstractNamedValueArgumentResolver {
40+
41+
42+
@Override
43+
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
44+
RequestAttribute annot = parameter.getParameterAnnotation(RequestAttribute.class);
45+
return (annot == null ? null :
46+
new NamedValueInfo(annot.name(), annot.required(), null, "request attribute", false));
47+
}
48+
49+
@Override
50+
protected void addRequestValue(String name, Object value, HttpRequestValues.Builder requestValues) {
51+
requestValues.addAttribute(name, value);
52+
}
53+
54+
}

spring-web/src/test/java/org/springframework/web/service/invoker/PathVariableArgumentResolverTests.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,7 @@ void pathVariable() {
4848

4949
@SuppressWarnings("SameParameterValue")
5050
private void assertPathVariable(String name, @Nullable String expectedValue) {
51-
assertThat(this.client.getRequestValues().getUriVariables().get(name))
52-
.isEqualTo(expectedValue);
51+
assertThat(this.client.getRequestValues().getUriVariables().get(name)).isEqualTo(expectedValue);
5352
}
5453

5554

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.service.invoker;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
import org.springframework.lang.Nullable;
22+
import org.springframework.web.bind.annotation.RequestAttribute;
23+
import org.springframework.web.service.annotation.GetExchange;
24+
25+
import static org.assertj.core.api.Assertions.assertThat;
26+
27+
/**
28+
* Unit tests for {@link RequestAttributeArgumentResolver}.
29+
* <p>For base class functionality, see {@link NamedValueArgumentResolverTests}.
30+
*
31+
* @author Rossen Stoyanchev
32+
*/
33+
class RequestAttributeArgumentResolverTests {
34+
35+
private final TestHttpClientAdapter client = new TestHttpClientAdapter();
36+
37+
private final Service service = HttpServiceProxyFactory.builder(this.client).build().createClient(Service.class);
38+
39+
40+
// Base class functionality should be tested in NamedValueArgumentResolverTests.
41+
42+
@Test
43+
void cookieValue() {
44+
this.service.execute("test");
45+
assertAttribute("attribute", "test");
46+
}
47+
48+
@SuppressWarnings("SameParameterValue")
49+
private void assertAttribute(String name, @Nullable String expectedValue) {
50+
assertThat(this.client.getRequestValues().getAttributes().get(name)).isEqualTo(expectedValue);
51+
}
52+
53+
54+
private interface Service {
55+
56+
@GetExchange
57+
void execute(@RequestAttribute String attribute);
58+
59+
}
60+
61+
}

spring-webflux/src/main/java/org/springframework/web/reactive/function/client/support/WebClientAdapter.java

+1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ else if (requestValues.getUriTemplate() != null) {
103103

104104
bodySpec.headers(headers -> headers.putAll(requestValues.getHeaders()));
105105
bodySpec.cookies(cookies -> cookies.putAll(requestValues.getCookies()));
106+
bodySpec.attributes(attributes -> attributes.putAll(requestValues.getAttributes()));
106107

107108
if (requestValues.getBodyValue() != null) {
108109
bodySpec.bodyValue(requestValues.getBodyValue());

spring-webflux/src/test/java/org/springframework/web/reactive/function/client/support/WebClientHttpServiceProxyTests.java

+44-14
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
import java.io.IOException;
2121
import java.time.Duration;
22+
import java.util.HashMap;
23+
import java.util.Map;
2224
import java.util.function.Consumer;
2325

2426
import okhttp3.mockwebserver.MockResponse;
@@ -29,11 +31,13 @@
2931
import reactor.core.publisher.Mono;
3032
import reactor.test.StepVerifier;
3133

32-
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
34+
import org.springframework.web.bind.annotation.RequestAttribute;
3335
import org.springframework.web.reactive.function.client.WebClient;
3436
import org.springframework.web.service.annotation.GetExchange;
3537
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
3638

39+
import static org.assertj.core.api.Assertions.assertThat;
40+
3741

3842
/**
3943
* Integration tests for {@link HttpServiceProxyFactory HTTP Service proxy}
@@ -45,22 +49,10 @@ public class WebClientHttpServiceProxyTests {
4549

4650
private MockWebServer server;
4751

48-
private TestHttpService httpService;
49-
5052

5153
@BeforeEach
5254
void setUp() {
5355
this.server = new MockWebServer();
54-
WebClient webClient = WebClient
55-
.builder()
56-
.clientConnector(new ReactorClientHttpConnector())
57-
.baseUrl(this.server.url("/").toString())
58-
.build();
59-
60-
WebClientAdapter clientAdapter = new WebClientAdapter(webClient);
61-
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(clientAdapter).build();
62-
63-
this.httpService = proxyFactory.createClient(TestHttpService.class);
6456
}
6557

6658
@SuppressWarnings("ConstantConditions")
@@ -78,12 +70,47 @@ void greeting() {
7870
prepareResponse(response ->
7971
response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
8072

81-
StepVerifier.create(this.httpService.getGreeting())
73+
StepVerifier.create(initHttpService().getGreeting())
8274
.expectNext("Hello Spring!")
8375
.expectComplete()
8476
.verify(Duration.ofSeconds(5));
8577
}
8678

79+
@Test
80+
void greetingWithRequestAttribute() {
81+
82+
Map<String, Object> attributes = new HashMap<>();
83+
84+
WebClient webClient = WebClient.builder()
85+
.baseUrl(this.server.url("/").toString())
86+
.filter((request, next) -> {
87+
attributes.putAll(request.attributes());
88+
return next.exchange(request);
89+
})
90+
.build();
91+
92+
prepareResponse(response ->
93+
response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!"));
94+
95+
StepVerifier.create(initHttpService(webClient).getGreetingWithAttribute("myAttributeValue"))
96+
.expectNext("Hello Spring!")
97+
.expectComplete()
98+
.verify(Duration.ofSeconds(5));
99+
100+
assertThat(attributes).containsEntry("myAttribute", "myAttributeValue");
101+
}
102+
103+
private TestHttpService initHttpService() {
104+
WebClient webClient = WebClient.builder().baseUrl(this.server.url("/").toString()).build();
105+
return initHttpService(webClient);
106+
}
107+
108+
private TestHttpService initHttpService(WebClient webClient) {
109+
WebClientAdapter clientAdapter = new WebClientAdapter(webClient);
110+
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(clientAdapter).build();
111+
return proxyFactory.createClient(TestHttpService.class);
112+
}
113+
87114
private void prepareResponse(Consumer<MockResponse> consumer) {
88115
MockResponse response = new MockResponse();
89116
consumer.accept(response);
@@ -96,6 +123,9 @@ private interface TestHttpService {
96123
@GetExchange("/greeting")
97124
Mono<String> getGreeting();
98125

126+
@GetExchange("/greeting")
127+
Mono<String> getGreetingWithAttribute(@RequestAttribute String myAttribute);
128+
99129
}
100130

101131

0 commit comments

Comments
 (0)