From ea398d7b7e72d1e1a9f7a002a291f4e0b8660e70 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Thu, 22 Jun 2023 20:36:28 +0100 Subject: [PATCH] Support constructing target object in DataBinder See gh-26721 --- .../validation/DataBinder.java | 226 ++++++++++++++- .../web/bind/ServletRequestDataBinder.java | 111 +++++++- .../web/bind/WebDataBinder.java | 30 +- .../ConfigurableWebBindingInitializer.java | 22 +- .../support/DefaultDataBinderFactory.java | 50 ++-- .../bind/support/WebRequestDataBinder.java | 23 +- .../ModelAttributeMethodProcessor.java | 266 ++++-------------- .../ModelAttributeMethodProcessorTests.java | 10 +- ...odelAttributeMethodProcessorKotlinTests.kt | 17 +- .../ExtendedServletRequestDataBinder.java | 42 ++- .../ServletModelAttributeMethodProcessor.java | 32 +-- 11 files changed, 550 insertions(+), 279 deletions(-) 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 T convertIfNecessary(@Nullable Object value, @Nullable Class requi } + /** + * Create the target with constructor injection of values. It is expected that + * {@link #setTargetType(ResolvableType)} was previously called and that + * {@link #getTarget()} is {@code null}. + *

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 failedParamNames = new HashSet<>(4); + boolean bindFailure = false; + for (int i = 0; i < paramNames.length; i++) { + String paramName = paramNames[i]; + Class paramType = paramTypes[i]; + Object value = valueResolver.resolveValue(paramName, paramType); + try { + MethodParameter methodParam = MethodParameter.forFieldAwareConstructor(ctor, i, paramName); + if (value == null && methodParam.isOptional()) { + args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null); + } + else { + args[i] = convertIfNecessary(value, paramType, methodParam); + } + } + catch (TypeMismatchException ex) { + ex.initPropertyName(paramName); + args[i] = null; + failedParamNames.add(paramName); + getBindingResult().recordFieldValue(paramName, paramType, value); + getBindingErrorProcessor().processPropertyAccessException(ex, getBindingResult()); + bindFailure = true; + } + } + + if (bindFailure) { + for (int i = 0; i < paramNames.length; i++) { + String paramName = paramNames[i]; + if (!failedParamNames.contains(paramName)) { + Object value = args[i]; + getBindingResult().recordFieldValue(paramName, paramTypes[i], value); + validateArgument(ctor.getDeclaringClass(), paramName, value); + } + } + if (!(this.targetType.getSource() instanceof MethodParameter param && param.isOptional())) { + try { + this.target = BeanUtils.instantiateClass(ctor, args); + } + catch (BeanInstantiationException ex) { + // swallow and proceed without target instance + } + } + return; + } + + try { + this.target = BeanUtils.instantiateClass(ctor, args); + } + catch (BeanInstantiationException ex) { + if (KotlinDetector.isKotlinType(clazz) && ex.getCause() instanceof NullPointerException cause) { + ObjectError error = new ObjectError(ctor.getName(), cause.getMessage()); + getBindingResult().addError(error); + return; + } + throw ex; + } + } + + // Now that target is set, add PropertyEditor's to PropertyAccessor + if (this.typeConverter != null) { + this.typeConverter.registerCustomEditors(getPropertyAccessor()); + } + } + + private void validateArgument(Class constructorClass, String name, @Nullable Object value) { + Object[] validationHints = null; + if (this.targetType.getSource() instanceof MethodParameter parameter) { + for (Annotation ann : parameter.getParameterAnnotations()) { + validationHints = ValidationAnnotationUtils.determineValidationHints(ann); + if (validationHints != null) { + break; + } + } + } + if (validationHints == null) { + return; + } + for (Validator validator : getValidatorsToApply()) { + if (validator instanceof SmartValidator smartValidator) { + try { + smartValidator.validateValue( + constructorClass, name, value, getBindingResult(), validationHints); + } + catch (IllegalArgumentException ex) { + // No corresponding field on the target class... + } + } + } + } + /** * Bind the given property values to this binder's target. *

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: * *

  * 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.
+	 * 

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 files = multipartRequest.getFiles(name); + if (!files.isEmpty()) { + return (files.size() == 1 ? files.get(0) : files); + } + } + else if (isFormDataPost(this.request)) { + HttpServletRequest httpRequest = WebUtils.getNativeRequest(this.request, HttpServletRequest.class); + if (httpRequest != null && HttpMethod.POST.matches(httpRequest.getMethod())) { + List parts = StandardServletPartUtils.getParts(httpRequest, name); + if (!parts.isEmpty()) { + return (parts.size() == 1 ? parts.get(0) : parts); + } + } + } + return null; + } + } + } diff --git a/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java b/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java index 54f516d157a8..586f6c67c9ff 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.java +++ b/spring-web/src/main/java/org/springframework/web/bind/WebDataBinder.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. @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.function.BiFunction; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyValue; @@ -193,6 +194,33 @@ public boolean isBindEmptyMultipartFiles() { } + /** + * Check if a value can be resolved if {@link #getFieldDefaultPrefix()} + * or {@link #getFieldMarkerPrefix()} is prepended. + * @param name the name of the value to resolve + * @param type the type of value expected + * @param resolver delegate resolver to use for the checks + * @return the resolved value, or {@code null} + * @since 6.1 + */ + @Nullable + protected Object resolvePrefixValue(String name, Class type, BiFunction, Object> resolver) { + Object value = resolver.apply(name, type); + if (value == null) { + String prefix = getFieldDefaultPrefix(); + if (prefix != null) { + value = resolver.apply(prefix + name, type); + } + if (value == null) { + prefix = getFieldMarkerPrefix(); + if (prefix != null && resolver.apply(prefix + name, type) != null) { + value = getEmptyValue(type); + } + } + } + return value; + } + /** * This implementation performs a field default and marker check * before delegating to the superclass binding process. diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java b/spring-web/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java index e2f600e005cf..8a3220f8ff1e 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/ConfigurableWebBindingInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -203,9 +203,11 @@ public void initBinder(WebDataBinder binder) { if (this.bindingErrorProcessor != null) { binder.setBindingErrorProcessor(this.bindingErrorProcessor); } - if (this.validator != null && binder.getTarget() != null && - this.validator.supports(binder.getTarget().getClass())) { - binder.setValidator(this.validator); + if (this.validator != null) { + Class type = getTargetType(binder); + if (type != null && this.validator.supports(type)) { + binder.setValidator(this.validator); + } } if (this.conversionService != null) { binder.setConversionService(this.conversionService); @@ -217,4 +219,16 @@ public void initBinder(WebDataBinder binder) { } } + @Nullable + private static Class getTargetType(WebDataBinder binder) { + Class type = null; + if (binder.getTarget() != null) { + type = binder.getTarget().getClass(); + } + else if (binder.getTargetType() != null) { + type = binder.getTargetType().resolve(); + } + return type; + } + } diff --git a/spring-web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java b/spring-web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java index e1551e389123..f65b875fbb9c 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java +++ b/spring-web/src/main/java/org/springframework/web/bind/support/DefaultDataBinderFactory.java @@ -19,6 +19,7 @@ import java.lang.annotation.Annotation; import org.springframework.core.MethodParameter; +import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; import org.springframework.validation.DataBinder; import org.springframework.web.bind.WebDataBinder; @@ -69,11 +70,41 @@ public void setMethodValidationApplicable(boolean methodValidationApplicable) { public final WebDataBinder createBinder( NativeWebRequest webRequest, @Nullable Object target, String objectName) throws Exception { + return createBinderInternal(webRequest, target, objectName, null); + } + + /** + * {@inheritDoc}. + *

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 failedParams = new HashSet<>(4); - - for (int i = 0; i < paramNames.length; i++) { - String paramName = paramNames[i]; - Class paramType = paramTypes[i]; - Object value = webRequest.getParameterValues(paramName); - - // Since WebRequest#getParameter exposes a single-value parameter as an array - // with a single element, we unwrap the single value in such cases, analogous - // to WebExchangeDataBinder.addBindValue(Map, String, List). - if (ObjectUtils.isArray(value) && Array.getLength(value) == 1) { - value = Array.get(value, 0); - } - - if (value == null) { - if (fieldDefaultPrefix != null) { - value = webRequest.getParameter(fieldDefaultPrefix + paramName); - } - if (value == null) { - if (fieldMarkerPrefix != null && webRequest.getParameter(fieldMarkerPrefix + paramName) != null) { - value = binder.getEmptyValue(paramType); - } - else { - value = resolveConstructorArgument(paramName, paramType, webRequest); - } - } - } - - try { - MethodParameter methodParam = MethodParameter.forFieldAwareConstructor(ctor, i, paramName); - if (value == null && methodParam.isOptional()) { - args[i] = (methodParam.getParameterType() == Optional.class ? Optional.empty() : null); - } - else { - args[i] = binder.convertIfNecessary(value, paramType, methodParam); - } - } - catch (TypeMismatchException ex) { - ex.initPropertyName(paramName); - args[i] = null; - failedParams.add(paramName); - binder.getBindingResult().recordFieldValue(paramName, paramType, value); - binder.getBindingErrorProcessor().processPropertyAccessException(ex, binder.getBindingResult()); - bindingFailure = true; - } - } - - if (bindingFailure) { - BindingResult result = binder.getBindingResult(); - for (int i = 0; i < paramNames.length; i++) { - String paramName = paramNames[i]; - if (!failedParams.contains(paramName)) { - Object value = args[i]; - result.recordFieldValue(paramName, paramTypes[i], value); - validateValueIfApplicable(binder, parameter, ctor.getDeclaringClass(), paramName, value); - } - } - if (!parameter.isOptional()) { - try { - Object target = BeanUtils.instantiateClass(ctor, args); - throw new MethodArgumentNotValidException(parameter, result) { - @Override - public Object getTarget() { - return target; - } - }; - } - catch (BeanInstantiationException ex) { - // swallow and proceed without target instance - } - } - throw new MethodArgumentNotValidException(parameter, result); - } + throw new UnsupportedOperationException(); + } - try { - return BeanUtils.instantiateClass(ctor, args); - } - catch (BeanInstantiationException ex) { - if (KotlinDetector.isKotlinType(ctor.getDeclaringClass()) && - ex.getCause() instanceof NullPointerException cause) { - BindingResult result = binder.getBindingResult(); - ObjectError error = new ObjectError(ctor.getName(), cause.getMessage()); - result.addError(error); - throw new MethodArgumentNotValidException(parameter, result); - } - else { - throw ex; - } - } + /** + * Extension point to create the attribute, binding the request to constructor args. + * @param binder the data binder instance to use for the binding + * @param request the current request + * @since 6.1 + */ + protected void constructAttribute(WebDataBinder binder, NativeWebRequest request) { + ((WebRequestDataBinder) binder).construct(request); } /** - * Extension point to bind the request to the target object. + * Extension point to bind the request to the target object via setters/fields. * @param binder the data binder instance to use for the binding * @param request the current request */ @@ -355,28 +232,16 @@ protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest requ ((WebRequestDataBinder) binder).bind(request); } - @Nullable + /** + * Resolve the value for a constructor argument. + * @deprecated and not called; replaced by built-in support for + * constructor initialization in {@link org.springframework.validation.DataBinder} + */ + @Deprecated(since = "6.1", forRemoval = true) public Object resolveConstructorArgument(String paramName, Class paramType, NativeWebRequest request) throws Exception { - MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class); - if (multipartRequest != null) { - List files = multipartRequest.getFiles(paramName); - if (!files.isEmpty()) { - return (files.size() == 1 ? files.get(0) : files); - } - } - else if (StringUtils.startsWithIgnoreCase( - request.getHeader(HttpHeaders.CONTENT_TYPE), MediaType.MULTIPART_FORM_DATA_VALUE)) { - HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); - if (servletRequest != null && HttpMethod.POST.matches(servletRequest.getMethod())) { - List parts = StandardServletPartUtils.getParts(servletRequest, paramName); - if (!parts.isEmpty()) { - return (parts.size() == 1 ? parts.get(0) : parts); - } - } - } - return null; + throw new UnsupportedOperationException(); } /** @@ -401,38 +266,15 @@ protected void validateIfApplicable(WebDataBinder binder, MethodParameter parame /** * Validate the specified candidate value if applicable. - *

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() given(factory.createBinder(any(), any(), eq("param"), any())) - .willAnswer { WebRequestDataBinder(it.getArgument(1)) } + .willAnswer { + val binder = WebRequestDataBinder(it.getArgument(1)) + binder.setTargetType(ResolvableType.forMethodParameter(this.param)) + binder + } assertThat(processor.resolveArgument(this.param, container, requestWithParam, factory)).isEqualTo(Param("b")) } @@ -71,7 +76,11 @@ class ModelAttributeMethodProcessorKotlinTests { val requestWithParam = ServletWebRequest(mockRequest) val factory = mock() given(factory.createBinder(any(), any(), eq("param"), any())) - .willAnswer { WebRequestDataBinder(it.getArgument(1)) } + .willAnswer { + val binder = WebRequestDataBinder(it.getArgument(1)) + binder.setTargetType(ResolvableType.forMethodParameter(this.param)) + binder + } assertThatThrownBy { processor.resolveArgument(this.param, container, requestWithParam, factory) }.isInstanceOf(MethodArgumentNotValidException::class.java) diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java index 1b2ce97ea7e2..15fda2594b08 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExtendedServletRequestDataBinder.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. @@ -23,6 +23,7 @@ import org.springframework.beans.MutablePropertyValues; import org.springframework.lang.Nullable; import org.springframework.web.bind.ServletRequestDataBinder; +import org.springframework.web.bind.WebDataBinder; import org.springframework.web.servlet.HandlerMapping; /** @@ -67,14 +68,17 @@ public ExtendedServletRequestDataBinder(@Nullable Object target, String objectNa } + @Override + protected ServletRequestValueResolver createValueResolver(ServletRequest request) { + return new ExtendedServletRequestValueResolver(request, this); + } + /** * Merge URI variables into the property values to use for data binding. */ @Override protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) { - String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; - @SuppressWarnings("unchecked") - Map uriVars = (Map) request.getAttribute(attr); + Map uriVars = getUriVars(request); if (uriVars != null) { uriVars.forEach((name, value) -> { if (mpvs.contains(name)) { @@ -89,4 +93,34 @@ protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) } } + @SuppressWarnings("unchecked") + @Nullable + private static Map getUriVars(ServletRequest request) { + String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; + return (Map) request.getAttribute(attr); + } + + + /** + * Resolver of values that looks up URI path variables. + */ + private static class ExtendedServletRequestValueResolver extends ServletRequestValueResolver { + + ExtendedServletRequestValueResolver(ServletRequest request, WebDataBinder dataBinder) { + super(request, dataBinder); + } + + @Override + protected Object getRequestParameter(String name, Class type) { + Object value = super.getRequestParameter(name, type); + if (value == null) { + Map uriVars = getUriVars(getRequest()); + if (uriVars != null) { + value = uriVars.get(name); + } + } + return value; + } + } + } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletModelAttributeMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletModelAttributeMethodProcessor.java index 68e8cdd24720..5569bc8b7737 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletModelAttributeMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ServletModelAttributeMethodProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 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. @@ -146,35 +146,25 @@ protected Object createAttributeFromRequestValue(String sourceValue, String attr } /** - * This implementation downcasts {@link WebDataBinder} to - * {@link ServletRequestDataBinder} before binding. - * @see ServletRequestDataBinderFactory + * Downcast to {@link ServletRequestDataBinder} to invoke {@code constructTarget(ServletRequest)}. */ @Override - protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) { + protected void constructAttribute(WebDataBinder binder, NativeWebRequest request) { ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class); Assert.state(servletRequest != null, "No ServletRequest"); ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder; - servletBinder.bind(servletRequest); + servletBinder.construct(servletRequest); } + /** + * Downcast to {@link ServletRequestDataBinder} to invoke {@code bind(ServletRequest)}. + */ @Override - @Nullable - public Object resolveConstructorArgument(String paramName, Class paramType, NativeWebRequest request) - throws Exception { - - Object value = super.resolveConstructorArgument(paramName, paramType, request); - if (value != null) { - return value; - } + protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) { ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class); - if (servletRequest != null) { - String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; - @SuppressWarnings("unchecked") - Map uriVars = (Map) servletRequest.getAttribute(attr); - return uriVars.get(paramName); - } - return null; + Assert.state(servletRequest != null, "No ServletRequest"); + ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder; + servletBinder.bind(servletRequest); } }