20
20
import java .lang .annotation .Annotation ;
21
21
import java .lang .reflect .Constructor ;
22
22
import java .util .Map ;
23
+ import java .util .Optional ;
23
24
24
25
import org .apache .commons .logging .Log ;
25
26
import org .apache .commons .logging .LogFactory ;
26
27
27
28
import org .springframework .beans .BeanUtils ;
29
+ import org .springframework .beans .TypeMismatchException ;
28
30
import org .springframework .core .DefaultParameterNameDiscoverer ;
29
31
import org .springframework .core .MethodParameter ;
30
32
import org .springframework .core .ParameterNameDiscoverer ;
31
33
import org .springframework .core .annotation .AnnotationUtils ;
32
34
import org .springframework .lang .Nullable ;
33
35
import org .springframework .util .Assert ;
34
36
import org .springframework .validation .BindException ;
37
+ import org .springframework .validation .BindingResult ;
35
38
import org .springframework .validation .Errors ;
39
+ import org .springframework .validation .FieldError ;
36
40
import org .springframework .validation .annotation .Validated ;
37
41
import org .springframework .web .bind .WebDataBinder ;
38
42
import org .springframework .web .bind .annotation .ModelAttribute ;
@@ -110,61 +114,120 @@ public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAn
110
114
Assert .state (binderFactory != null , "ModelAttributeMethodProcessor requires WebDataBinderFactory" );
111
115
112
116
String name = ModelFactory .getNameForParameter (parameter );
113
- Object attribute = (mavContainer .containsAttribute (name ) ? mavContainer .getModel ().get (name ) :
114
- createAttribute (name , parameter , binderFactory , webRequest ));
115
-
116
117
if (!mavContainer .isBindingDisabled (name )) {
117
118
ModelAttribute ann = parameter .getParameterAnnotation (ModelAttribute .class );
118
119
if (ann != null && !ann .binding ()) {
119
120
mavContainer .setBindingDisabled (name );
120
121
}
121
122
}
122
123
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
+ }
127
160
}
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 );
131
164
}
165
+ bindingResult = binder .getBindingResult ();
132
166
}
133
167
134
168
// 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 ();
136
170
mavContainer .removeAttributes (bindingResultModel );
137
171
mavContainer .addAllAttributes (bindingResultModel );
138
172
139
- return (parameter .getParameterType ().isInstance (attribute ) ? attribute :
140
- binder .convertIfNecessary (binder .getTarget (), parameter .getParameterType (), parameter ));
173
+ return attribute ;
141
174
}
142
175
143
176
/**
144
177
* Extension point to create the model attribute if not found in the model,
145
178
* 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.
152
186
* @param attributeName the name of the attribute (never {@code null})
153
187
* @param parameter the method parameter declaration
154
188
* @param binderFactory for creating WebDataBinder instance
155
189
* @param webRequest the current request
156
190
* @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)
157
195
*/
158
196
protected Object createAttribute (String attributeName , MethodParameter parameter ,
159
197
WebDataBinderFactory binderFactory , NativeWebRequest webRequest ) throws Exception {
160
198
161
- Class <?> type = parameter .getParameterType ();
162
-
199
+ MethodParameter nestedParameter = parameter .nestedIfOptional ();
200
+ Class <?> type = nestedParameter .getNestedParameterType ();
201
+
163
202
Constructor <?> ctor = BeanUtils .findPrimaryConstructor (type );
164
203
if (ctor == null ) {
165
204
throw new IllegalStateException ("No primary constructor found for " + type .getName ());
166
205
}
167
206
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
+
168
231
if (ctor .getParameterCount () == 0 ) {
169
232
// A single default constructor -> clearly a standard JavaBeans arrangement.
170
233
return BeanUtils .instantiateClass (ctor );
@@ -179,10 +242,22 @@ protected Object createAttribute(String attributeName, MethodParameter parameter
179
242
() -> "Invalid number of parameter names: " + paramNames .length + " for constructor " + ctor );
180
243
Object [] args = new Object [paramTypes .length ];
181
244
WebDataBinder binder = binderFactory .createBinder (webRequest , null , attributeName );
245
+ boolean bindingFailure = false ;
182
246
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 ());
186
261
}
187
262
return BeanUtils .instantiateClass (ctor , args );
188
263
}
@@ -219,11 +294,23 @@ protected void validateIfApplicable(WebDataBinder binder, MethodParameter parame
219
294
220
295
/**
221
296
* Whether to raise a fatal bind exception on validation errors.
297
+ * <p>The default implementation delegates to {@link #isBindExceptionRequired(MethodParameter)}.
222
298
* @param binder the data binder used to perform data binding
223
299
* @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)
225
302
*/
226
303
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 ) {
227
314
int i = parameter .getParameterIndex ();
228
315
Class <?>[] paramTypes = parameter .getExecutable ().getParameterTypes ();
229
316
boolean hasBindingResult = (paramTypes .length > (i + 1 ) && Errors .class .isAssignableFrom (paramTypes [i + 1 ]));
0 commit comments