Внедрить репозиторий внутри ConstraintValidator с Spring 4 и конфигурацией интерполяции сообщений

Я создал небольшой примерный проект, чтобы показать две проблемы, с которыми я сталкиваюсь при настройке проверки Spring Boot и ее интеграции с Hibernate. Я уже пробовал другие ответы, которые я нашел по этой теме, но, к сожалению, они не сработали для меня или в которых предлагалось отключить проверку Hibernate.

Я хочу использовать специальный валидатор, реализующий ConstraintValidator<ValidUser, User>, и вставить в него свой UserRepository. В то же время я хочу сохранить поведение Hibernate по умолчанию, которое проверяет наличие ошибок проверки во время обновления / сохранения.

Пишу сюда для полноты картины основные разделы приложения.

Пользовательская конфигурация В этом классе я установил пользовательский валидатор с пользовательским MessageSource, поэтому Spring будет читать сообщения из файла 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;
    }

}

Компонент. Ничего особенного, если не пользовательский валидатор @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;
    }

}

Пользовательский валидатор Вот где я пытаюсь внедрить репозиторий. Репозиторий всегда имеет значение null, если не когда я отключаю проверку Hibernate.

    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)

В моих тестах вы можете попробовать из исходного кода, у меня две проблемы:

  1. Внедренный репозиторий внутри UserValidator имеет значение null, если я не отключу проверку Hibernate (spring.jpa.properties.javax.persistence.validation.mode = none)
  2. Даже если я отключу валидатор Hibernate, мои тестовые примеры не пройдут, потому что что-то мешает Spring использовать интерполяцию строк по умолчанию для сообщений проверки, которые должны быть чем-то вроде [Constraint]. [Имя класса в нижнем регистре]. [PropertyName]. Я не хочу использовать аннотацию ограничения с элементом значения, подобным этому @NotBlank(message="{mycustom.message}"), потому что я не вижу смысла, учитывая, что у него есть собственная конвенция для интерполяции, и я могу воспользоваться этим ... это означает меньше кодирования.

прилагаю код; вы можете просто запустить тесты Junit и увидеть ошибки (проверка Hibernate включена, проверьте application.properties).

Что я делаю неправильно? Что я мог сделать, чтобы решить эти две проблемы?

====== ОБНОВЛЕНИЕ ======

Чтобы уточнить, прочтите документацию по проверке Spring https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation-beanvalidation-spring-constraints они говорят:

По умолчанию LocalValidatorFactoryBean настраивает SpringConstraintValidatorFactory, который использует Spring для создания экземпляров ConstraintValidator. Это позволяет вашим настраиваемым ConstraintValidators извлекать выгоду из внедрения зависимостей, как и любой другой компонент Spring.

Как видите, реализация ConstraintValidator может иметь зависимости @Autowired, как и любой другой компонент Spring.

В моем классе конфигурации я создавал свои LocalValidatorFactoryBean по мере их написания.

Еще один интересный вопрос: this и это, но мне с ними не повезло.

====== ОБНОВЛЕНИЕ 2 ======

После долгих исследований кажется, что с валидатором Hibernate инъекция не предусмотрена.

Я нашел несколько способов сделать это:

1-й способ

Создайте этот класс конфигурации:

 @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);
    }
}

2-й способ

Создать служебный компонент

    @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);

    }

}

а затем в инициализации валидатора:

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

очень важно

В обоих случаях, чтобы заставить его работать, вам нужно «перезагрузить» диспетчер сущностей следующим образом:

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

В любом случае, я не знаю, действительно ли это безопасный способ. Скорее всего, доступ к уровню сохраняемости вообще не является хорошей практикой.


person drenda    schedule 05.10.2017    source источник
comment
: (Я столкнулся с этим точным сценарием, когда мне нужно выполнить операцию SQL внутри ConstraintValidator с использованием CrudRepository. Удалось ли вам решить эту @drenda?   -  person Gideon    schedule 05.08.2020


Ответы (1)


Если вам действительно нужно использовать инъекцию в вашем валидаторе, попробуйте добавить к нему аннотацию @Configurable:

@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;
    }

}

Из документации к этой аннотации:

Помечает класс как имеющий право на конфигурацию, управляемую Spring

Итак, это должно решить вашу нулевую проблему. Однако, чтобы он работал, вам нужно будет настроить AspectJ ... (проверьте, как использовать @Configurable в Spring для этого)

person mark_o    schedule 06.10.2017
comment
Спасибо за ответ. Я обновил свой вопрос, указав, что внедрение зависимостей должно работать без каких-либо особых уловок, основанных на том, что они говорят. Однако я пытаюсь следовать вашему пути, используя EnableSpringConfigured и EnableLoadTimeWeaving, но внедренный репозиторий всегда имеет значение null. - person drenda; 06.10.2017