Skip to content

Commit 74972fb

Browse files
committed
Add updateModel to BindingContext
The method includes logic that is currently in ViewResolutionResultHandler but fits well in BindingContext and also includes the call to saveModel method from the InitBinderBindingContext subclass, which was called too early until now from RequestMappingHandlerAdapter before the model has been fully updated. This mirrors a similar method in ModelFactory on the Spring MVC side which also combines those two tasks. Closes gh-30821
1 parent 15b6626 commit 74972fb

File tree

6 files changed

+68
-47
lines changed

6 files changed

+68
-47
lines changed

spring-webflux/src/main/java/org/springframework/web/reactive/BindingContext.java

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,19 @@
1717
package org.springframework.web.reactive;
1818

1919
import java.lang.annotation.Annotation;
20+
import java.util.Collection;
2021
import java.util.Collections;
2122
import java.util.Map;
2223

2324
import reactor.core.publisher.Mono;
2425

26+
import org.springframework.beans.BeanUtils;
2527
import org.springframework.core.MethodParameter;
2628
import org.springframework.core.ReactiveAdapterRegistry;
2729
import org.springframework.core.ResolvableType;
2830
import org.springframework.lang.Nullable;
2931
import org.springframework.ui.Model;
32+
import org.springframework.validation.BindingResult;
3033
import org.springframework.validation.DataBinder;
3134
import org.springframework.validation.support.BindingAwareConcurrentModel;
3235
import org.springframework.web.bind.support.WebBindingInitializer;
@@ -57,20 +60,30 @@ public class BindingContext {
5760

5861
private boolean methodValidationApplicable;
5962

63+
private final ReactiveAdapterRegistry reactiveAdapterRegistry;
64+
6065

6166
/**
62-
* Create a new {@code BindingContext}.
67+
* Create an instance without an initializer.
6368
*/
6469
public BindingContext() {
6570
this(null);
6671
}
6772

6873
/**
69-
* Create a new {@code BindingContext} with the given initializer.
70-
* @param initializer the binding initializer to apply (may be {@code null})
74+
* Create an instance with the given initializer, which may be {@code null}.
7175
*/
7276
public BindingContext(@Nullable WebBindingInitializer initializer) {
77+
this(initializer, ReactiveAdapterRegistry.getSharedInstance());
78+
}
79+
80+
/**
81+
* Create an instance with the given initializer and {@code ReactiveAdapterRegistry}.
82+
* @since 6.1
83+
*/
84+
public BindingContext(@Nullable WebBindingInitializer initializer, ReactiveAdapterRegistry registry) {
7385
this.initializer = initializer;
86+
this.reactiveAdapterRegistry = new ReactiveAdapterRegistry();
7487
}
7588

7689

@@ -151,6 +164,34 @@ protected WebExchangeDataBinder initDataBinder(WebExchangeDataBinder binder, Ser
151164
return binder;
152165
}
153166

167+
/**
168+
* Invoked before rendering to add {@link BindingResult} attributes where
169+
* necessary, and also to promote model attributes listed as
170+
* {@code @SessionAttributes} to the session.
171+
* @param exchange the current exchange
172+
* @since 6.1
173+
*/
174+
public void updateModel(ServerWebExchange exchange) {
175+
Map<String, Object> model = getModel().asMap();
176+
for (Map.Entry<String, Object> entry : model.entrySet()) {
177+
String name = entry.getKey();
178+
Object value = entry.getValue();
179+
if (isBindingCandidate(name, value)) {
180+
if (!model.containsKey(BindingResult.MODEL_KEY_PREFIX + name)) {
181+
WebExchangeDataBinder binder = createDataBinder(exchange, value, name);
182+
model.put(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
183+
}
184+
}
185+
}
186+
}
187+
188+
private boolean isBindingCandidate(String name, @Nullable Object value) {
189+
return (!name.startsWith(BindingResult.MODEL_KEY_PREFIX) && value != null &&
190+
!value.getClass().isArray() && !(value instanceof Collection) && !(value instanceof Map) &&
191+
this.reactiveAdapterRegistry.getAdapter(null, value) == null &&
192+
!BeanUtils.isSimpleValueType(value.getClass()));
193+
}
194+
154195

155196
/**
156197
* Extended variant of {@link WebExchangeDataBinder}, adding path variables.

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

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.List;
2020

21+
import org.springframework.core.ReactiveAdapterRegistry;
2122
import org.springframework.lang.Nullable;
2223
import org.springframework.util.Assert;
2324
import org.springframework.util.ObjectUtils;
@@ -52,11 +53,11 @@ class InitBinderBindingContext extends BindingContext {
5253

5354
InitBinderBindingContext(
5455
@Nullable WebBindingInitializer initializer, List<SyncInvocableHandlerMethod> binderMethods,
55-
boolean methodValidationApplicable) {
56+
boolean methodValidationApplicable, ReactiveAdapterRegistry registry) {
5657

57-
super(initializer);
58+
super(initializer, registry);
5859
this.binderMethods = binderMethods;
59-
this.binderMethodContext = new BindingContext(initializer);
60+
this.binderMethodContext = new BindingContext(initializer, registry);
6061
setMethodValidationApplicable(methodValidationApplicable);
6162
}
6263

@@ -101,8 +102,8 @@ private void invokeBinderMethod(
101102
}
102103

103104
/**
104-
* Provide the context required to apply {@link #saveModel()} after the
105-
* controller method has been invoked.
105+
* Provide the context required to promote model attributes listed as
106+
* {@code @SessionAttributes} to the session during {@link #updateModel}.
106107
*/
107108
public void setSessionContext(SessionAttributesHandler attributesHandler, WebSession session) {
108109
this.saveModelOperation = () -> {
@@ -115,14 +116,12 @@ public void setSessionContext(SessionAttributesHandler attributesHandler, WebSes
115116
};
116117
}
117118

118-
/**
119-
* Save model attributes in the session based on a type-level declarations
120-
* in an {@code @SessionAttributes} annotation.
121-
*/
122-
public void saveModel() {
119+
@Override
120+
public void updateModel(ServerWebExchange exchange) {
123121
if (this.saveModelOperation != null) {
124122
this.saveModelOperation.run();
125123
}
124+
super.updateModel(exchange);
126125
}
127126

128127
}

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,12 +186,16 @@ public boolean supports(Object handler) {
186186

187187
@Override
188188
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
189+
190+
Assert.state(this.methodResolver != null &&
191+
this.modelInitializer != null && this.reactiveAdapterRegistry != null, "Not initialized");
192+
189193
HandlerMethod handlerMethod = (HandlerMethod) handler;
190-
Assert.state(this.methodResolver != null && this.modelInitializer != null, "Not initialized");
191194

192195
InitBinderBindingContext bindingContext = new InitBinderBindingContext(
193196
this.webBindingInitializer, this.methodResolver.getInitBinderMethods(handlerMethod),
194-
this.methodResolver.hasMethodValidator() && handlerMethod.shouldValidateArguments());
197+
this.methodResolver.hasMethodValidator() && handlerMethod.shouldValidateArguments(),
198+
this.reactiveAdapterRegistry);
195199

196200
InvocableHandlerMethod invocableMethod = this.methodResolver.getRequestMappingMethod(handlerMethod);
197201

@@ -202,7 +206,6 @@ public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {
202206
.initModel(handlerMethod, bindingContext, exchange)
203207
.then(Mono.defer(() -> invocableMethod.invoke(exchange, bindingContext)))
204208
.doOnNext(result -> result.setExceptionHandler(exceptionHandler))
205-
.doOnNext(result -> bindingContext.saveModel())
206209
.onErrorResume(ex -> exceptionHandler.handleError(exchange, ex));
207210
}
208211

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

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 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.
@@ -17,7 +17,6 @@
1717
package org.springframework.web.reactive.result.view;
1818

1919
import java.util.ArrayList;
20-
import java.util.Collection;
2120
import java.util.Collections;
2221
import java.util.List;
2322
import java.util.Locale;
@@ -41,9 +40,7 @@
4140
import org.springframework.lang.Nullable;
4241
import org.springframework.ui.Model;
4342
import org.springframework.util.StringUtils;
44-
import org.springframework.validation.BindingResult;
4543
import org.springframework.web.bind.annotation.ModelAttribute;
46-
import org.springframework.web.bind.support.WebExchangeDataBinder;
4744
import org.springframework.web.reactive.BindingContext;
4845
import org.springframework.web.reactive.HandlerResult;
4946
import org.springframework.web.reactive.HandlerResultHandler;
@@ -243,7 +240,7 @@ else if (View.class.isAssignableFrom(clazz)) {
243240
viewsMono = resolveViews(getDefaultViewName(exchange), locale);
244241
}
245242
BindingContext bindingContext = result.getBindingContext();
246-
updateBindingResult(bindingContext, exchange);
243+
bindingContext.updateModel(exchange);
247244
return viewsMono.flatMap(views -> render(views, model.asMap(), bindingContext, exchange));
248245
});
249246
}
@@ -289,27 +286,6 @@ private String getNameForReturnValue(MethodParameter returnType) {
289286
.orElseGet(() -> Conventions.getVariableNameForParameter(returnType));
290287
}
291288

292-
private void updateBindingResult(BindingContext context, ServerWebExchange exchange) {
293-
Map<String, Object> model = context.getModel().asMap();
294-
for (Map.Entry<String, Object> entry : model.entrySet()) {
295-
String name = entry.getKey();
296-
Object value = entry.getValue();
297-
if (isBindingCandidate(name, value)) {
298-
if (!model.containsKey(BindingResult.MODEL_KEY_PREFIX + name)) {
299-
WebExchangeDataBinder binder = context.createDataBinder(exchange, value, name);
300-
model.put(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
301-
}
302-
}
303-
}
304-
}
305-
306-
private boolean isBindingCandidate(String name, @Nullable Object value) {
307-
return (!name.startsWith(BindingResult.MODEL_KEY_PREFIX) && value != null &&
308-
!value.getClass().isArray() && !(value instanceof Collection) && !(value instanceof Map) &&
309-
getAdapterRegistry().getAdapter(null, value) == null &&
310-
!BeanUtils.isSimpleValueType(value.getClass()));
311-
}
312-
313289
private Mono<? extends Void> render(List<View> views, Map<String, Object> model,
314290
BindingContext bindingContext, ServerWebExchange exchange) {
315291

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ private BindingContext createBindingContext(String methodName, Class<?>... param
133133
handlerMethod.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
134134

135135
return new InitBinderBindingContext(
136-
this.bindingInitializer, Collections.singletonList(handlerMethod), false);
136+
this.bindingInitializer, Collections.singletonList(handlerMethod), false,
137+
ReactiveAdapterRegistry.getSharedInstance());
137138
}
138139

139140

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ public void saveModelAttributeToSession() {
144144
assertThat(session).isNotNull();
145145
assertThat(session.getAttributes()).isEmpty();
146146

147-
context.saveModel();
147+
context.updateModel(this.exchange);
148148
assertThat(session.getAttributes()).hasSize(1);
149149
assertThat(((TestBean) session.getRequiredAttribute("bean")).getName()).isEqualTo("Bean");
150150
}
@@ -164,7 +164,7 @@ public void retrieveModelAttributeFromSession() {
164164
HandlerMethod handlerMethod = new HandlerMethod(controller, method);
165165
this.modelInitializer.initModel(handlerMethod, context, this.exchange).block(TIMEOUT);
166166

167-
context.saveModel();
167+
context.updateModel(this.exchange);
168168
assertThat(session.getAttributes()).hasSize(1);
169169
assertThat(((TestBean) session.getRequiredAttribute("bean")).getName()).isEqualTo("Session Bean");
170170
}
@@ -197,7 +197,7 @@ public void clearModelAttributeFromSession() {
197197
this.modelInitializer.initModel(handlerMethod, context, this.exchange).block(TIMEOUT);
198198

199199
context.getSessionStatus().setComplete();
200-
context.saveModel();
200+
context.updateModel(this.exchange);
201201

202202
assertThat(session.getAttributes()).isEmpty();
203203
}
@@ -211,7 +211,8 @@ private InitBinderBindingContext getBindingContext(Object controller) {
211211
.toList();
212212

213213
WebBindingInitializer bindingInitializer = new ConfigurableWebBindingInitializer();
214-
return new InitBinderBindingContext(bindingInitializer, binderMethods, false);
214+
return new InitBinderBindingContext(
215+
bindingInitializer, binderMethods, false, ReactiveAdapterRegistry.getSharedInstance());
215216
}
216217

217218

0 commit comments

Comments
 (0)