Skip to content

Commit 9786750

Browse files
committed
Improve @RequestAttribute WebFlux resolver
The resolver now takes into account the possibility the attribute itself may be a reactive type. Issue: SPR-16158
1 parent 14f02d7 commit 9786750

File tree

3 files changed

+107
-34
lines changed

3 files changed

+107
-34
lines changed

spring-web/src/test/java/org/springframework/web/method/MvcAnnotationPredicates.java

+37
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.springframework.http.HttpStatus;
2525
import org.springframework.web.bind.annotation.MatrixVariable;
2626
import org.springframework.web.bind.annotation.ModelAttribute;
27+
import org.springframework.web.bind.annotation.RequestAttribute;
2728
import org.springframework.web.bind.annotation.RequestBody;
2829
import org.springframework.web.bind.annotation.RequestMapping;
2930
import org.springframework.web.bind.annotation.RequestMethod;
@@ -62,6 +63,10 @@ public static RequestPartPredicate requestPart() {
6263
return new RequestPartPredicate();
6364
}
6465

66+
public static RequestAttributePredicate requestAttribute() {
67+
return new RequestAttributePredicate();
68+
}
69+
6570
public static MatrixVariablePredicate matrixAttribute() {
6671
return new MatrixVariablePredicate();
6772
}
@@ -256,6 +261,38 @@ public boolean test(Method method) {
256261
}
257262
}
258263

264+
public static class RequestAttributePredicate implements Predicate<MethodParameter> {
265+
266+
private String name;
267+
268+
private boolean required = true;
269+
270+
271+
public RequestAttributePredicate name(String name) {
272+
this.name = name;
273+
return this;
274+
}
275+
276+
public RequestAttributePredicate noName() {
277+
this.name = "";
278+
return this;
279+
}
280+
281+
public RequestAttributePredicate notRequired() {
282+
this.required = false;
283+
return this;
284+
}
285+
286+
287+
@Override
288+
public boolean test(MethodParameter parameter) {
289+
RequestAttribute annotation = parameter.getParameterAnnotation(RequestAttribute.class);
290+
return annotation != null &&
291+
(this.name == null || annotation.name().equals(this.name)) &&
292+
annotation.required() == this.required;
293+
}
294+
}
295+
259296
public static class ResponseStatusPredicate implements Predicate<Method> {
260297

261298
private HttpStatus code = HttpStatus.INTERNAL_SERVER_ERROR;

spring-webflux/src/main/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolver.java

+23-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616

1717
package org.springframework.web.reactive.result.method.annotation;
1818

19+
import reactor.core.publisher.Mono;
20+
1921
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
2022
import org.springframework.core.MethodParameter;
23+
import org.springframework.core.ReactiveAdapter;
2124
import org.springframework.core.ReactiveAdapterRegistry;
2225
import org.springframework.lang.Nullable;
2326
import org.springframework.util.Assert;
@@ -51,7 +54,7 @@ public RequestAttributeMethodArgumentResolver(@Nullable ConfigurableBeanFactory
5154

5255
@Override
5356
public boolean supportsParameter(MethodParameter param) {
54-
return checkAnnotatedParamNoReactiveWrapper(param, RequestAttribute.class, (annot, type) -> true);
57+
return param.hasParameterAnnotation(RequestAttribute.class);
5558
}
5659

5760

@@ -64,7 +67,25 @@ protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
6467

6568
@Override
6669
protected Object resolveNamedValue(String name, MethodParameter parameter, ServerWebExchange exchange) {
67-
return exchange.getAttribute(name);
70+
Object value = exchange.getAttribute(name);
71+
ReactiveAdapter toAdapter = getAdapterRegistry().getAdapter(parameter.getParameterType());
72+
if (toAdapter != null) {
73+
if (value == null) {
74+
Assert.isTrue(toAdapter.supportsEmpty(),
75+
() -> "No request attribute '" + name + "' and target type " +
76+
parameter.getGenericParameterType() + " doesn't support empty values.");
77+
return toAdapter.fromPublisher(Mono.empty());
78+
}
79+
if (parameter.getParameterType().isAssignableFrom(value.getClass())) {
80+
return value;
81+
}
82+
ReactiveAdapter fromAdapter = getAdapterRegistry().getAdapter(value.getClass());
83+
Assert.isTrue(fromAdapter != null,
84+
() -> getClass().getSimpleName() + " doesn't support " +
85+
"reactive type wrapper: " + parameter.getGenericParameterType());
86+
return toAdapter.fromPublisher(fromAdapter.toPublisher(value));
87+
}
88+
return value;
6889
}
6990

7091
@Override

spring-webflux/src/test/java/org/springframework/web/reactive/result/method/annotation/RequestAttributeMethodArgumentResolverTests.java

+47-32
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,24 @@
1616

1717
package org.springframework.web.reactive.result.method.annotation;
1818

19-
import java.lang.reflect.Method;
19+
import java.time.Duration;
2020
import java.util.Optional;
2121

22+
import io.reactivex.Single;
2223
import org.junit.Before;
2324
import org.junit.Test;
2425
import reactor.core.publisher.Mono;
2526
import reactor.test.StepVerifier;
2627

2728
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
28-
import org.springframework.core.DefaultParameterNameDiscoverer;
29-
import org.springframework.core.GenericTypeResolver;
3029
import org.springframework.core.MethodParameter;
3130
import org.springframework.core.ReactiveAdapterRegistry;
32-
import org.springframework.core.annotation.SynthesizingMethodParameter;
3331
import org.springframework.format.support.DefaultFormattingConversionService;
3432
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
3533
import org.springframework.mock.web.test.server.MockServerWebExchange;
36-
import org.springframework.util.ReflectionUtils;
3734
import org.springframework.web.bind.annotation.RequestAttribute;
3835
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
36+
import org.springframework.web.method.ResolvableMethod;
3937
import org.springframework.web.reactive.BindingContext;
4038
import org.springframework.web.server.ServerWebInputException;
4139

@@ -45,7 +43,7 @@
4543
import static org.junit.Assert.assertNull;
4644
import static org.junit.Assert.assertSame;
4745
import static org.junit.Assert.assertTrue;
48-
import static org.junit.Assert.fail;
46+
import static org.springframework.web.method.MvcAnnotationPredicates.requestAttribute;
4947

5048
/**
5149
* Unit tests for {@link RequestAttributeMethodArgumentResolver}.
@@ -58,37 +56,36 @@ public class RequestAttributeMethodArgumentResolverTests {
5856

5957
private final MockServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/"));
6058

61-
private Method handleMethod;
59+
private final ResolvableMethod testMethod = ResolvableMethod.on(getClass())
60+
.named("handleWithRequestAttribute").build();
6261

6362

6463
@Before
6564
public void setup() throws Exception {
6665
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
6766
context.refresh();
68-
ReactiveAdapterRegistry adapterRegistry = new ReactiveAdapterRegistry();
69-
this.resolver = new RequestAttributeMethodArgumentResolver(context.getBeanFactory(), adapterRegistry);
70-
this.handleMethod = ReflectionUtils.findMethod(getClass(), "handleWithRequestAttribute", (Class<?>[]) null);
67+
ReactiveAdapterRegistry registry = new ReactiveAdapterRegistry();
68+
this.resolver = new RequestAttributeMethodArgumentResolver(context.getBeanFactory(), registry);
7169
}
7270

7371

7472
@Test
7573
public void supportsParameter() throws Exception {
76-
assertTrue(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 0)));
77-
assertFalse(this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 4)));
78-
try {
79-
this.resolver.supportsParameter(new MethodParameter(this.handleMethod, 5));
80-
fail();
81-
}
82-
catch (IllegalStateException ex) {
83-
assertTrue("Unexpected error message:\n" + ex.getMessage(),
84-
ex.getMessage().startsWith(
85-
"RequestAttributeMethodArgumentResolver doesn't support reactive type wrapper"));
86-
}
74+
75+
assertTrue(this.resolver.supportsParameter(
76+
this.testMethod.annot(requestAttribute().noName()).arg(Foo.class)));
77+
78+
// SPR-16158
79+
assertTrue(this.resolver.supportsParameter(
80+
this.testMethod.annotPresent(RequestAttribute.class).arg(Mono.class, Foo.class)));
81+
82+
assertFalse(this.resolver.supportsParameter(
83+
this.testMethod.annotNotPresent(RequestAttribute.class).arg()));
8784
}
8885

8986
@Test
9087
public void resolve() throws Exception {
91-
MethodParameter param = initMethodParameter(0);
88+
MethodParameter param = this.testMethod.annot(requestAttribute().noName()).arg(Foo.class);
9289
Mono<Object> mono = this.resolver.resolveArgument(param, new BindingContext(), this.exchange);
9390
StepVerifier.create(mono)
9491
.expectNextCount(0)
@@ -103,7 +100,7 @@ public void resolve() throws Exception {
103100

104101
@Test
105102
public void resolveWithName() throws Exception {
106-
MethodParameter param = initMethodParameter(1);
103+
MethodParameter param = this.testMethod.annot(requestAttribute().name("specialFoo")).arg();
107104
Foo foo = new Foo();
108105
this.exchange.getAttributes().put("specialFoo", foo);
109106
Mono<Object> mono = this.resolver.resolveArgument(param, new BindingContext(), this.exchange);
@@ -112,7 +109,7 @@ public void resolveWithName() throws Exception {
112109

113110
@Test
114111
public void resolveNotRequired() throws Exception {
115-
MethodParameter param = initMethodParameter(2);
112+
MethodParameter param = this.testMethod.annot(requestAttribute().name("foo").notRequired()).arg();
116113
Mono<Object> mono = this.resolver.resolveArgument(param, new BindingContext(), this.exchange);
117114
assertNull(mono.block());
118115

@@ -124,7 +121,7 @@ public void resolveNotRequired() throws Exception {
124121

125122
@Test
126123
public void resolveOptional() throws Exception {
127-
MethodParameter param = initMethodParameter(3);
124+
MethodParameter param = this.testMethod.annot(requestAttribute().name("foo")).arg(Optional.class, Foo.class);
128125
Mono<Object> mono = this.resolver.resolveArgument(param, new BindingContext(), this.exchange);
129126

130127
assertNotNull(mono.block());
@@ -146,12 +143,30 @@ public void resolveOptional() throws Exception {
146143
assertSame(foo, optional.get());
147144
}
148145

146+
@Test // SPR-16158
147+
public void resolveMonoParameter() throws Exception {
148+
MethodParameter param = this.testMethod.annot(requestAttribute().noName()).arg(Mono.class, Foo.class);
149149

150-
private MethodParameter initMethodParameter(int parameterIndex) {
151-
MethodParameter param = new SynthesizingMethodParameter(this.handleMethod, parameterIndex);
152-
param.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
153-
GenericTypeResolver.resolveParameterType(param, this.resolver.getClass());
154-
return param;
150+
// Mono attribute
151+
Foo foo = new Foo();
152+
Mono<Foo> fooMono = Mono.just(foo);
153+
this.exchange.getAttributes().put("fooMono", fooMono);
154+
Mono<Object> mono = this.resolver.resolveArgument(param, new BindingContext(), this.exchange);
155+
assertSame(fooMono, mono.block(Duration.ZERO));
156+
157+
// RxJava Single attribute
158+
Single<Foo> singleMono = Single.just(foo);
159+
this.exchange.getAttributes().clear();
160+
this.exchange.getAttributes().put("fooMono", singleMono);
161+
mono = this.resolver.resolveArgument(param, new BindingContext(), this.exchange);
162+
Object value = mono.block(Duration.ZERO);
163+
assertTrue(value instanceof Mono);
164+
assertSame(foo, ((Mono<?>) value).block(Duration.ZERO));
165+
166+
// No attribute --> Mono.empty
167+
this.exchange.getAttributes().clear();
168+
mono = this.resolver.resolveArgument(param, new BindingContext(), this.exchange);
169+
assertSame(Mono.empty(), mono.block(Duration.ZERO));
155170
}
156171

157172

@@ -161,8 +176,8 @@ private void handleWithRequestAttribute(
161176
@RequestAttribute("specialFoo") Foo namedFoo,
162177
@RequestAttribute(name="foo", required = false) Foo notRequiredFoo,
163178
@RequestAttribute(name="foo") Optional<Foo> optionalFoo,
164-
String notSupported,
165-
@RequestAttribute Mono<Foo> alsoNotSupported) {
179+
@RequestAttribute Mono<Foo> fooMono,
180+
String notSupported) {
166181
}
167182

168183

0 commit comments

Comments
 (0)