Skip to content

Commit d37d668

Browse files
committed
WebFlux constructs model attribute via DataBinder
See gh-26721
1 parent 801f01e commit d37d668

File tree

4 files changed

+141
-149
lines changed

4 files changed

+141
-149
lines changed

spring-web/src/main/java/org/springframework/web/bind/support/WebExchangeDataBinder.java

Lines changed: 38 additions & 4 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.
@@ -71,14 +71,30 @@ public WebExchangeDataBinder(@Nullable Object target, String objectName) {
7171
}
7272

7373

74+
/**
75+
* Use a default or single data constructor to create the target by
76+
* binding request parameters, multipart files, or parts to constructor args.
77+
* <p>After the call, use {@link #getBindingResult()} to check for bind errors.
78+
* If there are none, the target is set, and {@link #bind} can be called for
79+
* further initialization via setters.
80+
* @param exchange the request to bind
81+
* @return a {@code Mono<Void>} that completes when the target is created
82+
* @since 6.1
83+
*/
84+
public Mono<Void> construct(ServerWebExchange exchange) {
85+
return getValuesToBind(exchange)
86+
.doOnNext(map -> construct(new MapValueResolver(map)))
87+
.then();
88+
}
89+
7490
/**
7591
* Bind query parameters, form data, or multipart form data to the binder target.
7692
* @param exchange the current exchange
77-
* @return a {@code Mono<Void>} when binding is complete
93+
* @return a {@code Mono<Void>} that completes when binding is complete
7894
*/
7995
public Mono<Void> bind(ServerWebExchange exchange) {
8096
return getValuesToBind(exchange)
81-
.doOnNext(values -> doBind(new MutablePropertyValues(values)))
97+
.doOnNext(map -> doBind(new MutablePropertyValues(map)))
8298
.then();
8399
}
84100

@@ -128,4 +144,22 @@ protected static void addBindValue(Map<String, Object> params, String key, List<
128144
}
129145
}
130146

131-
}
147+
148+
/**
149+
* Resolve values from a map.
150+
*/
151+
private static class MapValueResolver implements ValueResolver {
152+
153+
private final Map<String, Object> map;
154+
155+
private MapValueResolver(Map<String, Object> map) {
156+
this.map = map;
157+
}
158+
159+
@Override
160+
public Object resolveValue(String name, Class<?> type) {
161+
return this.map.get(name);
162+
}
163+
}
164+
165+
}

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

Lines changed: 31 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import org.springframework.core.MethodParameter;
2626
import org.springframework.core.ReactiveAdapterRegistry;
27+
import org.springframework.core.ResolvableType;
2728
import org.springframework.lang.Nullable;
2829
import org.springframework.ui.Model;
2930
import org.springframework.validation.DataBinder;
@@ -92,60 +93,62 @@ public void setMethodValidationApplicable(boolean methodValidationApplicable) {
9293

9394

9495
/**
95-
* Create a {@link WebExchangeDataBinder} to apply data binding and
96-
* validation with on the target, command object.
96+
* Create a binder with a target object.
9797
* @param exchange the current exchange
9898
* @param target the object to create a data binder for
9999
* @param name the name of the target object
100100
* @return the created data binder
101101
* @throws ServerErrorException if {@code @InitBinder} method invocation fails
102102
*/
103103
public WebExchangeDataBinder createDataBinder(ServerWebExchange exchange, @Nullable Object target, String name) {
104-
WebExchangeDataBinder dataBinder = new ExtendedWebExchangeDataBinder(target, name);
105-
if (this.initializer != null) {
106-
this.initializer.initBinder(dataBinder);
107-
}
108-
return initDataBinder(dataBinder, exchange);
109-
}
110-
111-
/**
112-
* Initialize the data binder instance for the given exchange.
113-
* @throws ServerErrorException if {@code @InitBinder} method invocation fails
114-
*/
115-
protected WebExchangeDataBinder initDataBinder(WebExchangeDataBinder binder, ServerWebExchange exchange) {
116-
return binder;
104+
return createDataBinder(exchange, target, name, null);
117105
}
118106

119107
/**
120-
* Create a {@link WebExchangeDataBinder} without a target object for type
121-
* conversion of request values to simple types.
108+
* Shortcut method to create a binder without a target object.
122109
* @param exchange the current exchange
123110
* @param name the name of the target object
124111
* @return the created data binder
125112
* @throws ServerErrorException if {@code @InitBinder} method invocation fails
126113
*/
127114
public WebExchangeDataBinder createDataBinder(ServerWebExchange exchange, String name) {
128-
return createDataBinder(exchange, null, name);
115+
return createDataBinder(exchange, null, name, null);
129116
}
130117

131118
/**
132-
* Variant of {@link #createDataBinder(ServerWebExchange, Object, String)}
133-
* with a {@link MethodParameter} for which the {@code DataBinder} is created.
134-
* That may provide more insight to initialize the {@link WebExchangeDataBinder}.
135-
* <p>By default, if the parameter has {@code @Valid}, Bean Validation is
136-
* excluded, deferring to method validation.
119+
* Create a binder with a target object and a {@code MethodParameter}.
120+
* If the target is {@code null}, then
121+
* {@link WebExchangeDataBinder#setTargetType targetType} is set.
137122
* @since 6.1
138123
*/
139124
public WebExchangeDataBinder createDataBinder(
140-
ServerWebExchange exchange, @Nullable Object target, String name, MethodParameter parameter) {
125+
ServerWebExchange exchange, @Nullable Object target, String name, @Nullable MethodParameter parameter) {
126+
127+
WebExchangeDataBinder dataBinder = new ExtendedWebExchangeDataBinder(target, name);
128+
if (target == null && parameter != null) {
129+
dataBinder.setTargetType(ResolvableType.forMethodParameter(parameter));
130+
}
141131

142-
WebExchangeDataBinder dataBinder = createDataBinder(exchange, target, name);
143-
if (this.methodValidationApplicable) {
144-
MethodValidationInitializer.updateBinder(dataBinder, parameter);
132+
if (this.initializer != null) {
133+
this.initializer.initBinder(dataBinder);
145134
}
135+
dataBinder = initDataBinder(dataBinder, exchange);
136+
137+
if (this.methodValidationApplicable && parameter != null) {
138+
MethodValidationInitializer.initBinder(dataBinder, parameter);
139+
}
140+
146141
return dataBinder;
147142
}
148143

144+
/**
145+
* Initialize the data binder instance for the given exchange.
146+
* @throws ServerErrorException if {@code @InitBinder} method invocation fails
147+
*/
148+
protected WebExchangeDataBinder initDataBinder(WebExchangeDataBinder binder, ServerWebExchange exchange) {
149+
return binder;
150+
}
151+
149152

150153
/**
151154
* Extended variant of {@link WebExchangeDataBinder}, adding path variables.
@@ -170,7 +173,7 @@ public Mono<Map<String, Object>> getValuesToBind(ServerWebExchange exchange) {
170173
*/
171174
private static class MethodValidationInitializer {
172175

173-
public static void updateBinder(DataBinder binder, MethodParameter parameter) {
176+
public static void initBinder(DataBinder binder, MethodParameter parameter) {
174177
if (ReactiveAdapterRegistry.getSharedInstance().getAdapter(parameter.getParameterType()) == null) {
175178
for (Annotation annotation : parameter.getParameterAnnotations()) {
176179
if (annotation.annotationType().getName().equals("jakarta.validation.Valid")) {

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

Lines changed: 65 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@
1717
package org.springframework.web.reactive.result.method.annotation;
1818

1919
import java.lang.annotation.Annotation;
20-
import java.lang.reflect.Constructor;
21-
import java.util.List;
2220
import java.util.Map;
23-
import java.util.Optional;
2421

2522
import reactor.core.publisher.Mono;
2623
import reactor.core.publisher.Sinks;
@@ -31,7 +28,6 @@
3128
import org.springframework.core.MethodParameter;
3229
import org.springframework.core.ReactiveAdapter;
3330
import org.springframework.core.ReactiveAdapterRegistry;
34-
import org.springframework.core.ResolvableType;
3531
import org.springframework.lang.Nullable;
3632
import org.springframework.ui.Model;
3733
import org.springframework.util.Assert;
@@ -100,72 +96,74 @@ else if (this.useDefaultResolution) {
10096
public Mono<Object> resolveArgument(
10197
MethodParameter parameter, BindingContext context, ServerWebExchange exchange) {
10298

103-
ResolvableType type = ResolvableType.forMethodParameter(parameter);
104-
Class<?> resolvedType = type.resolve();
105-
ReactiveAdapter adapter = (resolvedType != null ? getAdapterRegistry().getAdapter(resolvedType) : null);
106-
ResolvableType valueType = (adapter != null ? type.getGeneric() : type);
107-
108-
Assert.state(adapter == null || !adapter.isMultiValue(),
109-
() -> getClass().getSimpleName() + " does not support multi-value reactive type wrapper: " +
110-
parameter.getGenericParameterType());
99+
Class<?> resolvedType = parameter.getParameterType();
100+
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(resolvedType);
101+
Assert.state(adapter == null || !adapter.isMultiValue(), "Multi-value publisher is not supported");
111102

112103
String name = ModelInitializer.getNameForParameter(parameter);
113-
Mono<?> attributeMono = prepareAttributeMono(name, valueType, context, exchange);
114104

115-
// unsafe(): we're intercepting, already serialized Publisher signals
105+
Mono<WebExchangeDataBinder> dataBinderMono = initDataBinder(
106+
name, (adapter != null ? parameter.nested() : parameter), context, exchange);
107+
108+
// unsafe() is OK: source is Reactive Streams Publisher
116109
Sinks.One<BindingResult> bindingResultSink = Sinks.unsafe().one();
117110

118111
Map<String, Object> model = context.getModel().asMap();
119112
model.put(BindingResult.MODEL_KEY_PREFIX + name, bindingResultSink.asMono());
120113

121-
return attributeMono.flatMap(attribute -> {
122-
WebExchangeDataBinder binder = context.createDataBinder(exchange, attribute, name, parameter);
123-
return (!bindingDisabled(parameter) ? bindRequestParameters(binder, exchange) : Mono.empty())
124-
.doOnError(bindingResultSink::tryEmitError)
125-
.doOnSuccess(aVoid -> {
126-
validateIfApplicable(binder, parameter, exchange);
127-
BindingResult bindingResult = binder.getBindingResult();
128-
model.put(BindingResult.MODEL_KEY_PREFIX + name, bindingResult);
129-
model.put(name, attribute);
130-
// Ignore result: serialized and buffered (should never fail)
131-
bindingResultSink.tryEmitValue(bindingResult);
132-
})
133-
.then(Mono.fromCallable(() -> {
134-
BindingResult errors = binder.getBindingResult();
135-
if (adapter != null) {
136-
return adapter.fromPublisher(errors.hasErrors() ?
137-
Mono.error(new WebExchangeBindException(parameter, errors)) : attributeMono);
138-
}
139-
else {
140-
if (errors.hasErrors() && !hasErrorsArgument(parameter)) {
141-
throw new WebExchangeBindException(parameter, errors);
142-
}
143-
return attribute;
144-
}
145-
}));
146-
});
114+
return dataBinderMono
115+
.flatMap(binder -> {
116+
Object attribute = binder.getTarget();
117+
Assert.state(attribute != null, "Expected model attribute instance");
118+
return (!bindingDisabled(parameter) ? bindRequestParameters(binder, exchange) : Mono.empty())
119+
.doOnError(bindingResultSink::tryEmitError)
120+
.doOnSuccess(aVoid -> {
121+
validateIfApplicable(binder, parameter, exchange);
122+
BindingResult bindingResult = binder.getBindingResult();
123+
model.put(BindingResult.MODEL_KEY_PREFIX + name, bindingResult);
124+
model.put(name, attribute);
125+
// Ignore result: serialized and buffered (should never fail)
126+
bindingResultSink.tryEmitValue(bindingResult);
127+
})
128+
.then(Mono.fromCallable(() -> {
129+
BindingResult errors = binder.getBindingResult();
130+
if (adapter != null) {
131+
Mono<Object> mono = (errors.hasErrors() ?
132+
Mono.error(new WebExchangeBindException(parameter, errors)) :
133+
Mono.just(attribute));
134+
return adapter.fromPublisher(mono);
135+
}
136+
else {
137+
if (errors.hasErrors() && !hasErrorsArgument(parameter)) {
138+
throw new WebExchangeBindException(parameter, errors);
139+
}
140+
return attribute;
141+
}
142+
}));
143+
});
147144
}
148145

149-
private Mono<?> prepareAttributeMono(
150-
String name, ResolvableType type, BindingContext context, ServerWebExchange exchange) {
151-
152-
Object attribute = context.getModel().asMap().get(name);
146+
private Mono<WebExchangeDataBinder> initDataBinder(
147+
String name, MethodParameter parameter, BindingContext context, ServerWebExchange exchange) {
153148

154-
if (attribute == null) {
155-
attribute = removeReactiveAttribute(context.getModel(), name);
149+
Object value = context.getModel().asMap().get(name);
150+
if (value == null) {
151+
value = removeReactiveAttribute(name, context.getModel());
156152
}
157-
158-
if (attribute == null) {
159-
return createAttribute(name, type.toClass(), context, exchange);
153+
if (value != null) {
154+
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(null, value);
155+
Assert.isTrue(adapter == null || !adapter.isMultiValue(), "Multi-value publisher is not supported");
156+
return (adapter != null ? Mono.from(adapter.toPublisher(value)) : Mono.just(value))
157+
.map(attr -> context.createDataBinder(exchange, attr, name, parameter));
158+
}
159+
else {
160+
WebExchangeDataBinder binder = context.createDataBinder(exchange, null, name, parameter);
161+
return constructAttribute(binder, exchange).thenReturn(binder);
160162
}
161-
162-
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(null, attribute);
163-
Assert.isTrue(adapter == null || !adapter.isMultiValue(), "Model attribute must be single-value publisher");
164-
return (adapter != null ? Mono.from(adapter.toPublisher(attribute)) : Mono.justOrEmpty(attribute));
165163
}
166164

167165
@Nullable
168-
private Object removeReactiveAttribute(Model model, String name) {
166+
private Object removeReactiveAttribute(String name, Model model) {
169167
for (Map.Entry<String, Object> entry : model.asMap().entrySet()) {
170168
if (entry.getKey().startsWith(name)) {
171169
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(null, entry.getValue());
@@ -181,66 +179,24 @@ private Object removeReactiveAttribute(Model model, String name) {
181179
return null;
182180
}
183181

184-
private Mono<?> createAttribute(
185-
String attributeName, Class<?> clazz, BindingContext context, ServerWebExchange exchange) {
186-
187-
Constructor<?> ctor = BeanUtils.getResolvableConstructor(clazz);
188-
return constructAttribute(ctor, attributeName, context, exchange);
189-
}
190-
191-
private Mono<?> constructAttribute(Constructor<?> ctor, String attributeName,
192-
BindingContext context, ServerWebExchange exchange) {
193-
194-
if (ctor.getParameterCount() == 0) {
195-
// A single default constructor -> clearly a standard JavaBeans arrangement.
196-
return Mono.just(BeanUtils.instantiateClass(ctor));
197-
}
198-
199-
// A single data class constructor -> resolve constructor arguments from request parameters.
200-
WebExchangeDataBinder binder = context.createDataBinder(exchange, null, attributeName);
201-
return getValuesToBind(binder, exchange).map(bindValues -> {
202-
String[] paramNames = BeanUtils.getParameterNames(ctor);
203-
Class<?>[] paramTypes = ctor.getParameterTypes();
204-
Object[] args = new Object[paramTypes.length];
205-
String fieldDefaultPrefix = binder.getFieldDefaultPrefix();
206-
String fieldMarkerPrefix = binder.getFieldMarkerPrefix();
207-
for (int i = 0; i < paramNames.length; i++) {
208-
String paramName = paramNames[i];
209-
Class<?> paramType = paramTypes[i];
210-
Object value = bindValues.get(paramName);
211-
if (value == null) {
212-
if (fieldDefaultPrefix != null) {
213-
value = bindValues.get(fieldDefaultPrefix + paramName);
214-
}
215-
if (value == null && fieldMarkerPrefix != null) {
216-
if (bindValues.get(fieldMarkerPrefix + paramName) != null) {
217-
value = binder.getEmptyValue(paramType);
218-
}
219-
}
220-
}
221-
value = (value instanceof List<?> list ? list.toArray() : value);
222-
MethodParameter methodParam = MethodParameter.forFieldAwareConstructor(ctor, i, paramName);
223-
if (value == null && methodParam.isOptional()) {
224-
args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null);
225-
}
226-
else {
227-
args[i] = binder.convertIfNecessary(value, paramTypes[i], methodParam);
228-
}
229-
}
230-
return BeanUtils.instantiateClass(ctor, args);
231-
});
182+
/**
183+
* Protected method to obtain the values for data binding.
184+
* @deprecated and not called; replaced by built-in support for
185+
* constructor initialization in {@link org.springframework.validation.DataBinder}
186+
*/
187+
@Deprecated(since = "6.1", forRemoval = true)
188+
public Mono<Map<String, Object>> getValuesToBind(WebExchangeDataBinder binder, ServerWebExchange exchange) {
189+
throw new UnsupportedOperationException();
232190
}
233191

234192
/**
235-
* Protected method to obtain the values for data binding. By default this
236-
* method delegates to {@link WebExchangeDataBinder#getValuesToBind}.
237-
* @param binder the data binder in use
193+
* Extension point to create the attribute, binding the request to constructor args.
194+
* @param binder the data binder instance to use for the binding
238195
* @param exchange the current exchange
239-
* @return a map of bind values
240-
* @since 5.3
196+
* @since 6.1
241197
*/
242-
public Mono<Map<String, Object>> getValuesToBind(WebExchangeDataBinder binder, ServerWebExchange exchange) {
243-
return binder.getValuesToBind(exchange);
198+
protected Mono<Void> constructAttribute(WebExchangeDataBinder binder, ServerWebExchange exchange) {
199+
return binder.construct(exchange);
244200
}
245201

246202
/**

0 commit comments

Comments
 (0)