【问题标题】:How to specify different locales for each bean validation call如何为每个 bean 验证调用指定不同的语言环境
【发布时间】:2021-12-30 17:29:27
【问题描述】:

我正在一个运行 Spring Batch 作业的平台上工作,这些作业负责从第三方应用程序检索一组对象,执行 bean 验证并将任何违反约束的情况返回给第三方应用程序供用户使用正确(没有违规的项目被转换并传递给另一个应用程序)。现在,我们使用 Spring Boot 配置的Validator,这一切在英文中都很好用。

我们正在扩展哪些用户可以访问第三方应用程序,现在需要以适合创建对象的用户的语言提供约束验证。我有一种方法可以查找特定对象所需的语言/区域设置,但我缺少的是如何告诉Validatorvalidate(<T> object) 方法返回的Set<ConstraintViolation<T>> 中消息的区域设置。此外,可能同时运行多个作业,每个作业都验证自己的对象类型,并需要以不同的语言报告违规行为。理想情况下,最好有一个validate(<T> object, Locale locale) 方法,但Validator 接口中不存在该方法。

我的第一个想法是编写一个自定义MessageInterpolator,并在每次验证之前设置适当的Locale(请参阅下面的ValueMessageInterpolatorDemoJobConfig)但是它不是线程安全的,所以我们可以结束用错误的语言处理消息。

我也考虑过是否有办法使用LocaleResolver 界面来代替,但我没有看到与MessageInterpolator 没有相同问题的解决方案。

根据我目前所确定的,似乎我唯一的解决方案是:

  1. 为每个需要一个的批处理作业/步骤实例化单独的Validators 和MessageInterpolators,并使用已经介绍的方法。由于在这些对象之间循环,这种方法似乎效率很低。
  2. 创建一个包含Validators 集合的服务bean,每个所需的区域设置一个。然后,每个批处理作业/步骤都可以引用这个新服务,并且该服务将负责委派给适当的Validator。可以像这样设置验证器,并将所需的验证器数量限制为我们支持的语言数量。
javax.validation.Validator caFRValidator = Validation.byProvider(HibernateValidator.class).configure().localeResolver(context -> {return Locale.CANADA_FRENCH;}).buildValidatorFactory().getValidator();
javax.validation.Validator usValidator = Validation.byProvider(HibernateValidator.class).configure().localeResolver(context -> {return Locale.US;}).buildValidatorFactory().getValidator();
javax.validation.Validator germanValidator = Validation.byProvider(HibernateValidator.class).configure().localeResolver(context -> {return Locale.GERMANY;}).buildValidatorFactory().getValidator();
  1. 不要直接调用Validator,而是创建一个只接受对象进行验证的微服务,然后通过Accept-Language 标头传入必要的Locale。虽然我可能只拥有一个 Validator bean 就可以逃脱惩罚,但这似乎不必要地复杂。

是否有替代方法可以用来解决这个问题?

我们目前正在使用 2.5.3 spring-boot-starter-parent pom 来管理依赖项,并且在我们需要实施这些更改时可能会更新到最新的 2.6.x 版本。

ValueMessageInterpolator.java

public class ValueMessageInterpolator implements MessageInterpolator {

    private final MessageInterpolator interpolator;
    private Locale currentLocale;
    
    public ValueMessageInterpolator(MessageInterpolator interp) {
        this.interpolator = interp;
        this.currentLocale = Locale.getDefault();
    }
    
    public void setLocale(Locale locale) {
        this.currentLocale = locale;
    }
    
    @Override
    public String interpolate(String messageTemplate, Context context) {
        return interpolator.interpolate(messageTemplate, context, currentLocale);
    }

    @Override
    public String interpolate(String messageTemplate, Context context, Locale locale) {
        return interpolator.interpolate(messageTemplate, context, locale);
    }

}

ToBeValidated.java

public class ToBeValidated {
    @NotBlank
    private final String value;

    private final Locale locale;
    
    // Other boilerplate code removed
}

DemoJobConfig.java

@Configuration
@EnableBatchProcessing
public class DemoJobConfig extends DefaultBatchConfigurer {

    @Bean
    public ValueMessageInterpolator buildInterpolator() {
        return new ValueMessageInterpolator(Validation.byDefaultProvider().configure().getDefaultMessageInterpolator());
    }

    @Bean
    public javax.validation.Validator buildValidator(ValueMessageInterpolator valueInterp) {
        return Validation.byDefaultProvider().configure().messageInterpolator(valueInterp).buildValidatorFactory().getValidator();
    }

    @Bean
    public Job configureJob(JobBuilderFactory jobFactory, Step demoStep) {
        return jobFactory.get("demoJob").start(demoStep).build();
    }

    @Bean
    public Step configureStep(StepBuilderFactory stepFactory, javax.validation.Validator constValidator, ValueMessageInterpolator interpolator) {

        ItemReader<ToBeValidated> reader = 
                new ListItemReader<ToBeValidated>(Arrays.asList(
                        new ToBeValidated("values1", Locale.US),            // (No errors)
                        new ToBeValidated("", Locale.US),                   // value: must not be blank
                        new ToBeValidated("", Locale.CANADA),               // value: must not be blank
                        new ToBeValidated("value3", Locale.CANADA_FRENCH),  // (No errors)  
                        new ToBeValidated("", Locale.FRANCE),               // value: ne doit pas être vide
                        new ToBeValidated("", Locale.GERMANY)               // value: kann nicht leer sein
                        ));

        Validator<ToBeValidated> springValidator = new Validator<ToBeValidated>() {
            @Override
            public void validate(ToBeValidated value) throws ValidationException {
                interpolator.setLocale(value.getLocale());
                String errors = constValidator.validate(value).stream().map(v -> v.getPropertyPath().toString() +": "+v.getMessage()).collect(Collectors.joining(","));
                if(errors != null && !errors.isEmpty()) {
                    throw new ValidationException(errors);
                }
            }
        };

        ItemProcessor<ToBeValidated, ToBeValidated> processor = new ValidatingItemProcessor<ToBeValidated>(springValidator);

        ItemWriter<ToBeValidated> writer =  new ItemWriter<ToBeValidated>() {
            @Override
            public void write(List<? extends ToBeValidated> items) throws Exception {
                items.forEach(System.out::println);
            }
        };
        
        SkipListener<ToBeValidated, ToBeValidated> skipListener = new SkipListener<ToBeValidated, ToBeValidated>() {
            @Override
            public void onSkipInRead(Throwable t) {}

            @Override
            public void onSkipInWrite(ToBeValidated item, Throwable t) {}

            @Override
            public void onSkipInProcess(ToBeValidated item, Throwable t) {
                System.out.println("Skipped ["+item.toString()+"] for reason(s) ["+t.getMessage()+"]");
            }
        };

        return stepFactory.get("demoStep")
                .<ToBeValidated, ToBeValidated>chunk(2)
                .reader(reader)
                .processor(processor)
                .writer(writer)
                .faultTolerant()
                .skip(ValidationException.class)
                .skipLimit(10)
                .listener(skipListener)
                .build();
    }

    @Override
    public PlatformTransactionManager getTransactionManager() {
        return new ResourcelessTransactionManager();
    }
}

【问题讨论】:

  • afaik Spring 附带的 messageinterpolator(使用 Spring MessageSource)完全符合您的要求。
  • @m.deinum,谢谢,我又看了LocaleContextMessageInterpolatorLocaleContextHolder.setLocale(),它解决了我的问题。

标签: spring spring-boot spring-batch hibernate-validator


【解决方案1】:

Spring Boot 中的 ValidationAutoConfiguration 创建了一个 LocalValidatorFactoryBean,其中在 afterPropertiesSet() 方法中配置了一个 LocaleContextMessageInterpolator

因此,支持此要求所需的唯一更改是在ItemProcessor 中的验证调用之前添加LocaleContextHolder.setLocale(Locale locale)LocalContextHolder 保留一个 ThreadLocal&lt;LocaleContext&gt;,它允许每个线程(作业/步骤)保留它自己的当前使用的 Locale 的版本。

        Validator<ToBeValidated> springValidator = new Validator<ToBeValidated>() {
            @Override
            public void validate(ToBeValidated value) throws ValidationException {
                LocaleContextHolder.setLocale(value.getLocale());
                String errors = constValidator.validate(value).stream().map(v -> v.getPropertyPath().toString() +": "+v.getMessage()).collect(Collectors.joining(","));
                if(errors != null && !errors.isEmpty()) {
                    throw new ValidationException(errors);
                }
            }
        };

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-26
    • 1970-01-01
    • 1970-01-01
    • 2012-05-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-09
    相关资源
    最近更新 更多