2020import java .lang .annotation .Annotation ;
2121import java .lang .reflect .Constructor ;
2222import java .util .Map ;
23+ import java .util .Optional ;
2324
2425import org .apache .commons .logging .Log ;
2526import org .apache .commons .logging .LogFactory ;
2627
2728import org .springframework .beans .BeanUtils ;
29+ import org .springframework .beans .TypeMismatchException ;
2830import org .springframework .core .DefaultParameterNameDiscoverer ;
2931import org .springframework .core .MethodParameter ;
3032import org .springframework .core .ParameterNameDiscoverer ;
3133import org .springframework .core .annotation .AnnotationUtils ;
3234import org .springframework .lang .Nullable ;
3335import org .springframework .util .Assert ;
3436import org .springframework .validation .BindException ;
37+ import org .springframework .validation .BindingResult ;
3538import org .springframework .validation .Errors ;
39+ import org .springframework .validation .FieldError ;
3640import org .springframework .validation .annotation .Validated ;
3741import org .springframework .web .bind .WebDataBinder ;
3842import org .springframework .web .bind .annotation .ModelAttribute ;
@@ -110,61 +114,120 @@ public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAn
110114 Assert .state (binderFactory != null , "ModelAttributeMethodProcessor requires WebDataBinderFactory" );
111115
112116 String name = ModelFactory .getNameForParameter (parameter );
113- Object attribute = (mavContainer .containsAttribute (name ) ? mavContainer .getModel ().get (name ) :
114- createAttribute (name , parameter , binderFactory , webRequest ));
115-
116117 if (!mavContainer .isBindingDisabled (name )) {
117118 ModelAttribute ann = parameter .getParameterAnnotation (ModelAttribute .class );
118119 if (ann != null && !ann .binding ()) {
119120 mavContainer .setBindingDisabled (name );
120121 }
121122 }
122123
123- WebDataBinder binder = binderFactory .createBinder (webRequest , attribute , name );
124- if (binder .getTarget () != null ) {
125- if (!mavContainer .isBindingDisabled (name )) {
126- bindRequestParameters (binder , webRequest );
124+ Object attribute = null ;
125+ BindingResult bindingResult = null ;
126+
127+ if (mavContainer .containsAttribute (name )) {
128+ attribute = mavContainer .getModel ().get (name );
129+ }
130+ else {
131+ // Create attribute instance
132+ try {
133+ attribute = createAttribute (name , parameter , binderFactory , webRequest );
134+ }
135+ catch (BindException ex ) {
136+ if (isBindExceptionRequired (parameter )) {
137+ // No BindingResult parameter -> fail with BindException
138+ throw ex ;
139+ }
140+ // Otherwise, expose null/empty value and associated BindingResult
141+ if (parameter .getParameterType () == Optional .class ) {
142+ attribute = Optional .empty ();
143+ }
144+ bindingResult = ex .getBindingResult ();
145+ }
146+ }
147+
148+ if (bindingResult == null ) {
149+ // Bean property binding and validation;
150+ // skipped in case of binding failure on construction.
151+ WebDataBinder binder = binderFactory .createBinder (webRequest , attribute , name );
152+ if (binder .getTarget () != null ) {
153+ if (!mavContainer .isBindingDisabled (name )) {
154+ bindRequestParameters (binder , webRequest );
155+ }
156+ validateIfApplicable (binder , parameter );
157+ if (binder .getBindingResult ().hasErrors () && isBindExceptionRequired (binder , parameter )) {
158+ throw new BindException (binder .getBindingResult ());
159+ }
127160 }
128- validateIfApplicable ( binder , parameter );
129- if (binder . getBindingResult ().hasErrors () && isBindExceptionRequired ( binder , parameter )) {
130- throw new BindException (binder .getBindingResult () );
161+ // Value type adaptation, also covering java.util.Optional
162+ if (! parameter . getParameterType ().isInstance ( attribute )) {
163+ attribute = binder . convertIfNecessary (binder .getTarget (), parameter . getParameterType (), parameter );
131164 }
165+ bindingResult = binder .getBindingResult ();
132166 }
133167
134168 // Add resolved attribute and BindingResult at the end of the model
135- Map <String , Object > bindingResultModel = binder . getBindingResult () .getModel ();
169+ Map <String , Object > bindingResultModel = bindingResult .getModel ();
136170 mavContainer .removeAttributes (bindingResultModel );
137171 mavContainer .addAllAttributes (bindingResultModel );
138172
139- return (parameter .getParameterType ().isInstance (attribute ) ? attribute :
140- binder .convertIfNecessary (binder .getTarget (), parameter .getParameterType (), parameter ));
173+ return attribute ;
141174 }
142175
143176 /**
144177 * Extension point to create the model attribute if not found in the model,
145178 * with subsequent parameter binding through bean properties (unless suppressed).
146- * <p>The default implementation uses the unique public no-arg constructor, if any,
147- * which may have arguments: It understands the JavaBeans {@link ConstructorProperties}
148- * annotation as well as runtime-retained parameter names in the bytecode,
149- * associating request parameters with constructor arguments by name. If no such
150- * constructor is found, the default constructor will be used (even if not public),
151- * assuming subsequent bean property bindings through setter methods.
179+ * <p>The default implementation typically uses the unique public no-arg constructor
180+ * if available but also handles a "primary constructor" approach for data classes:
181+ * It understands the JavaBeans {@link ConstructorProperties} annotation as well as
182+ * runtime-retained parameter names in the bytecode, associating request parameters
183+ * with constructor arguments by name. If no such constructor is found, the default
184+ * constructor will be used (even if not public), assuming subsequent bean property
185+ * bindings through setter methods.
152186 * @param attributeName the name of the attribute (never {@code null})
153187 * @param parameter the method parameter declaration
154188 * @param binderFactory for creating WebDataBinder instance
155189 * @param webRequest the current request
156190 * @return the created model attribute (never {@code null})
191+ * @throws BindException in case of constructor argument binding failure
192+ * @throws Exception in case of constructor invocation failure
193+ * @see #constructAttribute(Constructor, String, WebDataBinderFactory, NativeWebRequest)
194+ * @see BeanUtils#findPrimaryConstructor(Class)
157195 */
158196 protected Object createAttribute (String attributeName , MethodParameter parameter ,
159197 WebDataBinderFactory binderFactory , NativeWebRequest webRequest ) throws Exception {
160198
161- Class <?> type = parameter .getParameterType ();
162-
199+ MethodParameter nestedParameter = parameter .nestedIfOptional ();
200+ Class <?> type = nestedParameter .getNestedParameterType ();
201+
163202 Constructor <?> ctor = BeanUtils .findPrimaryConstructor (type );
164203 if (ctor == null ) {
165204 throw new IllegalStateException ("No primary constructor found for " + type .getName ());
166205 }
167206
207+ Object attribute = constructAttribute (ctor , attributeName , binderFactory , webRequest );
208+ if (parameter != nestedParameter ) {
209+ attribute = Optional .of (attribute );
210+ }
211+ return attribute ;
212+ }
213+
214+ /**
215+ * Construct a new attribute instance with the given constructor.
216+ * <p>Called from
217+ * {@link #createAttribute(String, MethodParameter, WebDataBinderFactory, NativeWebRequest)}
218+ * after constructor resolution.
219+ * @param ctor the constructor to use
220+ * @param attributeName the name of the attribute (never {@code null})
221+ * @param binderFactory for creating WebDataBinder instance
222+ * @param webRequest the current request
223+ * @return the created model attribute (never {@code null})
224+ * @throws BindException in case of constructor argument binding failure
225+ * @throws Exception in case of constructor invocation failure
226+ * @since 5.0
227+ */
228+ protected Object constructAttribute (Constructor <?> ctor , String attributeName ,
229+ WebDataBinderFactory binderFactory , NativeWebRequest webRequest ) throws Exception {
230+
168231 if (ctor .getParameterCount () == 0 ) {
169232 // A single default constructor -> clearly a standard JavaBeans arrangement.
170233 return BeanUtils .instantiateClass (ctor );
@@ -179,10 +242,22 @@ protected Object createAttribute(String attributeName, MethodParameter parameter
179242 () -> "Invalid number of parameter names: " + paramNames .length + " for constructor " + ctor );
180243 Object [] args = new Object [paramTypes .length ];
181244 WebDataBinder binder = binderFactory .createBinder (webRequest , null , attributeName );
245+ boolean bindingFailure = false ;
182246 for (int i = 0 ; i < paramNames .length ; i ++) {
183- String [] parameterValues = webRequest .getParameterValues (paramNames [i ]);
184- args [i ] = (parameterValues != null ? binder .convertIfNecessary (parameterValues , paramTypes [i ],
185- new MethodParameter (ctor , i )) : null );
247+ String [] paramValues = webRequest .getParameterValues (paramNames [i ]);
248+ try {
249+ args [i ] = (paramValues != null ?
250+ binder .convertIfNecessary (paramValues , paramTypes [i ], new MethodParameter (ctor , i )) : null );
251+ }
252+ catch (TypeMismatchException ex ) {
253+ bindingFailure = true ;
254+ binder .getBindingResult ().addError (new FieldError (
255+ binder .getObjectName (), paramNames [i ], ex .getValue (), true ,
256+ new String [] {ex .getErrorCode ()}, null , ex .getLocalizedMessage ()));
257+ }
258+ }
259+ if (bindingFailure ) {
260+ throw new BindException (binder .getBindingResult ());
186261 }
187262 return BeanUtils .instantiateClass (ctor , args );
188263 }
@@ -219,11 +294,23 @@ protected void validateIfApplicable(WebDataBinder binder, MethodParameter parame
219294
220295 /**
221296 * Whether to raise a fatal bind exception on validation errors.
297+ * <p>The default implementation delegates to {@link #isBindExceptionRequired(MethodParameter)}.
222298 * @param binder the data binder used to perform data binding
223299 * @param parameter the method parameter declaration
224- * @return {@code true} if the next method argument is not of type {@link Errors}
300+ * @return {@code true} if the next method parameter is not of type {@link Errors}
301+ * @see #isBindExceptionRequired(MethodParameter)
225302 */
226303 protected boolean isBindExceptionRequired (WebDataBinder binder , MethodParameter parameter ) {
304+ return isBindExceptionRequired (parameter );
305+ }
306+
307+ /**
308+ * Whether to raise a fatal bind exception on validation errors.
309+ * @param parameter the method parameter declaration
310+ * @return {@code true} if the next method parameter is not of type {@link Errors}
311+ * @since 5.0
312+ */
313+ protected boolean isBindExceptionRequired (MethodParameter parameter ) {
227314 int i = parameter .getParameterIndex ();
228315 Class <?>[] paramTypes = parameter .getExecutable ().getParameterTypes ();
229316 boolean hasBindingResult = (paramTypes .length > (i + 1 ) && Errors .class .isAssignableFrom (paramTypes [i + 1 ]));
0 commit comments