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