Skip to content

Commit 65ba865

Browse files
committed
Support for populating model attributes through data class constructors
Includes a new overloaded ModelAndView constructor with an HttpStatus argument, as well as a HandlerMethodArgumentResolverSupport refactoring (revised checkParameterType signature, actually implementing the HandlerMethodArgumentResolver interface). Issue: SPR-15199
1 parent b315435 commit 65ba865

File tree

30 files changed

+411
-274
lines changed

30 files changed

+411
-274
lines changed

spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java

Lines changed: 58 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,20 @@
1616

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

19+
import java.beans.ConstructorProperties;
1920
import java.lang.annotation.Annotation;
21+
import java.lang.reflect.Constructor;
2022
import java.util.Map;
2123

2224
import org.apache.commons.logging.Log;
2325
import org.apache.commons.logging.LogFactory;
2426

2527
import org.springframework.beans.BeanUtils;
28+
import org.springframework.core.DefaultParameterNameDiscoverer;
2629
import org.springframework.core.MethodParameter;
30+
import org.springframework.core.ParameterNameDiscoverer;
2731
import org.springframework.core.annotation.AnnotationUtils;
32+
import org.springframework.util.Assert;
2833
import org.springframework.validation.BindException;
2934
import org.springframework.validation.Errors;
3035
import org.springframework.validation.annotation.Validated;
@@ -52,10 +57,13 @@
5257
* attribute with or without the presence of an {@code @ModelAttribute}.
5358
*
5459
* @author Rossen Stoyanchev
60+
* @author Juergen Hoeller
5561
* @since 3.1
5662
*/
5763
public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
5864

65+
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
66+
5967
protected final Log logger = LogFactory.getLog(getClass());
6068

6169
private final boolean annotationNotRequired;
@@ -65,7 +73,7 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
6573
* Class constructor.
6674
* @param annotationNotRequired if "true", non-simple method arguments and
6775
* return values are considered model attributes with or without a
68-
* {@code @ModelAttribute} annotation.
76+
* {@code @ModelAttribute} annotation
6977
*/
7078
public ModelAttributeMethodProcessor(boolean annotationNotRequired) {
7179
this.annotationNotRequired = annotationNotRequired;
@@ -89,8 +97,8 @@ public boolean supportsParameter(MethodParameter parameter) {
8997
* with request values via data binding and optionally validated
9098
* if {@code @java.validation.Valid} is present on the argument.
9199
* @throws BindException if data binding and validation result in an error
92-
* and the next method parameter is not of type {@link Errors}.
93-
* @throws Exception if WebDataBinder initialization fails.
100+
* and the next method parameter is not of type {@link Errors}
101+
* @throws Exception if WebDataBinder initialization fails
94102
*/
95103
@Override
96104
public final Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
@@ -123,22 +131,54 @@ public final Object resolveArgument(MethodParameter parameter, ModelAndViewConta
123131
mavContainer.removeAttributes(bindingResultModel);
124132
mavContainer.addAllAttributes(bindingResultModel);
125133

126-
return binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
134+
return (parameter.getParameterType().isInstance(attribute) ? attribute :
135+
binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter));
127136
}
128137

129138
/**
130-
* Extension point to create the model attribute if not found in the model.
131-
* The default implementation uses the default constructor.
139+
* Extension point to create the model attribute if not found in the model,
140+
* with subsequent parameter binding through bean properties (unless suppressed).
141+
* <p>The default implementation uses the unique public no-arg constructor, if any,
142+
* which may have arguments: It understands the JavaBeans {@link ConstructorProperties}
143+
* annotation as well as runtime-retained parameter names in the bytecode,
144+
* associating request parameters with constructor arguments by name. If no such
145+
* constructor is found, the default constructor will be used (even if not public),
146+
* assuming subsequent bean property bindings through setter methods.
132147
* @param attributeName the name of the attribute (never {@code null})
133-
* @param methodParam the method parameter
148+
* @param parameter the method parameter declaration
134149
* @param binderFactory for creating WebDataBinder instance
135-
* @param request the current request
150+
* @param webRequest the current request
136151
* @return the created model attribute (never {@code null})
137152
*/
138-
protected Object createAttribute(String attributeName, MethodParameter methodParam,
139-
WebDataBinderFactory binderFactory, NativeWebRequest request) throws Exception {
153+
protected Object createAttribute(String attributeName, MethodParameter parameter,
154+
WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
155+
156+
Constructor<?>[] ctors = parameter.getParameterType().getConstructors();
157+
if (ctors.length != 1) {
158+
// No standard data class or standard JavaBeans arrangement ->
159+
// defensively go with default constructor, expecting regular bean property bindings.
160+
return BeanUtils.instantiateClass(parameter.getParameterType());
161+
}
162+
Constructor<?> ctor = ctors[0];
163+
if (ctor.getParameterCount() == 0) {
164+
// A single default constructor -> clearly a standard JavaBeans arrangement.
165+
return BeanUtils.instantiateClass(ctor);
166+
}
140167

141-
return BeanUtils.instantiateClass(methodParam.getParameterType());
168+
// A single data class constructor -> resolve constructor arguments from request parameters.
169+
ConstructorProperties cp = ctor.getAnnotation(ConstructorProperties.class);
170+
String[] paramNames = (cp != null ? cp.value() : parameterNameDiscoverer.getParameterNames(ctor));
171+
Assert.state(paramNames != null, () -> "Cannot resolve parameter names for constructor " + ctor);
172+
Class<?>[] paramTypes = ctor.getParameterTypes();
173+
Assert.state(paramNames.length == paramTypes.length,
174+
() -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor);
175+
Object[] args = new Object[paramTypes.length];
176+
WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName);
177+
for (int i = 0; i < paramNames.length; i++) {
178+
args[i] = binder.convertIfNecessary(
179+
webRequest.getParameterValues(paramNames[i]), paramTypes[i], new MethodParameter(ctor, i));
180+
}
181+
return BeanUtils.instantiateClass(ctor, args);
142182
}
143183

144184
/**
@@ -156,10 +196,10 @@ protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest requ
156196
* Spring's {@link org.springframework.validation.annotation.Validated},
157197
* and custom annotations whose name starts with "Valid".
158198
* @param binder the DataBinder to be used
159-
* @param methodParam the method parameter
199+
* @param parameter the method parameter declaration
160200
*/
161-
protected void validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) {
162-
Annotation[] annotations = methodParam.getParameterAnnotations();
201+
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
202+
Annotation[] annotations = parameter.getParameterAnnotations();
163203
for (Annotation ann : annotations) {
164204
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
165205
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
@@ -174,12 +214,12 @@ protected void validateIfApplicable(WebDataBinder binder, MethodParameter method
174214
/**
175215
* Whether to raise a fatal bind exception on validation errors.
176216
* @param binder the data binder used to perform data binding
177-
* @param methodParam the method argument
217+
* @param parameter the method parameter declaration
178218
* @return {@code true} if the next method argument is not of type {@link Errors}
179219
*/
180-
protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter methodParam) {
181-
int i = methodParam.getParameterIndex();
182-
Class<?>[] paramTypes = methodParam.getMethod().getParameterTypes();
220+
protected boolean isBindExceptionRequired(WebDataBinder binder, MethodParameter parameter) {
221+
int i = parameter.getParameterIndex();
222+
Class<?>[] paramTypes = parameter.getMethod().getParameterTypes();
183223
boolean hasBindingResult = (paramTypes.length > (i + 1) && Errors.class.isAssignableFrom(paramTypes[i + 1]));
184224
return !hasBindingResult;
185225
}

spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2015 the original author or authors.
2+
* Copyright 2002-2017 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.
@@ -70,7 +70,7 @@ public class ModelAttributeMethodProcessorTests {
7070

7171

7272
@Before
73-
public void setUp() throws Exception {
73+
public void setup() throws Exception {
7474
this.request = new ServletWebRequest(new MockHttpServletRequest());
7575
this.container = new ModelAndViewContainer();
7676
this.processor = new ModelAttributeMethodProcessor(false);
@@ -145,7 +145,7 @@ public void resolveArgumentFromModel() throws Exception {
145145
}
146146

147147
@Test
148-
public void resovleArgumentViaDefaultConstructor() throws Exception {
148+
public void resolveArgumentViaDefaultConstructor() throws Exception {
149149
WebDataBinder dataBinder = new WebRequestDataBinder(null);
150150
WebDataBinderFactory factory = mock(WebDataBinderFactory.class);
151151
given(factory.createBinder(any(), notNull(), eq("attrName"))).willReturn(dataBinder);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public interface HandlerMethodArgumentResolver {
4444
* @param exchange the current exchange
4545
* @return {@code Mono} for the argument value, possibly empty
4646
*/
47-
Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bindingContext,
48-
ServerWebExchange exchange);
47+
Mono<Object> resolveArgument(
48+
MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange);
4949

5050
}

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

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
1617
package org.springframework.web.reactive.result.method;
1718

1819
import java.lang.annotation.Annotation;
@@ -25,14 +26,14 @@
2526
import org.springframework.util.Assert;
2627

2728
/**
28-
* Base class for {@link HandlerMethodArgumentResolver} implementations with
29-
* access to a {@code ReactiveAdapterRegistry} and methods to check for
30-
* method parameter support.
29+
* Base class for {@link HandlerMethodArgumentResolver} implementations with access to a
30+
* {@code ReactiveAdapterRegistry} and methods to check for method parameter support.
3131
*
3232
* @author Rossen Stoyanchev
33+
* @author Juergen Hoeller
3334
* @since 5.0
3435
*/
35-
public abstract class HandlerMethodArgumentResolverSupport {
36+
public abstract class HandlerMethodArgumentResolverSupport implements HandlerMethodArgumentResolver {
3637

3738
private final ReactiveAdapterRegistry adapterRegistry;
3839

@@ -55,12 +56,12 @@ public ReactiveAdapterRegistry getAdapterRegistry() {
5556
* Evaluate the {@code Predicate} on the the method parameter type or on
5657
* the generic type within a reactive type wrapper.
5758
*/
58-
protected boolean checkParamType(MethodParameter param, Predicate<Class<?>> predicate) {
59-
Class<?> type = param.getParameterType();
59+
protected boolean checkParameterType(MethodParameter parameter, Predicate<Class<?>> predicate) {
60+
Class<?> type = parameter.getParameterType();
6061
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(type);
6162
if (adapter != null) {
62-
assertHasValues(adapter, param);
63-
type = param.nested().getNestedParameterType();
63+
assertHasValues(adapter, parameter);
64+
type = parameter.nested().getNestedParameterType();
6465
}
6566
return predicate.test(type);
6667
}
@@ -77,25 +78,25 @@ private void assertHasValues(ReactiveAdapter adapter, MethodParameter param) {
7778
* {@code IllegalStateException} if the same matches the generic type
7879
* within a reactive type wrapper.
7980
*/
80-
protected boolean checkParamTypeNoReactiveWrapper(MethodParameter param, Predicate<Class<?>> predicate) {
81-
Class<?> type = param.getParameterType();
81+
protected boolean checkParameterTypeNoReactiveWrapper(MethodParameter parameter, Predicate<Class<?>> predicate) {
82+
Class<?> type = parameter.getParameterType();
8283
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(type);
8384
if (adapter != null) {
84-
assertHasValues(adapter, param);
85-
type = param.nested().getNestedParameterType();
85+
assertHasValues(adapter, parameter);
86+
type = parameter.nested().getNestedParameterType();
8687
}
8788
if (predicate.test(type)) {
8889
if (adapter == null) {
8990
return true;
9091
}
91-
throw getReactiveWrapperError(param);
92+
throw buildReactiveWrapperException(parameter);
9293
}
9394
return false;
9495
}
9596

96-
private IllegalStateException getReactiveWrapperError(MethodParameter param) {
97+
private IllegalStateException buildReactiveWrapperException(MethodParameter parameter) {
9798
return new IllegalStateException(getClass().getSimpleName() +
98-
" doesn't support reactive type wrapper: " + param.getGenericParameterType());
99+
" doesn't support reactive type wrapper: " + parameter.getGenericParameterType());
99100
}
100101

101102
/**
@@ -105,29 +106,28 @@ private IllegalStateException getReactiveWrapperError(MethodParameter param) {
105106
* type within a reactive type wrapper.
106107
*/
107108
protected <A extends Annotation> boolean checkAnnotatedParamNoReactiveWrapper(
108-
MethodParameter param, Class<A> annotationType,
109-
BiPredicate<A, Class<?>> typePredicate) {
109+
MethodParameter parameter, Class<A> annotationType, BiPredicate<A, Class<?>> typePredicate) {
110110

111-
A annotation = param.getParameterAnnotation(annotationType);
111+
A annotation = parameter.getParameterAnnotation(annotationType);
112112
if (annotation == null) {
113113
return false;
114114
}
115115

116-
param = param.nestedIfOptional();
117-
Class<?> type = param.getNestedParameterType();
116+
parameter = parameter.nestedIfOptional();
117+
Class<?> type = parameter.getNestedParameterType();
118118

119119
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(type);
120120
if (adapter != null) {
121-
assertHasValues(adapter, param);
122-
param = param.nested();
123-
type = param.getNestedParameterType();
121+
assertHasValues(adapter, parameter);
122+
parameter = parameter.nested();
123+
type = parameter.getNestedParameterType();
124124
}
125125

126126
if (typePredicate.test(annotation, type)) {
127127
if (adapter == null) {
128128
return true;
129129
}
130-
throw getReactiveWrapperError(param);
130+
throw buildReactiveWrapperException(parameter);
131131
}
132132

133133
return false;

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2017 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.
@@ -33,15 +33,14 @@
3333
*/
3434
public interface SyncHandlerMethodArgumentResolver extends HandlerMethodArgumentResolver {
3535

36-
3736
/**
3837
* {@inheritDoc}
3938
* <p>By default this simply delegates to {@link #resolveArgumentValue} for
4039
* synchronous resolution.
4140
*/
4241
@Override
43-
default Mono<Object> resolveArgument(MethodParameter parameter, BindingContext bindingContext,
44-
ServerWebExchange exchange) {
42+
default Mono<Object> resolveArgument(
43+
MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) {
4544

4645
return Mono.justOrEmpty(resolveArgumentValue(parameter, bindingContext, exchange));
4746
}
@@ -53,7 +52,7 @@ default Mono<Object> resolveArgument(MethodParameter parameter, BindingContext b
5352
* @param exchange the current exchange
5453
* @return an {@code Optional} with the resolved value, possibly empty
5554
*/
56-
Optional<Object> resolveArgumentValue(MethodParameter parameter, BindingContext bindingContext,
57-
ServerWebExchange exchange);
55+
Optional<Object> resolveArgumentValue(
56+
MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange);
5857

5958
}

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2017 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.
@@ -114,7 +114,6 @@ protected Mono<Object> readBody(MethodParameter bodyParameter, boolean isBodyReq
114114
}
115115

116116
for (ServerHttpMessageReader<?> reader : getMessageReaders()) {
117-
118117
if (reader.canRead(elementType, mediaType)) {
119118
Map<String, Object> readHints = Collections.emptyMap();
120119
if (adapter != null && adapter.isMultiValue()) {
@@ -171,9 +170,9 @@ private ServerWebInputException getRequiredBodyError(MethodParameter parameter)
171170
private Object[] extractValidationHints(MethodParameter parameter) {
172171
Annotation[] annotations = parameter.getParameterAnnotations();
173172
for (Annotation ann : annotations) {
174-
Validated validAnnot = AnnotationUtils.getAnnotation(ann, Validated.class);
175-
if (validAnnot != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
176-
Object hints = (validAnnot != null ? validAnnot.value() : AnnotationUtils.getValue(ann));
173+
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
174+
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
175+
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
177176
return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
178177
}
179178
}

0 commit comments

Comments
 (0)