Dependency not autowired in JSR-303 Custom Validator
Asked Answered
G

3

14

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
}
Gyrostabilizer answered 28/8, 2018 at 22:20 Comment(15)
Possible duplicate of Autowired Repository is Null in Custom Constraint ValidatorCertificate
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.Gyrostabilizer
can you post the code how you're using UserService ?Dufy
Posted - UserService is used in UserControllerGyrostabilizer
can you post your spring xml or Java config as well?Dufy
I don't see @Valid or @Validated annotation in your sample. Who and where is doing the validation?Jennee
Unless I misunderstood how this works, '@Validated' and '@Valid' would be used only if you were doing automatic validation - say, annotating the parameter in the method with '@Valid' so that the object gets validated. I am doing manual validation using validator - so to answer your question, I calling the validator on User object in the UserService. All validations work fine - except for the custom one.Gyrostabilizer
could you please check if UserRepository and Validator get injected in UserService? I guess your validator not get managed by spring , you better off put @Autowired and recheck above the constructor and recheck.Enrika
@MohammadRezaAlagheband they do - all dependencies get autowired except for UserRepository in the validator.Gyrostabilizer
Try removing @Component from UniqueUsernameValidator and recheck, it should workEnrika
As the pre-commenter pointed out: the validator does NOT need to be annotated as @Component. Instead, check if your UserRepository is a component and debug if the UserRepository can be instantiated properly, i.e. all its members can be instantiated etc.Vachil
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...Fanjet
most voted answer here, it should help someone to answer :) #11721226Underwing
@cldjr Is bean of type LocalValidatorFactoryBean present in your Spring Configuration ?Constellation
@cldjr I have posted my answer below. Is there anything i need to add to your question?Bradford
B
2

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
Bradford answered 6/9, 2018 at 5:35 Comment(0)
C
0

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

Caterwaul answered 7/9, 2018 at 9:3 Comment(0)
M
0

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;
    }
}
Manageable answered 7/9, 2018 at 9:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.