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:

Java
public class Person {
    private String name;
    private int age;
    // regular getters and setters...
}
Kotlin
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.Validatorinterface:

  • supports(Class): Whether this Validator can validate instances of the provided Class?

  • validate(Object, org.springframework.validation.Errors): Validates the given object and, if detects errors, logs them in the given Errorsobject.

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:

Java

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");
        }
    }
}
Kotlin

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:

Java

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();
        }
    }
}
Kotlin

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.