diff --git a/spring-context/src/main/java/org/springframework/validation/DataBinder.java b/spring-context/src/main/java/org/springframework/validation/DataBinder.java index 9b1b5e014e24..0c43937b5b1b 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -17,30 +17,40 @@ package org.springframework.validation; import java.beans.PropertyEditor; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.Set; import java.util.function.Predicate; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.BeanUtils; import org.springframework.beans.ConfigurablePropertyAccessor; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyAccessException; import org.springframework.beans.PropertyAccessorUtils; import org.springframework.beans.PropertyBatchUpdateException; +import org.springframework.beans.PropertyEditorRegistrar; import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.PropertyValue; import org.springframework.beans.PropertyValues; import org.springframework.beans.SimpleTypeConverter; import org.springframework.beans.TypeConverter; import org.springframework.beans.TypeMismatchException; +import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.format.Formatter; @@ -50,10 +60,11 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.PatternMatchUtils; import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.ValidationAnnotationUtils; /** - * Binder that allows for setting property values on a target object, including - * support for validation and binding result analysis. + * Binder that allows applying property values to a target object via constructor + * and setter injection, and also supports validation and binding result analysis. * *
The binding process can be customized by specifying allowed field patterns, * required fields, custom editors, etc. @@ -105,6 +116,7 @@ * @see #registerCustomEditor * @see #setMessageCodesResolver * @see #setBindingErrorProcessor + * @see #construct * @see #bind * @see #getBindingResult * @see DefaultMessageCodesResolver @@ -126,7 +138,10 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { protected static final Log logger = LogFactory.getLog(DataBinder.class); @Nullable - private final Object target; + private Object target; + + @Nullable + ResolvableType targetType; private final String objectName; @@ -136,7 +151,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { private boolean directFieldAccess = false; @Nullable - private SimpleTypeConverter typeConverter; + private ExtendedTypeConverter typeConverter; private boolean ignoreUnknownFields = true; @@ -193,6 +208,8 @@ public DataBinder(@Nullable Object target, String objectName) { /** * Return the wrapped target object. + *
If the target object is {@code null} and {@link #getTargetType()} is set, + * then {@link #construct(ValueResolver)} may be called to create the target. */ @Nullable public Object getTarget() { @@ -206,6 +223,27 @@ public String getObjectName() { return this.objectName; } + /** + * Set the type for the target object. When the target is {@code null}, + * setting the targetType allows using {@link #construct(ValueResolver)} to + * create the target. + * @param targetType the type of the target object + * @since 6.1 + */ + public void setTargetType(ResolvableType targetType) { + Assert.state(this.target == null, "targetType is used to for target creation, but target is already set"); + this.targetType = targetType; + } + + /** + * Return the {@link #setTargetType configured} type for the target object. + * @since 6.1 + */ + @Nullable + public ResolvableType getTargetType() { + return this.targetType; + } + /** * Set whether this binder should attempt to "auto-grow" a nested path that contains a null value. *
If "true", a null path location will be populated with a default object value and traversed @@ -213,6 +251,8 @@ public String getObjectName() { * when accessing an out-of-bounds index. *
Default is "true" on a standard DataBinder. Note that since Spring 4.1 this feature is supported * for bean property access (DataBinder's default mode) and field access. + *
Used for setter/field injection via {@link #bind(PropertyValues)}, and not + * applicable to constructor initialization via {@link #construct(ValueResolver)}. * @see #initBeanPropertyAccess() * @see org.springframework.beans.BeanWrapper#setAutoGrowNestedPaths */ @@ -233,6 +273,8 @@ public boolean isAutoGrowNestedPaths() { * Specify the limit for array and collection auto-growing. *
Default is 256, preventing OutOfMemoryErrors in case of large indexes. * Raise this limit if your auto-growing needs are unusually high. + *
Used for setter/field injection via {@link #bind(PropertyValues)}, and not + * applicable to constructor initialization via {@link #construct(ValueResolver)}. * @see #initBeanPropertyAccess() * @see org.springframework.beans.BeanWrapper#setAutoGrowCollectionLimit */ @@ -335,7 +377,7 @@ protected ConfigurablePropertyAccessor getPropertyAccessor() { */ protected SimpleTypeConverter getSimpleTypeConverter() { if (this.typeConverter == null) { - this.typeConverter = new SimpleTypeConverter(); + this.typeConverter = new ExtendedTypeConverter(); if (this.conversionService != null) { this.typeConverter.setConversionService(this.conversionService); } @@ -389,6 +431,9 @@ public BindingResult getBindingResult() { *
Note that this setting only applies to binding operations * on this DataBinder, not to retrieving values via its * {@link #getBindingResult() BindingResult}. + *
Used for setter/field inject via {@link #bind(PropertyValues)}, and not + * applicable to constructor initialization via {@link #construct(ValueResolver)}, + * which uses only the values it needs. * @see #bind */ public void setIgnoreUnknownFields(boolean ignoreUnknownFields) { @@ -411,6 +456,9 @@ public boolean isIgnoreUnknownFields() { *
Note that this setting only applies to binding operations * on this DataBinder, not to retrieving values via its * {@link #getBindingResult() BindingResult}. + *
Used for setter/field inject via {@link #bind(PropertyValues)}, and not + * applicable to constructor initialization via {@link #construct(ValueResolver)}, + * which uses only the values it needs. * @see #bind */ public void setIgnoreInvalidFields(boolean ignoreInvalidFields) { @@ -439,6 +487,9 @@ public boolean isIgnoreInvalidFields() { *
More sophisticated matching can be implemented by overriding the * {@link #isAllowed} method. *
Alternatively, specify a list of disallowed field patterns. + *
Used for setter/field inject via {@link #bind(PropertyValues)}, and not + * applicable to constructor initialization via {@link #construct(ValueResolver)}, + * which uses only the values it needs. * @param allowedFields array of allowed field patterns * @see #setDisallowedFields * @see #isAllowed(String) @@ -475,6 +526,9 @@ public String[] getAllowedFields() { *
More sophisticated matching can be implemented by overriding the * {@link #isAllowed} method. *
Alternatively, specify a list of allowed field patterns. + *
Used for setter/field inject via {@link #bind(PropertyValues)}, and not + * applicable to constructor initialization via {@link #construct(ValueResolver)}, + * which uses only the values it needs. * @param disallowedFields array of disallowed field patterns * @see #setAllowedFields * @see #isAllowed(String) @@ -508,6 +562,9 @@ public String[] getDisallowedFields() { * incoming property values, a corresponding "missing field" error * will be created, with error code "required" (by the default * binding error processor). + *
Used for setter/field inject via {@link #bind(PropertyValues)}, and not
+ * applicable to constructor initialization via {@link #construct(ValueResolver)},
+ * which uses only the values it needs.
* @param requiredFields array of field names
* @see #setBindingErrorProcessor
* @see DefaultBindingErrorProcessor#MISSING_FIELD_ERROR_CODE
@@ -770,6 +827,133 @@ public Uses a public, no-arg constructor if available in the target object type,
+ * also supporting a "primary constructor" approach for data classes as follows:
+ * It understands the JavaBeans {@code ConstructorProperties} annotation as
+ * well as runtime-retained parameter names in the bytecode, associating
+ * input values with constructor arguments by name. If no such constructor is
+ * found, the default constructor will be used (even if not public), assuming
+ * subsequent bean property bindings through setter methods.
+ * After the call, use {@link #getBindingResult()} to check for failures
+ * to bind to, and/or validate constructor arguments. If there are no errors,
+ * the target is set, and {@link #doBind(MutablePropertyValues)} can be used
+ * for further initialization via setters.
+ * @param valueResolver to resolve constructor argument values with
+ * @throws BeanInstantiationException in case of constructor failure
+ * @since 6.1
+ */
+ public final void construct(ValueResolver valueResolver) {
+ Assert.state(this.target == null, "Target instance already available");
+ Assert.state(this.targetType != null, "Target type not set");
+
+ Class> clazz = this.targetType.resolve();
+ clazz = (Optional.class.equals(clazz) ? this.targetType.resolveGeneric(0) : clazz);
+ Assert.state(clazz != null, "Unknown data binding target type");
+
+ Constructor> ctor = BeanUtils.getResolvableConstructor(clazz);
+ if (ctor.getParameterCount() == 0) {
+ // A single default constructor -> clearly a standard JavaBeans arrangement.
+ this.target = BeanUtils.instantiateClass(ctor);
+ }
+ else {
+ // A single data class constructor -> resolve constructor arguments from request parameters.
+ String[] paramNames = BeanUtils.getParameterNames(ctor);
+ Class>[] paramTypes = ctor.getParameterTypes();
+ Object[] args = new Object[paramTypes.length];
+ Set This call can create field errors, representing basic binding
@@ -972,4 +1156,36 @@ else if (validator != null) {
return getBindingResult().getModel();
}
+
+ /**
+ * Contract to resolve a value in {@link #construct(ValueResolver)}.
+ */
+ @FunctionalInterface
+ public interface ValueResolver {
+
+ /**
+ * Look up the value for a constructor argument.
+ * @param name the argument name
+ * @param type the argument type
+ * @return the resolved value, possibly {@code null}
+ */
+ @Nullable
+ Object resolveValue(String name, Class> type);
+
+ }
+
+
+ /**
+ * {@link SimpleTypeConverter} that is also {@link PropertyEditorRegistrar}.
+ */
+ private static class ExtendedTypeConverter
+ extends SimpleTypeConverter implements PropertyEditorRegistrar {
+
+ @Override
+ public void registerCustomEditors(PropertyEditorRegistry registry) {
+ copyCustomEditorsTo(registry, null);
+ }
+ }
+
+
}
diff --git a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java
index 3ff10bd718ec..f004de89c710 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/ServletRequestDataBinder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,15 +16,21 @@
package org.springframework.web.bind;
+import java.lang.reflect.Array;
+import java.util.List;
+
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.Part;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
+import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
+import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartRequest;
import org.springframework.web.multipart.support.StandardServletPartUtils;
import org.springframework.web.util.WebUtils;
@@ -46,10 +52,9 @@
* which include specifying allowed/required fields, and registering custom
* property editors.
*
- * Can also be used for manual data binding in custom web controllers:
- * for example, in a plain Controller implementation or in a MultiActionController
- * handler method. Simply instantiate a ServletRequestDataBinder for each binding
- * process, and invoke {@code bind} with the current ServletRequest as argument:
+ * Can also be used for manual data binding. Simply instantiate a
+ * ServletRequestDataBinder for each binding process, and invoke {@code bind}
+ * with the current ServletRequest as argument:
*
* After the call, use {@link #getBindingResult()} to check for bind errors.
+ * If there are none, the target is set, and {@link #bind(ServletRequest)}
+ * can be called for further initialization via setters.
+ * @param request the request to bind
+ * @since 6.1
+ */
+ public void construct(ServletRequest request) {
+ construct(createValueResolver(request));
+ }
+
+ /**
+ * Allow subclasses to create the {@link ValueResolver} instance to use.
+ * @since 6.1
+ */
+ protected ServletRequestValueResolver createValueResolver(ServletRequest request) {
+ return new ServletRequestValueResolver(request, this);
+ }
+
/**
* Bind the parameters of the given request to this binder's target,
* also binding multipart files in case of a multipart request.
@@ -119,7 +145,7 @@ public void bind(ServletRequest request) {
if (multipartRequest != null) {
bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
}
- else if (StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.MULTIPART_FORM_DATA_VALUE)) {
+ else if (isFormDataPost(request)) {
HttpServletRequest httpServletRequest = WebUtils.getNativeRequest(request, HttpServletRequest.class);
if (httpServletRequest != null && HttpMethod.POST.matches(httpServletRequest.getMethod())) {
StandardServletPartUtils.bindParts(httpServletRequest, mpvs, isBindEmptyMultipartFiles());
@@ -129,6 +155,10 @@ else if (StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.MU
doBind(mpvs);
}
+ private static boolean isFormDataPost(ServletRequest request) {
+ return StringUtils.startsWithIgnoreCase(request.getContentType(), MediaType.MULTIPART_FORM_DATA_VALUE);
+ }
+
/**
* Extension point that subclasses can use to add extra bind values for a
* request. Invoked before {@link #doBind(MutablePropertyValues)}.
@@ -153,4 +183,73 @@ public void closeNoCatch() throws ServletRequestBindingException {
}
}
+ /**
+ * Return a {@code ServletRequest} {@link ValueResolver}. Mainly for use from
+ * {@link org.springframework.web.bind.support.WebRequestDataBinder}.
+ * @since 6.1
+ */
+ public static ValueResolver valueResolver(ServletRequest request, WebDataBinder binder) {
+ return new ServletRequestValueResolver(request, binder);
+ }
+
+
+ /**
+ * Resolver that looks up values to bind in a {@link ServletRequest}.
+ */
+ protected static class ServletRequestValueResolver implements ValueResolver {
+
+ private final ServletRequest request;
+
+ private final WebDataBinder dataBinder;
+
+ protected ServletRequestValueResolver(ServletRequest request, WebDataBinder dataBinder) {
+ this.request = request;
+ this.dataBinder = dataBinder;
+ }
+
+ protected ServletRequest getRequest() {
+ return this.request;
+ }
+
+ @Nullable
+ @Override
+ public final Object resolveValue(String name, Class> paramType) {
+ Object value = getRequestParameter(name, paramType);
+ if (value == null) {
+ value = this.dataBinder.resolvePrefixValue(name, paramType, this::getRequestParameter);
+ }
+ if (value == null) {
+ value = getMultipartValue(name);
+ }
+ return value;
+ }
+
+ @Nullable
+ protected Object getRequestParameter(String name, Class> type) {
+ Object value = this.request.getParameterValues(name);
+ return (ObjectUtils.isArray(value) && Array.getLength(value) == 1 ? Array.get(value, 0) : value);
+ }
+
+ @Nullable
+ private Object getMultipartValue(String name) {
+ MultipartRequest multipartRequest = WebUtils.getNativeRequest(this.request, MultipartRequest.class);
+ if (multipartRequest != null) {
+ List By default, if the parameter has {@code @Valid}, Bean Validation is
+ * excluded, deferring to method validation.
+ */
+ @Override
+ public final WebDataBinder createBinder(
+ NativeWebRequest webRequest, @Nullable Object target, String objectName,
+ MethodParameter parameter) throws Exception {
+
+ return createBinderInternal(webRequest, target, objectName, parameter);
+ }
+
+ private WebDataBinder createBinderInternal(
+ NativeWebRequest webRequest, @Nullable Object target, String objectName,
+ @Nullable MethodParameter parameter) throws Exception {
+
WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest);
+
+ if (target == null && parameter != null) {
+ dataBinder.setTargetType(ResolvableType.forMethodParameter(parameter));
+ }
+
if (this.initializer != null) {
this.initializer.initBinder(dataBinder);
}
initBinder(dataBinder, webRequest);
+
+ if (this.methodValidationApplicable && parameter != null) {
+ MethodValidationInitializer.initBinder(dataBinder, parameter);
+ }
+
return dataBinder;
}
@@ -104,30 +135,13 @@ protected void initBinder(WebDataBinder dataBinder, NativeWebRequest webRequest)
}
- /**
- * {@inheritDoc}.
- * By default, if the parameter has {@code @Valid}, Bean Validation is
- * excluded, deferring to method validation.
- */
- @Override
- public WebDataBinder createBinder(
- NativeWebRequest webRequest, @Nullable Object target, String objectName,
- MethodParameter parameter) throws Exception {
-
- WebDataBinder dataBinder = createBinder(webRequest, target, objectName);
- if (this.methodValidationApplicable) {
- MethodValidationInitializer.updateBinder(dataBinder, parameter);
- }
- return dataBinder;
- }
-
/**
* Excludes Bean Validation if the method parameter has {@code @Valid}.
*/
private static class MethodValidationInitializer {
- public static void updateBinder(DataBinder binder, MethodParameter parameter) {
+ public static void initBinder(DataBinder binder, MethodParameter parameter) {
for (Annotation annotation : parameter.getParameterAnnotations()) {
if (annotation.annotationType().getName().equals("jakarta.validation.Valid")) {
binder.setExcludedValidators(validator -> validator instanceof jakarta.validation.Validator);
diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java
index 98ba841a27e0..778fdc7ea8f2 100644
--- a/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java
+++ b/spring-web/src/main/java/org/springframework/web/bind/support/WebRequestDataBinder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
package org.springframework.web.bind.support;
+import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.MutablePropertyValues;
@@ -25,6 +26,7 @@
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
+import org.springframework.web.bind.ServletRequestDataBinder;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.WebRequest;
@@ -98,6 +100,25 @@ public WebRequestDataBinder(@Nullable Object target, String objectName) {
}
+ /**
+ * Use a default or single data constructor to create the target by
+ * binding request parameters, multipart files, or parts to constructor args.
+ * After the call, use {@link #getBindingResult()} to check for bind errors.
+ * If there are none, the target is set, and {@link #bind(WebRequest)}
+ * can be called for further initialization via setters.
+ * @param request the request to bind
+ * @since 6.1
+ */
+ public void construct(WebRequest request) {
+ if (request instanceof NativeWebRequest nativeRequest) {
+ ServletRequest servletRequest = nativeRequest.getNativeRequest(ServletRequest.class);
+ if (servletRequest != null) {
+ construct(ServletRequestDataBinder.valueResolver(servletRequest, this));
+ }
+ }
+ }
+
+
/**
* Bind the parameters of the given request to this binder's target,
* also binding multipart files in case of a multipart request.
diff --git a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java
index 54fbfef65c09..929fc6b0faef 100644
--- a/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java
+++ b/spring-web/src/main/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessor.java
@@ -17,37 +17,22 @@
package org.springframework.web.method.annotation;
import java.lang.annotation.Annotation;
-import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
-import java.util.HashSet;
-import java.util.List;
import java.util.Map;
import java.util.Optional;
-import java.util.Set;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.Part;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
-import org.springframework.beans.BeanInstantiationException;
import org.springframework.beans.BeanUtils;
-import org.springframework.beans.TypeMismatchException;
-import org.springframework.core.KotlinDetector;
import org.springframework.core.MethodParameter;
-import org.springframework.http.HttpHeaders;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
-import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
-import org.springframework.validation.ObjectError;
import org.springframework.validation.SmartValidator;
-import org.springframework.validation.Validator;
import org.springframework.validation.annotation.ValidationAnnotationUtils;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.WebDataBinder;
@@ -58,9 +43,6 @@
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
-import org.springframework.web.multipart.MultipartFile;
-import org.springframework.web.multipart.MultipartRequest;
-import org.springframework.web.multipart.support.StandardServletPartUtils;
/**
* Resolve {@code @ModelAttribute} annotated method arguments and handle
@@ -134,45 +116,45 @@ public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAn
mavContainer.setBinding(name, ann.binding());
}
- Object attribute = null;
+ Object attribute;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
- }
+ if (attribute == null || ObjectUtils.unwrapOptional(attribute) == null) {
+ bindingResult = binderFactory.createBinder(webRequest, null, name).getBindingResult();
+ attribute = wrapAsOptionalIfNecessary(parameter, null);
+ }
+ }
else {
- // Create attribute instance
try {
+ // Mainly to allow subclasses alternative to create attribute
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (MethodArgumentNotValidException ex) {
if (isBindExceptionRequired(parameter)) {
- // No BindingResult parameter -> fail with BindException
throw ex;
}
- // Otherwise, expose null/empty value and associated BindingResult
- if (parameter.getParameterType() == Optional.class) {
- attribute = Optional.empty();
- }
- else {
- attribute = ex.getTarget();
- }
+ attribute = wrapAsOptionalIfNecessary(parameter, ex.getTarget());
bindingResult = ex.getBindingResult();
}
}
+ // No BindingResult yet, proceed with binding and validation
if (bindingResult == null) {
- // Bean property binding and validation;
- // skipped in case of binding failure on construction.
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name, parameter);
- if (binder.getTarget() != null) {
+ if (attribute == null) {
+ constructAttribute(binder, webRequest);
+ attribute = wrapAsOptionalIfNecessary(parameter, binder.getTarget());
+ }
+ if (!binder.getBindingResult().hasErrors()) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
validateIfApplicable(binder, parameter);
- if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
- throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
- }
+ }
+ if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
+ throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
@@ -189,165 +171,60 @@ public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAn
return attribute;
}
+ @Nullable
+ private static Object wrapAsOptionalIfNecessary(MethodParameter parameter, @Nullable Object target) {
+ return (parameter.getParameterType() == Optional.class ? Optional.ofNullable(target) : target);
+ }
+
/**
* Extension point to create the model attribute if not found in the model,
* with subsequent parameter binding through bean properties (unless suppressed).
- * The default implementation typically uses the unique public no-arg constructor
- * if available but also handles a "primary constructor" approach for data classes:
- * It understands the JavaBeans {@code ConstructorProperties} annotation as well as
- * runtime-retained parameter names in the bytecode, associating request parameters
- * with constructor arguments by name. If no such constructor is found, the default
- * constructor will be used (even if not public), assuming subsequent bean property
- * bindings through setter methods.
+ * By default, as of 6.1 this method returns {@code null} in which case
+ * {@link org.springframework.validation.DataBinder#construct} is used instead
+ * to create the model attribute. The main purpose of this method then is to
+ * allow to create the model attribute in some other, alternative way.
* @param attributeName the name of the attribute (never {@code null})
* @param parameter the method parameter declaration
* @param binderFactory for creating WebDataBinder instance
* @param webRequest the current request
* @return the created model attribute (never {@code null})
* @throws BindException in case of constructor argument binding failure
- * @throws Exception in case of constructor invocation failure
+ * @throws Exception in case of constructor instantiation failure
* @see #constructAttribute(Constructor, String, MethodParameter, WebDataBinderFactory, NativeWebRequest)
* @see BeanUtils#findPrimaryConstructor(Class)
*/
+ @Nullable
protected Object createAttribute(String attributeName, MethodParameter parameter,
WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
- MethodParameter nestedParameter = parameter.nestedIfOptional();
- Class> clazz = nestedParameter.getNestedParameterType();
-
- Constructor> ctor = BeanUtils.getResolvableConstructor(clazz);
- Object attribute = constructAttribute(ctor, attributeName, parameter, binderFactory, webRequest);
- if (parameter != nestedParameter) {
- attribute = Optional.of(attribute);
- }
- return attribute;
+ return null;
}
/**
* Construct a new attribute instance with the given constructor.
- * Called from
- * {@link #createAttribute(String, MethodParameter, WebDataBinderFactory, NativeWebRequest)}
- * after constructor resolution.
- * @param ctor the constructor to use
- * @param attributeName the name of the attribute (never {@code null})
- * @param parameter the method parameter declaration
- * @param binderFactory for creating WebDataBinder instance
- * @param webRequest the current request
- * @return the created model attribute (never {@code null})
- * @throws BindException in case of constructor argument binding failure
- * @throws Exception in case of constructor invocation failure
* @since 5.1
+ * @deprecated and not called; replaced by built-in support for
+ * constructor initialization in {@link org.springframework.validation.DataBinder}
*/
- @SuppressWarnings("serial")
+ @Deprecated(since = "6.1", forRemoval = true)
protected Object constructAttribute(Constructor> ctor, String attributeName, MethodParameter parameter,
WebDataBinderFactory binderFactory, NativeWebRequest webRequest) throws Exception {
- if (ctor.getParameterCount() == 0) {
- // A single default constructor -> clearly a standard JavaBeans arrangement.
- return BeanUtils.instantiateClass(ctor);
- }
-
- // A single data class constructor -> resolve constructor arguments from request parameters.
- String[] paramNames = BeanUtils.getParameterNames(ctor);
- Class>[] paramTypes = ctor.getParameterTypes();
- Object[] args = new Object[paramTypes.length];
- WebDataBinder binder = binderFactory.createBinder(webRequest, null, attributeName, parameter);
- String fieldDefaultPrefix = binder.getFieldDefaultPrefix();
- String fieldMarkerPrefix = binder.getFieldMarkerPrefix();
- boolean bindingFailure = false;
- Set The default implementation checks for {@code @jakarta.validation.Valid},
- * Spring's {@link org.springframework.validation.annotation.Validated},
- * and custom annotations whose name starts with "Valid".
- * @param binder the DataBinder to be used
- * @param parameter the method parameter declaration
- * @param targetType the target type
- * @param fieldName the name of the field
- * @param value the candidate value
* @since 5.1
- * @see #validateIfApplicable(WebDataBinder, MethodParameter)
- * @see SmartValidator#validateValue(Class, String, Object, Errors, Object...)
+ * @deprecated and not called; replaced by built-in support for
+ * constructor initialization in {@link org.springframework.validation.DataBinder}
*/
+ @Deprecated(since = "6.1", forRemoval = true)
protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter parameter,
Class> targetType, String fieldName, @Nullable Object value) {
- for (Annotation ann : parameter.getParameterAnnotations()) {
- Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
- if (validationHints != null) {
- for (Validator validator : binder.getValidators()) {
- if (validator instanceof SmartValidator smartValidator) {
- try {
- smartValidator.validateValue(targetType, fieldName, value,
- binder.getBindingResult(), validationHints);
- }
- catch (IllegalArgumentException ex) {
- // No corresponding field on the target class...
- }
- }
- }
- break;
- }
- }
+ throw new UnsupportedOperationException();
}
/**
diff --git a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java
index 9f6d6cb64a73..d926dd25d292 100644
--- a/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java
+++ b/spring-web/src/test/java/org/springframework/web/method/annotation/ModelAttributeMethodProcessorTests.java
@@ -26,6 +26,7 @@
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.core.MethodParameter;
+import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.validation.BindingResult;
@@ -51,7 +52,7 @@
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.ArgumentMatchers.notNull;
+import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@@ -161,11 +162,13 @@ public void resolveArgumentFromModel() throws Exception {
@Test
public void resolveArgumentViaDefaultConstructor() throws Exception {
WebDataBinder dataBinder = new WebRequestDataBinder(null);
+ dataBinder.setTargetType(ResolvableType.forMethodParameter(this.paramNamedValidModelAttr));
+
WebDataBinderFactory factory = mock();
- given(factory.createBinder(any(), notNull(), eq("attrName"), any())).willReturn(dataBinder);
+ given(factory.createBinder(any(), isNull(), eq("attrName"), any())).willReturn(dataBinder);
this.processor.resolveArgument(this.paramNamedValidModelAttr, this.container, this.request, factory);
- verify(factory).createBinder(any(), notNull(), eq("attrName"), any());
+ verify(factory).createBinder(any(), isNull(), eq("attrName"), any());
}
@Test
@@ -281,6 +284,7 @@ public void resolveConstructorListArgumentFromCommaSeparatedRequestParameter() t
given(factory.createBinder(any(), any(), eq("testBeanWithConstructorArgs"), any()))
.willAnswer(invocation -> {
WebRequestDataBinder binder = new WebRequestDataBinder(invocation.getArgument(1));
+ binder.setTargetType(ResolvableType.forMethodParameter(this.beanWithConstructorArgs));
// Add conversion service which will convert "1,2" to a list
binder.setConversionService(new DefaultFormattingConversionService());
return binder;
diff --git a/spring-web/src/test/kotlin/org/springframework/web/method/annotation/ModelAttributeMethodProcessorKotlinTests.kt b/spring-web/src/test/kotlin/org/springframework/web/method/annotation/ModelAttributeMethodProcessorKotlinTests.kt
index 7330edf56329..1aba17990322 100644
--- a/spring-web/src/test/kotlin/org/springframework/web/method/annotation/ModelAttributeMethodProcessorKotlinTests.kt
+++ b/spring-web/src/test/kotlin/org/springframework/web/method/annotation/ModelAttributeMethodProcessorKotlinTests.kt
@@ -20,10 +20,12 @@ import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
-import org.mockito.ArgumentMatchers.*
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
import org.mockito.BDDMockito.given
import org.mockito.Mockito.mock
import org.springframework.core.MethodParameter
+import org.springframework.core.ResolvableType
import org.springframework.core.annotation.SynthesizingMethodParameter
import org.springframework.web.bind.MethodArgumentNotValidException
import org.springframework.web.bind.support.WebDataBinderFactory
@@ -31,7 +33,6 @@ import org.springframework.web.bind.support.WebRequestDataBinder
import org.springframework.web.context.request.ServletWebRequest
import org.springframework.web.method.support.ModelAndViewContainer
import org.springframework.web.testfixture.servlet.MockHttpServletRequest
-import kotlin.annotation.AnnotationTarget.*
/**
* Kotlin test fixture for [ModelAttributeMethodProcessor].
@@ -61,7 +62,11 @@ class ModelAttributeMethodProcessorKotlinTests {
val requestWithParam = ServletWebRequest(mockRequest)
val factory = mock
* MyBean myBean = new MyBean();
@@ -94,6 +99,27 @@ public ServletRequestDataBinder(@Nullable Object target, String objectName) {
}
+ /**
+ * Use a default or single data constructor to create the target by
+ * binding request parameters, multipart files, or parts to constructor args.
+ *