Skip to content

Commit fed6e9b

Browse files
committed
Merge branch '6.2.x'
2 parents 98cdae6 + c48ff35 commit fed6e9b

File tree

9 files changed

+176
-31
lines changed

9 files changed

+176
-31
lines changed

spring-test/src/main/java/org/springframework/mock/web/server/MockServerWebExchange.java

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.mock.web.server;
1818

19+
import java.security.Principal;
20+
1921
import org.jspecify.annotations.Nullable;
2022
import reactor.core.publisher.Mono;
2123

@@ -40,15 +42,19 @@
4042
*/
4143
public final class MockServerWebExchange extends DefaultServerWebExchange {
4244

45+
private final Mono<Principal> principalMono;
46+
4347

4448
private MockServerWebExchange(
4549
MockServerHttpRequest request, @Nullable WebSessionManager sessionManager,
46-
@Nullable ApplicationContext applicationContext) {
50+
@Nullable ApplicationContext applicationContext, @Nullable Principal principal) {
4751

4852
super(request, new MockServerHttpResponse(),
4953
sessionManager != null ? sessionManager : new DefaultWebSessionManager(),
5054
ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver(),
5155
applicationContext);
56+
57+
this.principalMono = (principal != null) ? Mono.just(principal) : Mono.empty();
5258
}
5359

5460

@@ -57,6 +63,16 @@ public MockServerHttpResponse getResponse() {
5763
return (MockServerHttpResponse) super.getResponse();
5864
}
5965

66+
/**
67+
* Return the user set via {@link Builder#principal(Principal)}.
68+
* @since 6.2.7
69+
*/
70+
@SuppressWarnings("unchecked")
71+
@Override
72+
public <T extends Principal> Mono<T> getPrincipal() {
73+
return (Mono<T>) this.principalMono;
74+
}
75+
6076

6177
/**
6278
* Create a {@link MockServerWebExchange} from the given mock request.
@@ -107,8 +123,9 @@ public static class Builder {
107123

108124
private @Nullable WebSessionManager sessionManager;
109125

110-
@Nullable
111-
private ApplicationContext applicationContext;
126+
private @Nullable ApplicationContext applicationContext;
127+
128+
private @Nullable Principal principal;
112129

113130
public Builder(MockServerHttpRequest request) {
114131
this.request = request;
@@ -146,11 +163,22 @@ public Builder applicationContext(ApplicationContext applicationContext) {
146163
return this;
147164
}
148165

166+
/**
167+
* Provide a user to associate with the exchange.
168+
* @param principal the principal to use
169+
* @since 6.2.7
170+
*/
171+
public Builder principal(@Nullable Principal principal) {
172+
this.principal = principal;
173+
return this;
174+
}
175+
149176
/**
150177
* Build the {@code MockServerWebExchange} instance.
151178
*/
152179
public MockServerWebExchange build() {
153-
return new MockServerWebExchange(this.request, this.sessionManager, this.applicationContext);
180+
return new MockServerWebExchange(
181+
this.request, this.sessionManager, this.applicationContext, this.principal);
154182
}
155183
}
156184

spring-web/src/main/java/org/springframework/web/client/support/RestClientAdapter.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ public <T> ResponseEntity<T> exchangeForEntity(HttpRequestValues values, Paramet
8585
return newRequest(values).retrieve().toEntity(bodyType);
8686
}
8787

88-
private RestClient.RequestBodySpec newRequest(HttpRequestValues values) {
88+
@SuppressWarnings("unchecked")
89+
private <B> RestClient.RequestBodySpec newRequest(HttpRequestValues values) {
8990

9091
HttpMethod httpMethod = values.getHttpMethod();
9192
Assert.notNull(httpMethod, "HttpMethod is required");
@@ -127,8 +128,14 @@ else if (values.getUriTemplate() != null) {
127128

128129
bodySpec.attributes(attributes -> attributes.putAll(values.getAttributes()));
129130

130-
if (values.getBodyValue() != null) {
131-
bodySpec.body(values.getBodyValue());
131+
B body = (B) values.getBodyValue();
132+
if (body != null) {
133+
if (values.getBodyValueType() != null) {
134+
bodySpec.body(body, (ParameterizedTypeReference<? super B>) values.getBodyValueType());
135+
}
136+
else {
137+
bodySpec.body(body);
138+
}
132139
}
133140

134141
return bodySpec;

spring-web/src/main/java/org/springframework/web/client/support/RestTemplateAdapter.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -86,7 +86,7 @@ public <T> ResponseEntity<T> exchangeForEntity(HttpRequestValues values, Paramet
8686
return this.restTemplate.exchange(newRequest(values), bodyType);
8787
}
8888

89-
private RequestEntity<?> newRequest(HttpRequestValues values) {
89+
private <B> RequestEntity<?> newRequest(HttpRequestValues values) {
9090
HttpMethod httpMethod = values.getHttpMethod();
9191
Assert.notNull(httpMethod, "HttpMethod is required");
9292

@@ -120,11 +120,16 @@ else if (values.getUriTemplate() != null) {
120120
builder.header(HttpHeaders.COOKIE, String.join("; ", cookies));
121121
}
122122

123-
if (values.getBodyValue() != null) {
124-
return builder.body(values.getBodyValue());
123+
Object body = values.getBodyValue();
124+
if (body == null) {
125+
return builder.build();
125126
}
126127

127-
return builder.build();
128+
if (values.getBodyValueType() != null) {
129+
return builder.body(body, values.getBodyValueType().getType());
130+
}
131+
132+
return builder.body(body);
128133
}
129134

130135

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import org.jspecify.annotations.Nullable;
2828

29+
import org.springframework.core.ParameterizedTypeReference;
2930
import org.springframework.http.HttpEntity;
3031
import org.springframework.http.HttpHeaders;
3132
import org.springframework.http.HttpMethod;
@@ -74,6 +75,8 @@ public class HttpRequestValues {
7475

7576
private final @Nullable Object bodyValue;
7677

78+
private @Nullable ParameterizedTypeReference<?> bodyValueType;
79+
7780

7881
/**
7982
* Construct {@link HttpRequestValues}.
@@ -176,6 +179,14 @@ public Map<String, Object> getAttributes() {
176179
return this.bodyValue;
177180
}
178181

182+
/**
183+
* Return the type for the {@linkplain #getBodyValue() body value}.
184+
* @since 6.2.7
185+
*/
186+
public @Nullable ParameterizedTypeReference<?> getBodyValueType() {
187+
return this.bodyValueType;
188+
}
189+
179190

180191
/**
181192
* Return a builder for {@link HttpRequestValues}.
@@ -264,6 +275,8 @@ public static class Builder implements Metadata {
264275

265276
private @Nullable Object bodyValue;
266277

278+
private @Nullable ParameterizedTypeReference<?> bodyValueType;
279+
267280
protected Builder() {
268281
}
269282

@@ -417,6 +430,15 @@ public void setBodyValue(@Nullable Object bodyValue) {
417430
this.bodyValue = bodyValue;
418431
}
419432

433+
/**
434+
* Variant of {@link #setBodyValue(Object)} with the body type.
435+
* @since 6.2.7
436+
*/
437+
public void setBodyValue(@Nullable Object bodyValue, @Nullable ParameterizedTypeReference<?> valueType) {
438+
setBodyValue(bodyValue);
439+
this.bodyValueType = valueType;
440+
}
441+
420442

421443
// Implementation of {@link Metadata} methods
422444

@@ -489,9 +511,14 @@ else if (uri != null) {
489511
Map<String, Object> attributes = (this.attributes != null ?
490512
new HashMap<>(this.attributes) : Collections.emptyMap());
491513

492-
return createRequestValues(
514+
HttpRequestValues requestValues = createRequestValues(
493515
this.httpMethod, uri, uriBuilderFactory, uriTemplate, uriVars,
494516
headers, cookies, this.version, attributes, bodyValue);
517+
518+
// In 6.2.x only, temporarily work around protected methods
519+
requestValues.bodyValueType = this.bodyValueType;
520+
521+
return requestValues;
495522
}
496523

497524
protected boolean hasParts() {

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

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -83,15 +83,16 @@ public boolean resolve(
8383
if (this.reactiveAdapterRegistry != null) {
8484
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(parameter.getParameterType());
8585
if (adapter != null) {
86-
MethodParameter nestedParameter = parameter.nested();
86+
MethodParameter nestedParam = parameter.nested();
8787

8888
String message = "Async type for @RequestBody should produce value(s)";
8989
Assert.isTrue(!adapter.isNoValue(), message);
90-
Assert.isTrue(nestedParameter.getNestedParameterType() != Void.class, message);
90+
Assert.isTrue(nestedParam.getNestedParameterType() != Void.class, message);
9191

92-
if (requestValues instanceof ReactiveHttpRequestValues.Builder reactiveRequestValues) {
93-
reactiveRequestValues.setBodyPublisher(
94-
adapter.toPublisher(argument), asParameterizedTypeRef(nestedParameter));
92+
if (requestValues instanceof ReactiveHttpRequestValues.Builder rrv) {
93+
rrv.setBodyPublisher(
94+
adapter.toPublisher(argument),
95+
ParameterizedTypeReference.forType(nestedParam.getNestedGenericParameterType()));
9596
}
9697
else {
9798
throw new IllegalStateException(
@@ -103,12 +104,8 @@ public boolean resolve(
103104
}
104105

105106
// Not a reactive type
106-
requestValues.setBodyValue(argument);
107+
requestValues.setBodyValue(argument, ParameterizedTypeReference.forType(parameter.getGenericParameterType()));
107108
return true;
108109
}
109110

110-
private static ParameterizedTypeReference<Object> asParameterizedTypeRef(MethodParameter nestedParam) {
111-
return ParameterizedTypeReference.forType(nestedParam.getNestedGenericParameterType());
112-
}
113-
114111
}

spring-web/src/test/java/org/springframework/web/client/support/RestClientAdapterTests.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@
2222
import java.lang.annotation.RetentionPolicy;
2323
import java.lang.annotation.Target;
2424
import java.net.URI;
25+
import java.util.LinkedHashSet;
2526
import java.util.Optional;
27+
import java.util.Set;
2628
import java.util.function.BiFunction;
2729
import java.util.stream.Stream;
2830

@@ -284,6 +286,19 @@ void apiVersion() throws Exception {
284286
assertThat(request.getHeader("X-API-Version")).isEqualTo("1.2");
285287
}
286288

289+
@ParameterizedAdapterTest // gh-34793
290+
void postSet(MockWebServer server, Service service) throws InterruptedException {
291+
Set<Person> persons = new LinkedHashSet<>();
292+
persons.add(new Person("John"));
293+
persons.add(new Person("Richard"));
294+
service.postPersonSet(persons);
295+
296+
RecordedRequest request = server.takeRequest();
297+
assertThat(request.getMethod()).isEqualTo("POST");
298+
assertThat(request.getPath()).isEqualTo("/persons");
299+
assertThat(request.getBody().readUtf8()).isEqualTo("[{\"name\":\"John\"},{\"name\":\"Richard\"}]");
300+
}
301+
287302

288303
private static MockWebServer anotherServer() {
289304
MockWebServer server = new MockWebServer();
@@ -317,6 +332,9 @@ private interface Service {
317332
@PostExchange
318333
void postMultipart(MultipartFile file, @RequestPart String anotherPart);
319334

335+
@PostExchange(url = "/persons", contentType = MediaType.APPLICATION_JSON_VALUE)
336+
void postPersonSet(@RequestBody Set<Person> set);
337+
320338
@PutExchange
321339
void putWithCookies(@CookieValue String firstCookie, @CookieValue String secondCookie);
322340

@@ -335,4 +353,19 @@ ResponseEntity<String> getWithUriBuilderFactory(
335353
ResponseEntity<String> getWithIgnoredUriBuilderFactory(URI uri, UriBuilderFactory uriBuilderFactory);
336354
}
337355

356+
357+
static final class Person {
358+
359+
private final String name;
360+
361+
Person(String name) {
362+
this.name = name;
363+
}
364+
365+
public String getName() {
366+
return this.name;
367+
}
368+
369+
}
370+
338371
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2024 the original author or authors.
2+
* Copyright 2002-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -54,6 +54,7 @@ void stringBody() {
5454
this.service.execute(body);
5555

5656
assertThat(getBodyValue()).isEqualTo(body);
57+
assertThat(getBodyValueType()).isEqualTo(new ParameterizedTypeReference<String>() {});
5758
assertThat(getPublisherBody()).isNull();
5859
}
5960

@@ -172,6 +173,10 @@ void optionalStringBody() {
172173
return getReactiveRequestValues().getBodyValue();
173174
}
174175

176+
private @Nullable ParameterizedTypeReference<?> getBodyValueType() {
177+
return getReactiveRequestValues().getBodyValueType();
178+
}
179+
175180
private @Nullable Publisher<?> getPublisherBody() {
176181
return getReactiveRequestValues().getBodyPublisher();
177182
}

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ public <T> Mono<ResponseEntity<Flux<T>>> exchangeForEntityFlux(HttpRequestValues
9898
return newRequest(requestValues).retrieve().toEntityFlux(bodyType);
9999
}
100100

101-
@SuppressWarnings("ReactiveStreamsUnusedPublisher")
102-
private WebClient.RequestBodySpec newRequest(HttpRequestValues values) {
101+
@SuppressWarnings({"ReactiveStreamsUnusedPublisher", "unchecked"})
102+
private <B> WebClient.RequestBodySpec newRequest(HttpRequestValues values) {
103103

104104
HttpMethod httpMethod = values.getHttpMethod();
105105
Assert.notNull(httpMethod, "HttpMethod is required");
@@ -135,12 +135,18 @@ else if (values.getUriTemplate() != null) {
135135
bodySpec.attributes(attributes -> attributes.putAll(values.getAttributes()));
136136

137137
if (values.getBodyValue() != null) {
138-
bodySpec.bodyValue(values.getBodyValue());
138+
if (values.getBodyValueType() != null) {
139+
B body = (B) values.getBodyValue();
140+
bodySpec.bodyValue(body, (ParameterizedTypeReference<B>) values.getBodyValueType());
141+
}
142+
else {
143+
bodySpec.bodyValue(values.getBodyValue());
144+
}
139145
}
140-
else if (values instanceof ReactiveHttpRequestValues reactiveRequestValues) {
141-
Publisher<?> body = reactiveRequestValues.getBodyPublisher();
146+
else if (values instanceof ReactiveHttpRequestValues rhrv) {
147+
Publisher<?> body = rhrv.getBodyPublisher();
142148
if (body != null) {
143-
ParameterizedTypeReference<?> elementType = reactiveRequestValues.getBodyPublisherElementType();
149+
ParameterizedTypeReference<?> elementType = rhrv.getBodyPublisherElementType();
144150
Assert.notNull(elementType, "Publisher body element type is required");
145151
bodySpec.body(body, elementType);
146152
}

0 commit comments

Comments
 (0)