There are pros and cons to treating validation as business logic, and Spring offers design rules for validation (and
data binding) that exclude neither. In particular, validation should not be tied to the web layer, should be easily
localizable, and should be able to connect to any available validator. Given these requirements, Spring provides a
Validator
contract, which is basic and can be used at every level of the application.
Data
binding is useful for ensuring that user input is dynamically bound to the application's domain model (or any
objects that are used to process user input). Spring provides a well-formulated DataBinder
to do
exactly this task. Validator
and DataBinder
make up the validation
package,
which is primarily used at the web tier, but is not limited to it.
BeanWrapper
is a
fundamental concept in the Spring Framework and is used in many cases. However, you probably won't need to use
BeanWrapper
directly. However, since this is reference documentation, we felt it necessary to provide
some clarification. We explain BeanWrapper
in this chapter because, if you are going to use it at all,
you will most likely do so when trying to bind data to objects.
Spring's lower-level DataBinder
and BeanWrapper
use the PropertyEditorSupport
implementation to parse and format property
values. The PropertyEditor
and PropertyEditorSupport
types are part of the JavaBeans class
specification and are also described in this chapter. Spring 3 introduced the core.convert
package,
which provides general type conversion facilities, as well as a high-level "format" package for formatting UI field
values. You can use these packages as simpler alternatives to implementing PropertyEditorSupport
. They
are also described in this chapter.
Spring supports Java Bean Validation through a custom framework and an
adapter to Spring's own Validator
contract. Applications can enable bean validation globally once and
use it solely for validation needs. At the web tier, applications can optionally register local instances of Validator
from Spring for DataBinder
, which can be useful for connecting custom validation logic.
Validation using Spring validator interface
Spring offers a Validator
interface that can be
used to validate objects. The Validator
interface operates using the Errors
object, so
during validation checks, validators can report validation errors to the Errors
object.
Consider the following example of a small data object:
public class Person {
private String name;
private int age;
// regular getters and setters...
}
class Person(val name: String, val age: Int)
The following example provides the validation logic for the Person
class by implementing the
following two methods of the org.springframework.validation.Validator
interface:
supports(Class)
: Whether thisValidator
can validate instances of the providedClass
?validate(Object, org.springframework.validation.Errors)
: Validates the given object and, if detects errors, logs them in the givenErrors
object.
The Validator
implementation is quite simple, especially if you know about the helper the ValidationUtils
class, which is also provided by the Spring Framework. The following example implements a Validator
for
Person
instances:
public class PersonValidator implements Validator {
/* *
* This validator only validates Person instances.
*/
public boolean supports(Class clazz) {
return Person.class.equals(clazz);
}
public void validate(Object obj, Errors e) {
ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
Person p = (Person) obj;
if (p.getAge() < 0) {
e.rejectValue("age", "negativevalue");
} else if (p.getAge() > 110) {
e.rejectValue("age", "too.darn.old");
}
}
}
class PersonValidator : Validator {
/**
* This validator only validates Person instances.
*/
override fun supports(clazz: Class<*>): Boolean {
return Person::class.java == clazz
}
override fun validate(obj: Any, e: Errors) {
ValidationUtils.rejectIfEmpty(e, "name" , "name.empty")
val p = obj as Person
if (p.age < 0) {
e.rejectValue("age", "negativevalue")
} else if (p.age > 110) {
e.rejectValue ("age", "too.darn.old")
}
}
}
static rejectIfEmpty(..)
method of the class ValidationUtils
is used to exclude the
name
property if it is null
or the empty string. View the javadoc at ValidationUtils
to see what functions it provides beyond those shown in the
example earlier.
Of course, you could implement a single Validator
class for validating each of
the nested objects in a fully functional object, but it may be better to encapsulate the validation logic for each
nested object class in its own Validator
implementation. A simple example of a "full-featured" object
would be a Customer
, which consists of two String
properties (the first and second name)
and a complex Address
object. Address
objects can be used independently of
Customer
objects, so a separate AddressValidator
has been implemented. If you want your
CustomerValidator
to reuse the logic contained in the AddressValidator
class without
resorting to copy and paste, you can inject a dependency into the AddressValidator
or create an
instance inside your CustomerValidator
as shown in the following example:
public class CustomerValidator implements Validator {
private final Validator addressValidator;
public CustomerValidator(Validator addressValidator) {
if (addressValidator == null) {
throw new IllegalArgumentException("The supplied [Validator] is " +
"required and must not be null.");
}
if (!addressValidator.supports(Address.class)) {
throw new IllegalArgumentException("The supplied [Validator] must " +
"support the validation of [Address] instances.");
}
this.addressValidator = addressValidator;
}
/**
* This validator validates instances of Customer as well as any subclasses of Customer.
*/
public boolean supports(Class clazz) {
return Customer.class.isAssignableFrom(clazz);
}
public void validate(Object target, Errors errors) {
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
Customer customer = (Customer) target;
try {
errors.pushNestedPath("address");
ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
} finally {
errors.popNestedPath();
}
}
}
class CustomerValidator(private val addressValidator: Validator) : Validator {
init {
if ( addressValidator == null) {
throw IllegalArgumentException("The supplied [Validator] is required and must not be null.")
}
if (!addressValidator.supports(Address::class.java)) {
throw IllegalArgumentException("The supplied [Validator ] must support the validation of [Address] instances.")
}
}
/*
* This validator validates Customer instances, as well as any Customer subclasses.
*/
override fun supports(clazz: Class<>): Boolean {
return Customer::class.java.isAssignableFrom(clazz)
}
override fun validate(target: Any, errors: Errors) {
ValidationUtils.rejectIfEmptyOrWhitespace( errors, "firstName", "field.required")
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required")
val customer = target as Customer
try {
errors.pushNestedPath("address")
ValidationUtils.invokeValidator(this. addressValidator, customer.address, errors)
} finally {
errors.popNestedPath()
}
}
}
Validation errors are reported to the Errors
object passed to the validator. In the case of
Spring Web MVC, you can use the <spring:bind/>
tag to check error messages, but you can also
check the Errors
object yourself. More information about the methods it offers can be found in javadoc.
Resolving code in error messages
We've covered database
binding and validation. This section discusses the output of messages corresponding to validation errors. In the
example shown in the previous section, we excluded the name
and age
fields. If we need to
display error messages using MessageSource
, then we can do this using the error code that we specify
when rejecting the field ("name" and "age" in this case). If you call (directly or indirectly, for example using the
ValidationUtils
class) rejectValue
or one of the other reject
methods from
the Errors
interface, then the base implementation will not only log the passed code, but will
also log a number of additional error codes. The MessageCodesResolver
defines what error codes the
Errors
interface logs. The default is DefaultMessageCodesResolver
, which (for example) not
only logs a message with the code you specify, but also logs messages that include the name of the field you passed
to the reject method. So, if a field is excluded using rejectValue("age", "too.darn.old")
, in addition
to the too.darn.old
code, Spring also registers too.darn.old.age
and too.darn.old.age.int
(the first includes the field name and the second the field type). This is for the convenience of developers when
working with error messages.
More information about MessageCodesResolver
and the default
strategy can be found in the javadoc at MessageCodesResolver
and DefaultMessageCodesResolver
, respectively.
GO TO FULL VERSION