preface

First of all, happy New Year! Good luck in the Year of ox!!

Let’s start our New Year’s study now. Today, we’ll talk about common implementations for validating data in a Springboot application.

Hibernate validator

The general implementation is to use the Bean validation API for validation. The reference implementation of the Bean validation API is the Hibernate validator.

All required dependencies are packaged in the SpringBootstarter POM SpringBootstarter validation. Therefore, usually you only need to start the following dependencies:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Copy the code

Validation constraints are defined by annotating the fields with the appropriate Bean validation annotations. Such as:

public class Address {
 
    @NotBlank
    @Size(max = 50)
    private String street;
 
    @NotBlank
    @Size(max = 50)
    private String city;
 
    @NotBlank
    @Size(max = 10)
    private String zipCode;
     
    @NotBlank
    @Size(max = 3)
    private String countryCOde;
 
    // getters + setters
}
Copy the code

The usefulness of these annotations is obvious. We will use the Address class in many of the examples that follow.

The full list of built-in constraint annotations can be found in the Bean validation document.

If necessary, define your own validation constraints by creating a custom constraint validator.

Request data validation

Most of the time, when building the RestAPI interface with Springboot, you need to validate incoming request data. This can be done by simply adding the @Valid annotation to the @RequestBody method parameter.

Such as:

@RestController
public class AddressController {
 
    @PostMapping("/address")
    public void createAddress(@Valid @RequestBody Address address) {
        // ..}}Copy the code

With the @VALID annotation, data constraint validation is enabled.

Spring now automatically validates the passed Address object against the previously defined constraints.

This type of validation is usually used to ensure that the data sent by the client is syntactically correct. If validation fails, the controller methods are not called and an HTTP 400 (error request) response is returned to the client. More complex business-specific validation constraints should usually be checked later in the business layer.

Persistence layer data validation

When using a relational database in a Springboot application, the Hibernate framework is used for the persistence layer and validation is also supported. Hibernate supports Bean validation. If the entity contains Bean validation annotations, these annotations are automatically checked when the entity is persisted.

Note that the persistence layer should never be the only place for validation. If validation fails here, it usually means that some kind of validation is missing in other application components. Persistence layer validation should be considered the last line of defense. In addition, the persistence layer is often too late for business-relevant validation.

Method parameter validation

Spring provides validation of data constraints for method parameters. By adding Bean validation annotations to method parameters. Spring then uses an AOP interceptor to validate the parameters before calling the actual method.

Such as:

@Service
@Validated
public class CustomerService {
 
    public void updateAddress(
            @Pattern(regexp = "\\w{2}\\d{8}") String customerId,
            @Valid Address newAddress
    ) {
        // ..}}Copy the code

In addition, this approach is useful for validating data entering the service layer. Before using this approach, however, you should be aware of its limitations, as this type of validation is only valid when the Spring proxy is involved.

Also, note that this approach makes unit testing more difficult.

Trigger Bean validation programmatically

In the above validation scheme, the actual validation is triggered by Spring or Hibernate. Many times, we need the flexibility to trigger Bean validation, depending on the timing.

Next, let’s try to programmatically trigger validation for beans.

Let’s first create a Facade bean:

@Component
public class ValidationFacade {
 
    private final Validator validator;
 
    public ValidationFacade(Validator validator) {
        this.validator = validator;
    }
 
    public <T> void validate(T object, Class
       ... groups) {
        Set<ConstraintViolation<T>> violations = validator.validate(object, groups);
        if(! violations.isEmpty()) {throw newConstraintViolationException(violations); }}}Copy the code

This bean accepts a validator as the constructor with a parameter. The validator is part of the Bean validation API and is responsible for validating Java objects.

In validate (..) Method, we use a validator to validate the passed object. The result is a set of constraint conflicts. If no validation constraint is violated (= object valid), then the collection is empty. Otherwise, we throw a constraint conflict exception.

We can now inject our validation facade into other beans. Such as:

@Service
public class CustomerService {
 
    private final ValidationFacade validationFacade;
 
    public CustomerService(ValidationFacade validationFacade) {
        this.validationFacade = validationFacade;
    }
 
    public void updateAddress(String customerId, Address newAddress) {
        validationFacade.validate(newAddress);
        // ...}}Copy the code

To validate an object Address (in this case, the new Address), we simply call validate (..) Methods. Of course, we can also inject validators directly into our customer service. However, in the case of validation errors, we generally do not want to deal with the returned set of constraint conflicts. Instead, we might just want to throw an exception, which is what the validation facade does.

In general, this is a good implementation for validation at the service/business layer. It is not limited to method parameters and can be used for different types of objects. For example, we can load an object from a database, modify it, and then validate it before proceeding.

This approach is also good for unit testing because we can simply simulate the validation facade. If we want to do real validation in our unit tests, we can manually create the required validator instances.

Verify the internal business class

We can do dynamic validation of the actual business Bean object at creation time.

Dynamic validation is important when driving design and development in THE DDD field. For example, when creating an address instance, the constructor can ensure that we cannot construct an invalid object:

public class Address {
 
    @NotBlank
    @Size(max = 50)
    private String street;
 
    @NotBlank
    @Size(max = 50)
    privateString city; .public Address(String street, String city) {
        this.street = street;
        this.city = city;
        ValidationHelper.validate(this); }}Copy the code

Here, the constructor calls static validate (..) Method to verify object state. This static validate (..) The method is similar to the one shown earlier in the ValidationFacade:

The difference here is that we did not retrieve the validator instance through Spring. Instead, we created it manually using the following method:

Validation.buildDefaultValidatorFactory().getValidator()
Copy the code

In this way, we can integrate validation directly into domain objects without having to rely on outsiders to validate the objects.

conclusion

Today we talked about different ways to handle validation in a Springboot application.

Different use, according to the specific business needs, we actually do their own development strategy.

All right, that’s it for today. In the New Year, we must study hard!