Skip to content

Commit 6b73700

Browse files
committed
Reactive support for @ModelAttribute methods
Issue: SPR-14542
1 parent e59dced commit 6b73700

File tree

9 files changed

+506
-80
lines changed

9 files changed

+506
-80
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/*
2+
* Copyright 2002-2016 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+
* http://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+
package org.springframework.web.reactive.result.method.annotation;
17+
18+
import java.lang.reflect.Method;
19+
import java.util.Arrays;
20+
import java.util.List;
21+
import java.util.stream.Collectors;
22+
import java.util.stream.Stream;
23+
24+
import reactor.core.publisher.Mono;
25+
26+
import org.springframework.core.MethodParameter;
27+
import org.springframework.core.ReactiveAdapter;
28+
import org.springframework.core.ReactiveAdapterRegistry;
29+
import org.springframework.core.ResolvableType;
30+
import org.springframework.core.annotation.AnnotatedElementUtils;
31+
import org.springframework.util.ClassUtils;
32+
import org.springframework.util.StringUtils;
33+
import org.springframework.web.bind.annotation.ModelAttribute;
34+
import org.springframework.web.bind.support.WebBindingInitializer;
35+
import org.springframework.web.method.HandlerMethod;
36+
import org.springframework.web.reactive.HandlerResult;
37+
import org.springframework.web.reactive.result.method.BindingContext;
38+
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
39+
import org.springframework.web.reactive.result.method.InvocableHandlerMethod;
40+
import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver;
41+
import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod;
42+
import org.springframework.web.server.ServerWebExchange;
43+
44+
/**
45+
* A helper class for {@link RequestMappingHandlerAdapter} that assists with
46+
* creating a {@code BindingContext} and initialize it, and its model, through
47+
* {@code @InitBinder} and {@code @ModelAttribute} methods.
48+
*
49+
* @author Rossen Stoyanchev
50+
* @since 5.0
51+
*/
52+
class BindingContextFactory {
53+
54+
private final RequestMappingHandlerAdapter adapter;
55+
56+
57+
public BindingContextFactory(RequestMappingHandlerAdapter adapter) {
58+
this.adapter = adapter;
59+
}
60+
61+
62+
public RequestMappingHandlerAdapter getAdapter() {
63+
return this.adapter;
64+
}
65+
66+
private WebBindingInitializer getBindingInitializer() {
67+
return getAdapter().getWebBindingInitializer();
68+
}
69+
70+
private List<SyncHandlerMethodArgumentResolver> getInitBinderArgumentResolvers() {
71+
return getAdapter().getInitBinderArgumentResolvers();
72+
}
73+
74+
private List<HandlerMethodArgumentResolver> getArgumentResolvers() {
75+
return getAdapter().getArgumentResolvers();
76+
}
77+
78+
private ReactiveAdapterRegistry getAdapterRegistry() {
79+
return getAdapter().getReactiveAdapterRegistry();
80+
}
81+
82+
private Stream<Method> getInitBinderMethods(HandlerMethod handlerMethod) {
83+
return getAdapter().getInitBinderMethods(handlerMethod.getBeanType()).stream();
84+
}
85+
86+
private Stream<Method> getModelAttributeMethods(HandlerMethod handlerMethod) {
87+
return getAdapter().getModelAttributeMethods(handlerMethod.getBeanType()).stream();
88+
}
89+
90+
91+
/**
92+
* Create and initialize a BindingContext for the current request.
93+
* @param handlerMethod the request handling method
94+
* @param exchange the current exchange
95+
* @return Mono with the BindingContext instance
96+
*/
97+
public Mono<BindingContext> createBindingContext(HandlerMethod handlerMethod,
98+
ServerWebExchange exchange) {
99+
100+
List<SyncInvocableHandlerMethod> invocableMethods = getInitBinderMethods(handlerMethod)
101+
.map(method -> {
102+
Object bean = handlerMethod.getBean();
103+
SyncInvocableHandlerMethod invocable = new SyncInvocableHandlerMethod(bean, method);
104+
invocable.setSyncArgumentResolvers(getInitBinderArgumentResolvers());
105+
return invocable;
106+
})
107+
.collect(Collectors.toList());
108+
109+
BindingContext bindingContext =
110+
new InitBinderBindingContext(getBindingInitializer(), invocableMethods);
111+
112+
return initModel(handlerMethod, bindingContext, exchange).then(Mono.just(bindingContext));
113+
}
114+
115+
@SuppressWarnings("Convert2MethodRef")
116+
private Mono<Void> initModel(HandlerMethod handlerMethod, BindingContext context,
117+
ServerWebExchange exchange) {
118+
119+
List<Mono<HandlerResult>> resultMonos = getModelAttributeMethods(handlerMethod)
120+
.map(method -> {
121+
Object bean = handlerMethod.getBean();
122+
InvocableHandlerMethod invocable = new InvocableHandlerMethod(bean, method);
123+
invocable.setArgumentResolvers(getArgumentResolvers());
124+
return invocable;
125+
})
126+
.map(invocable -> invocable.invoke(exchange, context))
127+
.collect(Collectors.toList());
128+
129+
return Mono
130+
.when(resultMonos, resultArr -> processModelMethodMonos(resultArr, context))
131+
.then(voidMonos -> Mono.when(voidMonos));
132+
}
133+
134+
private List<Mono<Void>> processModelMethodMonos(Object[] resultArr, BindingContext context) {
135+
return Arrays.stream(resultArr)
136+
.map(result -> processModelMethodResult((HandlerResult) result, context))
137+
.collect(Collectors.toList());
138+
}
139+
140+
private Mono<Void> processModelMethodResult(HandlerResult result, BindingContext context) {
141+
Object value = result.getReturnValue().orElse(null);
142+
if (value == null) {
143+
return Mono.empty();
144+
}
145+
146+
ResolvableType type = result.getReturnType();
147+
ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(type.getRawClass(), value);
148+
Class<?> valueType = (adapter != null ? type.resolveGeneric(0) : type.resolve());
149+
150+
if (Void.class.equals(valueType) || void.class.equals(valueType)) {
151+
return (adapter != null ? adapter.toMono(value) : Mono.empty());
152+
}
153+
154+
String name = getAttributeName(valueType, result.getReturnTypeSource());
155+
context.getModel().asMap().putIfAbsent(name, value);
156+
return Mono.empty();
157+
}
158+
159+
private String getAttributeName(Class<?> valueType, MethodParameter parameter) {
160+
Method method = parameter.getMethod();
161+
ModelAttribute annot = AnnotatedElementUtils.findMergedAnnotation(method, ModelAttribute.class);
162+
if (annot != null && StringUtils.hasText(annot.value())) {
163+
return annot.value();
164+
}
165+
// TODO: Conventions does not deal with async wrappers
166+
return ClassUtils.getShortNameAsProperty(valueType);
167+
}
168+
169+
}

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,20 +60,28 @@
6060
*/
6161
public class ModelAttributeMethodArgumentResolver implements HandlerMethodArgumentResolver {
6262

63-
private final boolean useDefaultResolution;
64-
6563
private final ReactiveAdapterRegistry adapterRegistry;
6664

65+
private final boolean useDefaultResolution;
66+
6767

6868
/**
6969
* Class constructor.
70+
* @param registry for adapting to other reactive types from and to Mono
71+
*/
72+
public ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry registry) {
73+
this(registry, false);
74+
}
75+
76+
/**
77+
* Class constructor with a default resolution mode flag.
78+
* @param registry for adapting to other reactive types from and to Mono
7079
* @param useDefaultResolution if "true", non-simple method arguments and
7180
* return values are considered model attributes with or without a
7281
* {@code @ModelAttribute} annotation present.
73-
* @param registry for adapting to other reactive types from and to Mono
7482
*/
75-
public ModelAttributeMethodArgumentResolver(boolean useDefaultResolution,
76-
ReactiveAdapterRegistry registry) {
83+
public ModelAttributeMethodArgumentResolver(ReactiveAdapterRegistry registry,
84+
boolean useDefaultResolution) {
7785

7886
Assert.notNull(registry, "'ReactiveAdapterRegistry' is required.");
7987
this.useDefaultResolution = useDefaultResolution;

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

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
import org.springframework.http.codec.HttpMessageReader;
4343
import org.springframework.util.ReflectionUtils;
4444
import org.springframework.web.bind.annotation.InitBinder;
45+
import org.springframework.web.bind.annotation.ModelAttribute;
46+
import org.springframework.web.bind.annotation.RequestMapping;
4547
import org.springframework.web.bind.support.WebBindingInitializer;
4648
import org.springframework.web.method.HandlerMethod;
4749
import org.springframework.web.method.annotation.ExceptionHandlerMethodResolver;
@@ -51,7 +53,6 @@
5153
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
5254
import org.springframework.web.reactive.result.method.InvocableHandlerMethod;
5355
import org.springframework.web.reactive.result.method.SyncHandlerMethodArgumentResolver;
54-
import org.springframework.web.reactive.result.method.SyncInvocableHandlerMethod;
5556
import org.springframework.web.server.ServerWebExchange;
5657

5758
/**
@@ -82,8 +83,12 @@ public class RequestMappingHandlerAdapter implements HandlerAdapter, BeanFactory
8283
private ConfigurableBeanFactory beanFactory;
8384

8485

86+
private final BindingContextFactory bindingContextFactory = new BindingContextFactory(this);
87+
8588
private final Map<Class<?>, Set<Method>> initBinderCache = new ConcurrentHashMap<>(64);
8689

90+
private final Map<Class<?>, Set<Method>> modelAttributeCache = new ConcurrentHashMap<>(64);
91+
8792
private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache =
8893
new ConcurrentHashMap<>(64);
8994

@@ -225,11 +230,12 @@ protected List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
225230
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
226231

227232
// Annotation-based argument resolution
228-
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
233+
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory()));
229234
resolvers.add(new RequestParamMapMethodArgumentResolver());
230235
resolvers.add(new PathVariableMethodArgumentResolver(getBeanFactory()));
231236
resolvers.add(new PathVariableMapMethodArgumentResolver());
232237
resolvers.add(new RequestBodyArgumentResolver(getMessageReaders(), getReactiveAdapterRegistry()));
238+
resolvers.add(new ModelAttributeMethodArgumentResolver(getReactiveAdapterRegistry()));
233239
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
234240
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
235241
resolvers.add(new CookieValueMethodArgumentResolver(getBeanFactory()));
@@ -240,6 +246,7 @@ protected List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
240246
// Type-based argument resolution
241247
resolvers.add(new HttpEntityArgumentResolver(getMessageReaders(), getReactiveAdapterRegistry()));
242248
resolvers.add(new ModelArgumentResolver());
249+
resolvers.add(new ErrorsMethodArgumentResolver(getReactiveAdapterRegistry()));
243250
resolvers.add(new ServerWebExchangeArgumentResolver());
244251
resolvers.add(new PrincipalArgumentResolver());
245252
resolvers.add(new WebSessionArgumentResolver());
@@ -251,6 +258,7 @@ protected List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
251258

252259
// Catch-all
253260
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
261+
resolvers.add(new ModelAttributeMethodArgumentResolver(getReactiveAdapterRegistry(), true));
254262
return resolvers;
255263
}
256264

@@ -290,34 +298,31 @@ public boolean supports(Object handler) {
290298

291299
@Override
292300
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
301+
293302
HandlerMethod handlerMethod = (HandlerMethod) handler;
294303
InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod);
295304
invocable.setArgumentResolvers(getArgumentResolvers());
296-
BindingContext bindingContext = getBindingContext(handlerMethod);
297-
return invocable.invoke(exchange, bindingContext)
298-
.map(result -> result.setExceptionHandler(
299-
ex -> handleException(ex, handlerMethod, bindingContext, exchange)))
300-
.otherwise(ex -> handleException(
301-
ex, handlerMethod, bindingContext, exchange));
305+
306+
Mono<BindingContext> bindingContextMono =
307+
this.bindingContextFactory.createBindingContext(handlerMethod, exchange);
308+
309+
return bindingContextMono.then(bindingContext ->
310+
invocable.invoke(exchange, bindingContext)
311+
.doOnNext(result -> result.setExceptionHandler(
312+
ex -> handleException(ex, handlerMethod, bindingContext, exchange)))
313+
.otherwise(ex -> handleException(
314+
ex, handlerMethod, bindingContext, exchange)));
302315
}
303316

304-
private BindingContext getBindingContext(HandlerMethod handlerMethod) {
305-
Class<?> handlerType = handlerMethod.getBeanType();
306-
Set<Method> methods = this.initBinderCache.get(handlerType);
307-
if (methods == null) {
308-
methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
309-
this.initBinderCache.put(handlerType, methods);
310-
}
311-
List<SyncInvocableHandlerMethod> initBinderMethods = new ArrayList<>();
312-
for (Method method : methods) {
313-
Object bean = handlerMethod.getBean();
314-
SyncInvocableHandlerMethod initBinderMethod = new SyncInvocableHandlerMethod(bean, method);
315-
initBinderMethod.setSyncArgumentResolvers(getInitBinderArgumentResolvers());
316-
initBinderMethods.add(initBinderMethod);
317-
}
318-
return new InitBinderBindingContext(getWebBindingInitializer(), initBinderMethods);
317+
Set<Method> getInitBinderMethods(Class<?> handlerType) {
318+
return this.initBinderCache.computeIfAbsent(handlerType, aClass ->
319+
MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS));
319320
}
320321

322+
Set<Method> getModelAttributeMethods(Class<?> handlerType) {
323+
return this.modelAttributeCache.computeIfAbsent(handlerType, aClass ->
324+
MethodIntrospector.selectMethods(handlerType, MODEL_ATTRIBUTE_METHODS));
325+
}
321326

322327
private Mono<HandlerResult> handleException(Throwable ex, HandlerMethod handlerMethod,
323328
BindingContext bindingContext, ServerWebExchange exchange) {
@@ -360,7 +365,14 @@ protected InvocableHandlerMethod findExceptionHandler(HandlerMethod handlerMetho
360365
/**
361366
* MethodFilter that matches {@link InitBinder @InitBinder} methods.
362367
*/
363-
public static final ReflectionUtils.MethodFilter INIT_BINDER_METHODS =
364-
method -> AnnotationUtils.findAnnotation(method, InitBinder.class) != null;
368+
public static final ReflectionUtils.MethodFilter INIT_BINDER_METHODS = method ->
369+
AnnotationUtils.findAnnotation(method, InitBinder.class) != null;
370+
371+
/**
372+
* MethodFilter that matches {@link ModelAttribute @ModelAttribute} methods.
373+
*/
374+
public static final ReflectionUtils.MethodFilter MODEL_ATTRIBUTE_METHODS = method ->
375+
(AnnotationUtils.findAnnotation(method, RequestMapping.class) == null) &&
376+
(AnnotationUtils.findAnnotation(method, ModelAttribute.class) != null);
365377

366378
}

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

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@
2020
import java.util.Map;
2121
import java.util.Optional;
2222

23-
import reactor.core.publisher.Mono;
24-
2523
import org.springframework.beans.BeanUtils;
2624
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
2725
import org.springframework.core.MethodParameter;
@@ -57,6 +55,17 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueSyncAr
5755

5856

5957
/**
58+
* Class constructor.
59+
* @param beanFactory a bean factory used for resolving ${...} placeholder
60+
* and #{...} SpEL expressions in default values, or {@code null} if default
61+
* values are not expected to contain expressions
62+
*/
63+
public RequestParamMethodArgumentResolver(ConfigurableBeanFactory beanFactory) {
64+
this(beanFactory, false);
65+
}
66+
67+
/**
68+
* Class constructor with a default resolution mode flag.
6069
* @param beanFactory a bean factory used for resolving ${...} placeholder
6170
* and #{...} SpEL expressions in default values, or {@code null} if default
6271
* values are not expected to contain expressions
@@ -65,7 +74,9 @@ public class RequestParamMethodArgumentResolver extends AbstractNamedValueSyncAr
6574
* is treated as a request parameter even if it isn't annotated, the
6675
* request parameter name is derived from the method parameter name.
6776
*/
68-
public RequestParamMethodArgumentResolver(ConfigurableBeanFactory beanFactory, boolean useDefaultResolution) {
77+
public RequestParamMethodArgumentResolver(ConfigurableBeanFactory beanFactory,
78+
boolean useDefaultResolution) {
79+
6980
super(beanFactory);
7081
this.useDefaultResolution = useDefaultResolution;
7182
}

spring-web-reactive/src/main/java/org/springframework/web/reactive/result/view/ViewResolutionResultHandler.java

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -191,15 +191,11 @@ public Mono<Void> handleResult(ServerWebExchange exchange, HandlerResult result)
191191
ReactiveAdapter adapter = getAdapterRegistry().getAdapterFrom(parameterType.getRawClass(), optional);
192192

193193
if (adapter != null) {
194-
if (optional.isPresent()) {
195-
Mono<?> converted = adapter.toMono(optional);
196-
returnValueMono = converted.map(o -> o);
197-
}
198-
else {
199-
returnValueMono = Mono.empty();
200-
}
201-
elementType = adapter.getDescriptor().isNoValue() ?
202-
ResolvableType.forClass(Void.class) : parameterType.getGeneric(0);
194+
returnValueMono = optional
195+
.map(value -> adapter.toMono(value).cast(Object.class))
196+
.orElse(Mono.empty());
197+
elementType = !adapter.getDescriptor().isNoValue() ?
198+
parameterType.getGeneric(0) : ResolvableType.forClass(Void.class);
203199
}
204200
else {
205201
returnValueMono = Mono.justOrEmpty(result.getReturnValue());

0 commit comments

Comments
 (0)