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
}
  • Possible duplicate of [Autowired Repository is Null in Custom Constraint Validator](https://stackoverflow.com/q/13599821/5221149) – Andreas Aug 28 '18 at 22:33
  • 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. –  Aug 28 '18 at 22:37
  • can you post the code how you're using UserService ? – Jayesh Aug 28 '18 at 22:39
  • Posted - UserService is used in UserController –  Aug 28 '18 at 22:56
  • 1
    can you post your spring xml or Java config as well? – Jayesh Aug 28 '18 at 23:26
  • I don't see `@Valid` or `@Validated` annotation in your sample. Who and where is doing the validation? – Pavel Horal Aug 28 '18 at 23:35
  • 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. –  Aug 28 '18 at 23:50
  • 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. – MohammadReza Alagheband Sep 01 '18 at 17:30
  • @MohammadRezaAlagheband they do - all dependencies get autowired except for UserRepository in the validator. –  Sep 02 '18 at 17:38
  • Try removing @Component from UniqueUsernameValidator and recheck, it should work – MohammadReza Alagheband Sep 02 '18 at 17:58
  • 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. – Pawel Os. Sep 04 '18 at 09:05
  • 1
    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 Sep 06 '18 at 15:29
  • most voted answer here, it should help someone to answer :) https://stackoverflow.com/questions/11720726/how-to-avoid-cross-dependency-between-layers-because-of-constraint-validatedby – pdem Sep 07 '18 at 09:10
  • @cldjr Is bean of type `LocalValidatorFactoryBean` present in your Spring Configuration ? – krisp Sep 07 '18 at 12:27
  • @cldjr I have posted my answer below. Is there anything i need to add to your question? – Prasad Sep 10 '18 at 05:30

3 Answers3

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
Prasad
  • 1,089
  • 13
  • 21
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

Arquillian
  • 125
  • 2
  • 16
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;
    }
}
Nikolas Charalambidis
  • 40,893
  • 16
  • 117
  • 183