Search code examples
javaspringspring-bootvalidation

Spring Boot Custom Validator with Persistence Layer


I'm working on modernizing an application to Spring Boot. Previously, we had used Struts2 as our main application framework, which handled form validation. We were able to easily invoke service layer validation methods, which had full access to the persistence layer. I'm looking into Spring Boot MVC validation. I like a lot of the annotation Bean validation, but I'm having a lot of trouble when it comes to custom validation.

In my application, we want to enforce unique names on our entities. To do this, we previously went to the database and tested if a name being provided by a user was already in use. I implemented logic for this in our Repository class.

public interface ContractorRepository extends CrudRepository<Contractor, Integer>, QuerydslPredicateExecutor<Contractor>, ListQuerydslPredicateExecutor<Contractor>, JpaRepository<Contractor, Integer> {
    int countByIdNotAndNameEqualsIgnoreCase(int id, String name);
}

Our logic is just checking for count to be 0. If the result is greater than 0, then a validation error is thrown. To facilitate this, I've tried to create a custom validator in accordance with documentation that I've been reading.

Here's the validator interface:

@Constraint(validatedBy = ContractorUniqueNameValidator.class)
@Target( { ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface ContractorUniqueName {
    String message() default "The name must be unique. Another Contractor already has this name.";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

And here's the implementation:

@Component
@NoArgsConstructor
public class ContractorUniqueNameValidator implements ConstraintValidator<ContractorUniqueName, Contractor> {

    @Autowired
    private ContractorRepository contractorRepository;

    @Override
    public void initialize(ContractorUniqueName validator) {
    }

    @Override
    public boolean isValid(Contractor validateObject, ConstraintValidatorContext cxt) {
        return contractorRepository.countByIdNotAndNameEqualsIgnoreCase(
            validateObject.getId(),
            validateObject.getName()
        ) <= 0;
    }
}

And here's the relevant parts of my bean. The custom validator is on the class level, since I need access to multiple fields:

@Entity
@Table(name="CONTRACTORS")
@Data
@NoArgsConstructor
/*
 * Validation annotations.
 */
@ContractorUniqueName
public class Contractor implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /**
     * The primary id of the contractor entity.
     */
    @GeneratedValue(strategy= GenerationType.SEQUENCE, generator = "CONTRACTOR_SQ")
    @SequenceGenerator(name="CONTRACTOR_SQ", sequenceName = "CONTRACTOR_SQ", allocationSize = 1)
    @Column(name = "ID", nullable = false, insertable = true, updatable = true, precision = 20, scale = 0)
    @Id
    private int id;

    /**
     * The name of the contractor
     */
    @Column(name = "NAME", nullable = false, insertable = true, updatable = true, length = 250)
    @Basic
    @NotBlank
    private String name;
    
    /**
     * Skipping all other fields - not relevant to question
     */
     ...
}

Finally, here's my action class:

@Controller
@Getter@Setter
public class ContractorAction {
    @Autowired
    private ContractorManager contractorManager;

    @GetMapping("/contractorEdit")
    public String edit() {
        return "administration/contractorEdit";
    }

    @PostMapping("/contractorSave")
    public String save(@Valid @ModelAttribute Contractor contractor, BindingResult validationResult) {
        if (validationResult.hasErrors()) {
            return "administration/contractorEdit";
        }

        contractorManager.saveContractor(contractor);
        return "redirect:/";
    }
}

The main problem I'm having is that I'm unable to get the ContractorRepository autowired into my validator. My validator is being called, but I consistently get a NullPointerException when the "contractorRepository" is accessed inside the "isValid" method. I've tried several different approaches, but nothing I've done so far has given me access to the ContractorRepository.

How can I implement custom validation with access to the persistence layer with Spring Boot? Everything I've been finding so far demonstrates the custom annotations as I currently have, and I have seen a couple of examples involving autowiring, which leads me to believe this should be possible. I don't necessarily need to utilize annotations. I'm fine with doing a different approach, I just haven't yet seen any alternatives. I just need to be able to validate with use of the persistence layer and have the validation errors included with the BindingResult.


Solution

  • If you read the spring documentation it says:

    By default, the LocalValidatorFactoryBean configures a SpringConstraintValidatorFactory that uses Spring to create ConstraintValidator instances. This lets your custom ConstraintValidators benefit from dependency injection like any other Spring bean.

    Most probably you have also hibernate for validation in classpath and LocalValidatorFactoryBean is never registered from spring because a Hibernate Validator is detected. As described in documentation:

    The basic configuration in the preceding example triggers bean validation to initialize by using its default bootstrap mechanism. A Bean Validation provider, such as the Hibernate Validator, is expected to be present in the classpath and is automatically detected.

    So you need to manually register the spring Validator using the bellow:

    @Configuration
    public class AppConfig {
    
        @Bean
        public LocalValidatorFactoryBean validator() {
            return new LocalValidatorFactoryBean();
        }
    }
    

    for spring to be able to use Dependency Injection inside the Validator.