Polishing

See gh-30952
This commit is contained in:
Rossen Stoyanchev
2023-08-01 07:50:06 +03:00
committed by rstoyanchev
parent bd23798323
commit 667eb42a63
5 changed files with 176 additions and 219 deletions

View File

@@ -3,23 +3,21 @@
[.small]#xref:web/webmvc/mvc-controller/ann-initbinder.adoc[See equivalent in the Servlet stack]#
`@Controller` or `@ControllerAdvice` classes can have `@InitBinder` methods, to
initialize instances of `WebDataBinder`. Those, in turn, are used to:
`@Controller` or `@ControllerAdvice` classes can have `@InitBinder` methods to
initialize `WebDataBinder` instances that in turn can:
* Bind request parameters (that is, form data or query) to a model object.
* Convert `String`-based request values (such as request parameters, path variables,
headers, cookies, and others) to the target type of controller method arguments.
* Format model object values as `String` values when rendering HTML forms.
* Bind request parameters to a model object.
* Convert request values from string to object property types.
* Format model object properties as strings when rendering HTML forms.
`@InitBinder` methods can register controller-specific `java.beans.PropertyEditor` or
Spring `Converter` and `Formatter` components. In addition, you can use the
xref:web/webflux/config.adoc#webflux-config-conversion[WebFlux Java configuration] to register `Converter` and
`Formatter` types in a globally shared `FormattingConversionService`.
In an `@Controller`, `DataBinder` customizations apply locally within the controller,
or even to a specific model attribute referenced by name through the annotation.
In an `@ControllerAdvice` customizations can apply to all or a subset of controllers.
`@InitBinder` methods support many of the same arguments that `@RequestMapping` methods
do, except for `@ModelAttribute` (command object) arguments. Typically, they are declared
with a `WebDataBinder` argument, for registrations, and a `void` return value.
The following example uses the `@InitBinder` annotation:
You can register `PropertyEditor`, `Converter`, and `Formatter` components in the
`DataBinder` for type conversion. Alternatively, you can use the
xref:web/webflux/config.adoc#webflux-config-conversion[WebFlux config] to register
`Converter` and `Formatter` components in a globally shared `FormattingConversionService`.
--
[tabs]
@@ -112,4 +110,5 @@ Kotlin::
== Model Design
[.small]#xref:web/webmvc/mvc-controller/ann-initbinder.adoc#mvc-ann-initbinder-model-design[See equivalent in the Servlet stack]#
include::partial$web/web-data-binding-model-design.adoc[]

View File

@@ -3,11 +3,8 @@
[.small]#xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[See equivalent in the Servlet stack]#
You can use the `@ModelAttribute` annotation on a method argument to access an attribute from the
model or have it instantiated if not present. The model attribute is also overlaid with
the values of query parameters and form fields whose names match to field names. This is
referred to as data binding, and it saves you from having to deal with parsing and
converting individual query parameters and form fields. The following example binds an instance of `Pet`:
The `@ModelAttribute` method parameter annotation binds request parameters onto a model
object. For example:
[tabs]
======
@@ -18,7 +15,7 @@ Java::
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute Pet pet) { } // <1>
----
<1> Bind an instance of `Pet`.
<1> Bind to an instance of `Pet`.
Kotlin::
+
@@ -27,28 +24,34 @@ Kotlin::
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute pet: Pet): String { } // <1>
----
<1> Bind an instance of `Pet`.
<1> Bind to an instance of `Pet`.
======
The `Pet` instance in the preceding example is resolved as follows:
The `Pet` instance may be:
* From the model if already added through xref:web/webflux/controller/ann-modelattrib-methods.adoc[`Model`].
* From the HTTP session through xref:web/webflux/controller/ann-methods/sessionattributes.adoc[`@SessionAttributes`].
* From the invocation of a default constructor.
* From the invocation of a "`primary constructor`" with arguments that match query
parameters or form fields. Argument names are determined through JavaBeans
`@ConstructorProperties` or through runtime-retained parameter names in the bytecode.
* Accessed from the model where it could have been added by a
xref:web/webflux/controller/ann-modelattrib-methods.adoc[`Model`].
* Accessed from the HTTP session if the model attribute was listed in
the class-level xref:web/webflux/controller/ann-methods/sessionattributes.adoc[`@SessionAttributes`].
* Instantiated through a default constructor.
* Instantiated through a "`primary constructor`" with arguments that match to Servlet
request parameters. Argument names are determined through runtime-retained parameter
names in the bytecode.
After the model attribute instance is obtained, data binding is applied. The
`WebExchangeDataBinder` class matches names of query parameters and form fields to field
names on the target `Object`. Matching fields are populated after type conversion is applied
where necessary. For more on data binding (and validation), see
xref:web/webmvc/mvc-config/validation.adoc[Validation]. For more on customizing data binding, see
xref:web/webflux/controller/ann-initbinder.adoc[`DataBinder`].
Once a model attribute instance is available, `WebDataBinder` binds request parameters to
properties of the target `Object` with type conversion where necessary.
For more on data binding and validation, see
xref:web/webmvc/mvc-config/validation.adoc[Validation].
For more on customizing data binding, see
xref:web/webmvc/mvc-controller/ann-initbinder.adoc[DataBinder].
Data binding can result in errors. By default, a `WebExchangeBindException` is raised, but,
to check for such errors in the controller method, you can add a `BindingResult` argument
immediately next to the `@ModelAttribute`, as the following example shows:
WebFlux, unlike Spring MVC, supports reactive types in the model, e.g. `Mono<Account>`.
You can declare a `@ModelAttribute` argument with or without a reactive type wrapper, and
it will be resolved accordingly to the actual value.
If data binding results in errors, by default a `WebExchangeBindException` is raised,
but you can also add a `BindingResult` argument immediately next to the `@ModelAttribute`
in order to handle such errors in the controller method. For example:
[tabs]
======
@@ -81,49 +84,9 @@ Kotlin::
<1> Adding a `BindingResult`.
======
You can automatically apply validation after data binding by adding the
`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation (see also
xref:core/validation/beanvalidation.adoc[Bean Validation] and
xref:web/webmvc/mvc-config/validation.adoc[Spring validation]). The following example uses the `@Valid` annotation:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { // <1>
if (result.hasErrors()) {
return "petForm";
}
// ...
}
----
<1> Using `@Valid` on a model attribute argument.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1>
if (result.hasErrors()) {
return "petForm"
}
// ...
}
----
<1> Using `@Valid` on a model attribute argument.
======
Spring WebFlux, unlike Spring MVC, supports reactive types in the model -- for example,
`Mono<Account>` or `io.reactivex.Single<Account>`. You can declare a `@ModelAttribute` argument
with or without a reactive type wrapper, and it will be resolved accordingly,
to the actual value if necessary. However, note that, to use a `BindingResult`
argument, you must declare the `@ModelAttribute` argument before it without a reactive
type wrapper, as shown earlier. Alternatively, you can handle any errors through the
reactive type, as the following example shows:
To use a `BindingResult` argument, you must declare the `@ModelAttribute` argument before
it without a reactive type wrapper. If you want to use the reactive, you can handle errors
directly through it. For example:
[tabs]
======
@@ -160,6 +123,42 @@ Kotlin::
----
======
You can automatically apply validation after data binding by adding the
`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation (see
xref:core/validation/beanvalidation.adoc[Bean Validation] and
xref:web/webmvc/mvc-config/validation.adoc[Spring validation]). For example:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) { // <1>
if (result.hasErrors()) {
return "petForm";
}
// ...
}
----
<1> Using `@Valid` on a model attribute argument.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@Valid @ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1>
if (result.hasErrors()) {
return "petForm"
}
// ...
}
----
<1> Using `@Valid` on a model attribute argument.
======
If method validation applies because other parameters have `@Constraint` annotations,
then `HandlerMethodValidationException` would be raised instead. See the section on
controller method xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation].

View File

@@ -3,23 +3,25 @@
[.small]#xref:web/webflux/controller/ann-initbinder.adoc[See equivalent in the Reactive stack]#
`@Controller` or `@ControllerAdvice` classes can have `@InitBinder` methods that
initialize instances of `WebDataBinder`, and those, in turn, can:
`@Controller` or `@ControllerAdvice` classes can have `@InitBinder` methods to
initialize `WebDataBinder` instances that in turn can:
* Bind request parameters (that is, form or query data) to a model object.
* Convert String-based request values (such as request parameters, path variables,
headers, cookies, and others) to the target type of controller method arguments.
* Format model object values as `String` values when rendering HTML forms.
* Bind request parameters to a model object.
* Convert request values from string to object property types.
* Format model object properties as strings when rendering HTML forms.
`@InitBinder` methods can register controller-specific `java.beans.PropertyEditor` or
Spring `Converter` and `Formatter` components. In addition, you can use the
xref:web/webmvc/mvc-config/conversion.adoc[MVC config] to register `Converter` and `Formatter`
types in a globally shared `FormattingConversionService`.
In an `@Controller`, `DataBinder` customizations apply locally within the controller,
or even to a specific model attribute referenced by name through the annotation.
In an `@ControllerAdvice` customizations can apply to all or a subset of controllers.
`@InitBinder` methods support many of the same arguments that `@RequestMapping` methods
do, except for `@ModelAttribute` (command object) arguments. Typically, they are declared
with a `WebDataBinder` argument (for registrations) and a `void` return value.
The following listing shows an example:
You can register `PropertyEditor`, `Converter`, and `Formatter` components in the
`DataBinder` for type conversion. Alternatively, you can use the
xref:web/webmvc/mvc-config/conversion.adoc[MVC config] to register `Converter` and
`Formatter` components in a globally shared `FormattingConversionService`.
`@InitBinder` methods can have many of the same arguments that `@RequestMapping` methods
have, with the notable exception of `@ModelAttribute`. Typically, such methods have a
`WebDataBinder` argument (for registrations) and a `void` return value, for example:
[tabs]
======

View File

@@ -3,11 +3,8 @@
[.small]#xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[See equivalent in the Reactive stack]#
You can use the `@ModelAttribute` annotation on a method argument to access an attribute from
the model or have it be instantiated if not present. The model attribute is also overlain with
values from HTTP Servlet request parameters whose names match to field names. This is referred
to as data binding, and it saves you from having to deal with parsing and converting individual
query parameters and form fields. The following example shows how to do so:
The `@ModelAttribute` method parameter annotation binds request parameters onto a model
object. For example:
[tabs]
======
@@ -20,7 +17,7 @@ Java::
// method logic...
}
----
<1> Bind an instance of `Pet`.
<1> Bind to an instance of `Pet`.
Kotlin::
+
@@ -31,30 +28,27 @@ fun processSubmit(@ModelAttribute pet: Pet): String { // <1>
// method logic...
}
----
<1> Bind an instance of `Pet`.
<1> Bind to an instance of `Pet`.
======
The `Pet` instance above is sourced in one of the following ways:
The `Pet` instance may be:
* Retrieved from the model where it may have been added by a
* Accessed from the model where it could have been added by a
xref:web/webmvc/mvc-controller/ann-modelattrib-methods.adoc[@ModelAttribute method].
* Retrieved from the HTTP session if the model attribute was listed in
* Accessed from the HTTP session if the model attribute was listed in
the class-level xref:web/webmvc/mvc-controller/ann-methods/sessionattributes.adoc[`@SessionAttributes`] annotation.
* Obtained through a `Converter` where the model attribute name matches the name of a
request value such as a path variable or a request parameter (see next example).
* Instantiated using its default constructor.
* Obtained through a `Converter` if the model attribute name matches the name of a
request value such as a path variable or a request parameter (example follows).
* Instantiated through a default constructor.
* Instantiated through a "`primary constructor`" with arguments that match to Servlet
request parameters. Argument names are determined through JavaBeans
`@ConstructorProperties` or through runtime-retained parameter names in the bytecode.
request parameters. Argument names are determined through runtime-retained parameter
names in the bytecode.
One alternative to using a xref:web/webmvc/mvc-controller/ann-modelattrib-methods.adoc[@ModelAttribute method] to
supply it or relying on the framework to create the model attribute, is to have a
`Converter<String, T>` to provide the instance. This is applied when the model attribute
name matches to the name of a request value such as a path variable or a request
parameter, and there is a `Converter` from `String` to the model attribute type.
In the following example, the model attribute name is `account` which matches the URI
path variable `account`, and there is a registered `Converter<String, Account>` which
could load the `Account` from a data store:
As mentioned above, a `Converter<String, T>` may be used to obtain the model object if
the model attribute name matches to the name of a request value such as a path variable or a
request parameter, _and_ there is a compatible `Converter<String, T>`. In the below example,
the model attribute name `account` matches URI path variable `account`, and there is a
registered `Converter<String, Account>` that perhaps retrieves it from a persistence store:
[tabs]
======
@@ -67,7 +61,6 @@ Java::
// ...
}
----
<1> Bind an instance of `Account` using an explicit attribute name.
Kotlin::
+
@@ -78,50 +71,14 @@ Kotlin::
// ...
}
----
<1> Bind an instance of `Account` using an explicit attribute name.
======
After the model attribute instance is obtained, data binding is applied. The
`WebDataBinder` class matches Servlet request parameter names (query parameters and form
fields) to field names on the target `Object`. Matching fields are populated after type
conversion is applied, where necessary. For more on data binding (and validation), see
xref:web/webmvc/mvc-config/validation.adoc[Validation]. For more on customizing data binding, see
xref:web/webmvc/mvc-controller/ann-initbinder.adoc[`DataBinder`].
Data binding can result in errors. By default, a `BindException` is raised. However, to check
for such errors in the controller method, you can add a `BindingResult` argument immediately next
to the `@ModelAttribute`, as the following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { // <1>
if (result.hasErrors()) {
return "petForm";
}
// ...
}
----
<1> Adding a `BindingResult` next to the `@ModelAttribute`.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1>
if (result.hasErrors()) {
return "petForm"
}
// ...
}
----
<1> Adding a `BindingResult` next to the `@ModelAttribute`.
======
Once a model attribute instance is available, `WebDataBinder` binds request parameters to
properties of the target `Object` with type conversion where necessary.
For more on data binding and validation, see
xref:web/webmvc/mvc-config/validation.adoc[Validation].
For more on customizing data binding, see
xref:web/webmvc/mvc-controller/ann-initbinder.adoc[DataBinder].
In some cases, you may want access to a model attribute without data binding. For such
cases, you can inject the `Model` into the controller and access it directly or,
@@ -171,13 +128,48 @@ Kotlin::
// ...
}
----
<1> Setting `@ModelAttribute(binding=false)`.
<1> Setting `@ModelAt\tribute(binding=false)`.
======
If data binding results in errors, by default a `MethodArgumentNotValidException` is raised,
but you can also add a `BindingResult` argument immediately next to the `@ModelAttribute`
in order to handle such errors in the controller method. For example:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result) { // <1>
if (result.hasErrors()) {
return "petForm";
}
// ...
}
----
<1> Adding a `BindingResult` next to the `@ModelAttribute`.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
----
@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
fun processSubmit(@ModelAttribute("pet") pet: Pet, result: BindingResult): String { // <1>
if (result.hasErrors()) {
return "petForm"
}
// ...
}
----
<1> Adding a `BindingResult` next to the `@ModelAttribute`.
======
You can automatically apply validation after data binding by adding the
`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation
(xref:core/validation/beanvalidation.adoc[Bean Validation] and
xref:web/webmvc/mvc-config/validation.adoc[Spring validation]). The following example shows how to do so:
`jakarta.validation.Valid` annotation or Spring's `@Validated` annotation.
See xref:core/validation/beanvalidation.adoc[Bean Validation] and
xref:web/webmvc/mvc-config/validation.adoc[Spring validation]. For example:
[tabs]
======
@@ -210,15 +202,13 @@ Kotlin::
<1> Validate the `Pet` instance.
======
If an `@ModelAttribute` is declared without `BindingResult` parameter after it, then
`MethodArgumentNotValueException` is raised. However, if method validation applies because
other parameters have `@Constraint` annotations, then `HandlerMethodValidationException`
is raised instead. For more details, see the section on
If there is no `BindingResult` parameter after the `@ModelAttribute`, then
`MethodArgumentNotValueException` is raised with the validation errors. However, if method
validation applies because other parameters have `@jakarta.validation.Constraint` annotations,
then `HandlerMethodValidationException` is raised instead. For more details, see the section
xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation].
TIP: Using `@ModelAttribute` is optional. By default, any parameter that is not a simple
value type as determined by
{api-spring-framework}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]
_AND_ that is not resolved by any other argument resolver is treated as an `@ModelAttribute`.

View File

@@ -1,29 +1,16 @@
In the context of web applications, _data binding_ involves the binding of HTTP request
parameters (that is, form data or query parameters) to properties in a model object and
its nested objects.
xref:core/validation/beans-beans.adoc#beans-binding[Data binding] for web requests involves
binding request parameters to a model object. By default, request parameters can be bound
to any public property of the model object, which means malicious clients can provide
extra values for properties that exist in the model object graph, but are not expected to
be set. This is why model object design requires careful consideration.
Only `public` properties following the
https://www.oracle.com/java/technologies/javase/javabeans-spec.html[JavaBeans naming conventions]
are exposed for data binding — for example, `public String getFirstName()` and
`public void setFirstName(String)` methods for a `firstName` property.
TIP: The model object, and its nested object graph, is also sometimes referred to as a
TIP: The model object, and its nested object graph is also sometimes referred to as a
_command object_, _form-backing object_, or _POJO_ (Plain Old Java Object).
By default, Spring permits binding to all public properties in the model object graph.
This means you need to carefully consider what public properties the model has, since a
client could target any public property path, even some that are not expected to be
targeted for a given use case.
For example, given an HTTP form data endpoint, a malicious client could supply values for
properties that exist in the model object graph but are not part of the HTML form
presented in the browser. This could lead to data being set on the model object and any
of its nested objects, that is not expected to be updated.
The recommended approach is to use a _dedicated model object_ that exposes only
properties that are relevant for the form submission. For example, on a form for changing
a user's email address, the model object should declare a minimum set of properties such
as in the following `ChangeEmailForm`.
A good practice is to use a _dedicated model object_ rather than exposing your domain
model such as JPA or Hibernate entities for web data binding. For example, on a form to
change an email address, create a `ChangeEmailForm` model object that declares only
the properties required for the input:
[source,java,indent=0,subs="verbatim,quotes"]
----
@@ -51,13 +38,9 @@ as in the following `ChangeEmailForm`.
}
----
If you cannot or do not want to use a _dedicated model object_ for each data
binding use case, you **must** limit the properties that are allowed for data binding.
Ideally, you can achieve this by registering _allowed field patterns_ via the
`setAllowedFields()` method on `WebDataBinder`.
For example, to register allowed field patterns in your application, you can implement an
`@InitBinder` method in a `@Controller` or `@ControllerAdvice` component as shown below:
If a dedicated model object is not feasible, we strongy recommend registering
`allowedFields` patterns (case sensitive) on `WebDataBinder` in order to prevent other
properties from being set. For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@@ -74,22 +57,6 @@ For example, to register allowed field patterns in your application, you can imp
}
----
In addition to registering allowed patterns, it is also possible to register _disallowed
field patterns_ via the `setDisallowedFields()` method in `DataBinder` and its subclasses.
Please note, however, that an "allow list" is safer than a "deny list". Consequently,
`setAllowedFields()` should be favored over `setDisallowedFields()`.
Note that matching against allowed field patterns is case-sensitive; whereas, matching
against disallowed field patterns is case-insensitive. In addition, a field matching a
disallowed pattern will not be accepted even if it also happens to match a pattern in the
allowed list.
[WARNING]
====
It is extremely important to properly configure allowed and disallowed field patterns
when exposing your domain model directly for data binding purposes. Otherwise, it is a
big security risk.
Furthermore, it is strongly recommended that you do **not** use types from your domain
model such as JPA or Hibernate entities as the model object in data binding scenarios.
====
You can also register `disallowedFields` patterns (case insensitive). However,
"allowed" configuration is preferred over "disallowed" as it is more explicit and less
prone to mistakes.