14
votes

I have a Spring Boot app that contains an User class - all fields have standard JSR-303 annotations (@NotNull, @Size, etc.) and validation works fine.

However when I add a custom validation to User, I can't get a dependency injected into a custom validator:

@Component
public class UniqueUsernameValidator implements 
ConstraintValidator<UniqueUsername, String> {

@Autowired
private UserRepository userRepository;

@Override
public boolean isValid(String username, ConstraintValidatorContext context) {
    // implements logic
}

@UniqueUsername annotation is declared as:

@Documented
@Retention(RUNTIME)
@Target({FIELD, ANNOTATION_TYPE, PARAMETER})
@Constraint(validatedBy = UniqueUsernameValidator.class)
@interface UniqueUsername {
   String message() default "{com.domain.user.nonUniqueUsername}";
   Class<?>[] groups() default { };
   Class<? extends Payload>[] payload() default { };
}

The annotated field:

@NotBlank
@Size(min = 2, max = 30)
@UniqueUsername
private String username;

And the validator usage:

@Service
public final class UserService {

   private final UserRepository userRepository;
   private final Validator validator;

   public UserService(UserRepository userRepository, Validator validator) 
   {
       this.userRepository = userRepository;
       this.validator = validator;
   }

   public void createUser(User user) {
      Set<ConstraintViolation<User>> validate = validator.validate(user);
      // logic...
   }
}

The problem is that UserRepository is not being autowired in UniqueUsernameValidator. Field is always null.

I am using a LocalValidatorFactoryBean.

Does anyone have any idea why autowiring's not working?


@Controller
public class UserController {

private final UserService userService;

public UserController(UserService userService) {
    this.userService = userService;
}

@PostMapping("/user/new")
public String createUser(@ModelAttribute("newUser") User newUser, BindingResult bindingResult,
                         Model model) {
    userService.createUser(newUser);
    // omitted
}
3
It's not - the accepted solution posted by Hardy doesn't work for me. In a nutshell, he suggests to use the Spring's custom validator factory, which I am already doing. Besides, that's an old question - it might no longer be applicable. - user1348997
can you post the code how you're using UserService ? - Jayesh
can you post your spring xml or Java config as well? - Jayesh
cldjr, debugging injection is way easier if you do this one simple trick (doctors hate this!): use constructor injection. When it can't inject the parameter it will "fail fast" instead of just turning up null later. Also, @MohammadRezaAlagheband & others, why would he NOT need to register the validator as a bean somehow? assuming he is using component-scan... - Lucas Ross

3 Answers

2
votes

You need to add @Valid annotation in front of entity class in the public String createUser(@ModelAttribute("newUser") User newUser) in front of User.

@RequestBody @Valid User user
0
votes

The UserRepository implementation needs an Annotation like "@Repository" or "@Component" or "@Service". Your UserService gets the repository instance via constructor. Maybe there was a "new UserRepositoryDao()" call used. And in your validator you are trying to autowire. I guess it's either not annotated as service OR not loaded in the spring context path or as a spring bean in your spring.xml

0
votes

I have dealt with the same problem a few months ago. Instead of autowiring repository, pass the service which already uses the very same repository through the annotation.

Declare the annotation to accept the field required to be unique and a service performing the validation.

@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueUsernameValidator.class)
@Documented
public @interface UniqueUsername {

    String message() default "{com.domain.user.nonUniqueUsername}";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};

    Class<? extends UniqueUsernameValidation> service();      // Validating service
    String fieldName();                                       // Unique field
}

Use it on the POJO like:

@NotBlank
@Size(min = 2, max = 30)
@UniqueUsername(service = UserService.class, fieldName = "username")
private String username;

Notice the service passed into annotation (Class<? extends UniqueUsernameValidation> service()) must implement UniqueUsernameValidation interface.

public interface UniqueUsernameValidation {
    public boolean isUnique(Object value, String fieldName) throws Exception;
}

Now make the passed UserService implement the interface above and override it's only method:

@Override
public boolean isUnique(Object value, String fieldName) throws Exception {
    if (!fieldName.equals("username")) {
        throw new Exception("Field name not supported");
    }

    String username = value.toString();
    // Here is the logic: Use 'username' to find another username in Repository
}

Don't forget to UniqueUsernameValidator which processes the annotation:

public class UniqueUsernameValidator implements ConstraintValidator<UniqueUsername, Object>
{
    @Autowired
    private ApplicationContext applicationContext;

    private UniqueUsernameValidation service;

    private String fieldName;

    @Override
    public void initialize(UniqueUsername unique) {
        Class<? extends UniqueUsernameValidation> clazz = unique.service();
        this.fieldName = unique.fieldName();
        try {
            this.service = this.applicationContext.getBean(clazz);
        } catch(Exception ex) {
            // Cant't initialize service which extends UniqueUsernameValidator
        }
    }

    @Override
    public boolean isValid(Object o, ConstraintValidatorContext context) {
        if (this.service !=null) {
            // here you check whether the username is unique using UserRepository
            return this.service.isUnique(o, this.fieldName))                    
        }
        return false;
    }
}