Inject Repository inside ConstraintValidator with Spring 4 and message interpolation configuration

我的梦境 提交于 2019-12-23 07:38:11

问题


I created a small example project to show two problems I'm experiencing in the configuration of Spring Boot validation and its integration with Hibernate. I already tried other replies I found about the topic but unfortunately they didn't work for me or that asked to disable Hibernate validation.

I want use a custom Validator implementing ConstraintValidator<ValidUser, User> and inject in it my UserRepository. At the same time I want to keep the default behaviour of Hibernate that checks for validation errors during update/persist.

I write here for completeness main sections of the app.

Custom configuration In this class I set a custom validator with a custom MessageSource, so Spring will read messages from the file resources/messages.properties

@Configuration
public class CustomConfiguration {

    @Bean
    public MessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setBasenames("classpath:/messages");
        messageSource.setUseCodeAsDefaultMessage(false);
        messageSource.setCacheSeconds((int) TimeUnit.HOURS.toSeconds(1));
        messageSource.setFallbackToSystemLocale(false);
        return messageSource;
    }

    @Bean
    public LocalValidatorFactoryBean validator() {
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        factoryBean.setValidationMessageSource(messageSource());
        return factoryBean;
    }

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
        methodValidationPostProcessor.setValidator(validator());
        return methodValidationPostProcessor;
    }

}

The bean Nothing special here if not the custom validator @ValidUser

@ValidUser
@Entity
public class User extends AbstractPersistable<Long> {
    private static final long serialVersionUID = 1119004705847418599L;

    @NotBlank
    @Column(nullable = false)
    private String name;

    /** CONTACT INFORMATION **/

    @Pattern(regexp = "^\\+{1}[1-9]\\d{1,14}$")
    private String landlinePhone;

    @Pattern(regexp = "^\\+{1}[1-9]\\d{1,14}$")
    private String mobilePhone;

    @NotBlank
    @Column(nullable = false, unique = true)
    private String username;

    @Email
    private String email;

    @JsonIgnore
    private String password;

    @Min(value = 0)
    private BigDecimal cashFund = BigDecimal.ZERO;

    public User() {

    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getLandlinePhone() {
        return landlinePhone;
    }

    public void setLandlinePhone(String landlinePhone) {
        this.landlinePhone = landlinePhone;
    }

    public String getMobilePhone() {
        return mobilePhone;
    }

    public void setMobilePhone(String mobilePhone) {
        this.mobilePhone = mobilePhone;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public BigDecimal getCashFund() {
        return cashFund;
    }

    public void setCashFund(BigDecimal cashFund) {
        this.cashFund = cashFund;
    }

}

Custom validator Here is where I try to inject the repository. The repository is always null if not when I disable Hibernate validation.

    public class UserValidator implements ConstraintValidator<ValidUser, User> {
    private Logger log = LogManager.getLogger();

    @Autowired
    private UserRepository userRepository;

    @Override
    public void initialize(ValidUser constraintAnnotation) {
    }

    @Override
    public boolean isValid(User value, ConstraintValidatorContext context) {
        try {
            User foundUser = userRepository.findByUsername(value.getUsername());

            if (foundUser != null && foundUser.getId() != value.getId()) {
                context.disableDefaultConstraintViolation();
                context.buildConstraintViolationWithTemplate("{ValidUser.unique.username}").addConstraintViolation();

                return false;
            }
        } catch (Exception e) {
            log.error("", e);
            return false;
        }
        return true;
    }

}

messages.properties

#CUSTOM VALIDATORS
ValidUser.message = I dati inseriti non sono validi. Verificare nuovamente e ripetere l'operazione.
ValidUser.unique.username = L'username [${validatedValue.getUsername()}] è già stato utilizzato. Sceglierne un altro e ripetere l'operazione.

#DEFAULT VALIDATORS
org.hibernate.validator.constraints.NotBlank.message = Il campo non può essere vuoto

# === USER ===
Pattern.user.landlinePhone = Il numero di telefono non è valido. Dovrebbe essere nel formato E.123 internazionale (https://en.wikipedia.org/wiki/E.123)

In my tests, you can try from the source code, I've two problems:

  1. The injected repository inside UserValidator is null if I don't disable Hibernate validation (spring.jpa.properties.javax.persistence.validation.mode=none)
  2. Even if I disable Hibernate validator, my test cases fail because something prevent Spring to use the default string interpolation for validation messages that should be something like [Constraint].[class name lowercase].[propertyName]. I don't want to use the constraint annotation with the value element like this @NotBlank(message="{mycustom.message}") because I don't see the point considering that has his own convetion for interpolation and I can take advantage of that...that means less coding.

I attach the code; you can just run Junit tests and see errors (Hibernate validation is enable, check application.properties).

What am I doing wrong? What could I do to solve those two problems?

====== UPDATE ======

Just to clarify, reading Spring validation documentation https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation-beanvalidation-spring-constraints they say:

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

As you can see, a ConstraintValidator implementation may have its dependencies @Autowired like any other Spring bean.

In my configuration class I created my LocalValidatorFactoryBean as they write.

Another interesting questions are this and this, but I had not luck with them.

====== UPDATE 2 ======

After a lot of reseach, seems with Hibernate validator the injection is not provided.

I found a couple of way you can do that:

1st way

Create this configuration class:

 @Configuration
public class HibernateValidationConfiguration extends HibernateJpaAutoConfiguration {

    public HibernateValidationConfiguration(DataSource dataSource, JpaProperties jpaProperties,
            ObjectProvider<JtaTransactionManager> jtaTransactionManager,
            ObjectProvider<TransactionManagerCustomizers> transactionManagerCustomizers) {
        super(dataSource, jpaProperties, jtaTransactionManager, transactionManagerCustomizers);
    }

    @Autowired
    private Validator validator;

    @Override
    protected void customizeVendorProperties(Map<String, Object> vendorProperties) {
        super.customizeVendorProperties(vendorProperties);
        vendorProperties.put("javax.persistence.validation.factory", validator);
    }
}

2nd way

Create an utility bean

    @Service
public class BeanUtil implements ApplicationContextAware {

    private static ApplicationContext context;

    @Override

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {

        context = applicationContext;

    }

    public static <T> T getBean(Class<T> beanClass) {

        return context.getBean(beanClass);

    }

}

and then in the validator initialization:

@Override
 public void initialize(ValidUser constraintAnnotation) {
 userRepository = BeanUtil.getBean(UserRepository.class);
 em = BeanUtil.getBean(EntityManager.class);
 }

very important

In both cases, in order to make the it works you have to "reset" the entity manager in this way:

@Override
public boolean isValid(User value, ConstraintValidatorContext context) {
    try {
        em.setFlushMode(FlushModeType.COMMIT);
        //your code
    } finally {
        em.setFlushMode(FlushModeType.AUTO);
    }
}

Anyway, I don't know if this is really a safe way. Probably it's not a good practice access to the persistence layer at all.


回答1:


If you really need to use injection in your Validator try adding @Configurable annotation on it:

@Configurable(autowire = Autowire.BY_TYPE, dependencyCheck = true)
public class UserValidator implements ConstraintValidator<ValidUser, User> {
    private Logger log = LogManager.getLogger();

    @Autowired
    private UserRepository userRepository;

    // this initialize method wouldn't be needed if you use HV 6.0 as it has a default implementation now
    @Override
    public void initialize(ValidUser constraintAnnotation) {
    }

    @Override
    public boolean isValid(User value, ConstraintValidatorContext context) {
        try {
            User foundUser = userRepository.findByUsername( value.getUsername() );

            if ( foundUser != null && foundUser.getId() != value.getId() ) {
                context.disableDefaultConstraintViolation();
                context.buildConstraintViolationWithTemplate( "{ValidUser.unique.username}" ).addConstraintViolation();

                return false;
            }
        } catch (Exception e) {
            log.error( "", e );
            return false;
        }
        return true;
    }

}

From the documentation to that annotation:

Marks a class as being eligible for Spring-driven configuration

So this should solve your null problem. To make it work though, you would need to configure AspectJ... (Check how to use @Configurable in Spring for that)



来源:https://stackoverflow.com/questions/46594706/inject-repository-inside-constraintvalidator-with-spring-4-and-message-interpola

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!