如何为每个 bean 验证调用指定不同的语言环境
How to specify different locales for each bean validation call
我正在一个平台上工作,该平台运行 Spring 批处理作业,负责从第三方应用程序检索一组 object,执行 bean 验证和 returns任何违反约束的行为都会反馈给第三方应用程序供用户纠正(没有违反约束的项目会被转换并传递给另一个应用程序)。现在,我们使用由 Spring Boot 配置的 Validator
,这在英语中运行良好。
我们正在扩展哪些用户可以访问第三方应用程序,现在需要以适合创建 object 的用户的语言提供约束验证。我有办法查找特定 object 所需的 language/locale,但我缺少的是如何告诉 Validator
Set<ConstraintViolation<T>>
中消息的语言环境由 validate(<T> object)
方法返回。此外,可能同时有多个作业 运行,每个作业都验证自己的类型 object 并需要以不同的语言报告违规行为。理想情况下,有一个 validate(<T> object, Locale locale)
方法会很好,但 Validator
接口中不存在该方法。
我的第一个想法是编写自定义 MessageInterpolator
,并在每次验证之前设置适当的 Locale
(请参阅下面的 ValueMessageInterpolator
和 DemoJobConfig
),但事实并非如此thread-safe,所以我们最终可能会收到错误语言的消息。
我还考虑过是否可以使用 LocaleResolver
界面来提供帮助,但我没有看到不会出现与 MessageInterpolator
相同问题的解决方案。
根据我目前所确定的,似乎我唯一的解决方案是:
- 为需要一个的每个批次 job/step 实例化单独的
Validator
s 和 MessageInterpolator
s 并使用已经介绍的方法。由于循环通过这些 objects,这种方法似乎相当低效。
- 创建一个包含 collection 个
Validator
的服务 bean,一个用于每个需要的 Locale。然后每个批次 job/step 可以引用这个新服务,并且该服务将负责委托给适当的 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();
- 与其直接调用
Validator
,不如创建一个只接受 object 进行验证的微服务,然后通过 Accept-Language header。虽然我可能只拥有一个 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();
}
}
Spring Boot 中的 ValidationAutoConfiguration
创建了一个 LocalValidatorFactoryBean
,其中,在 afterPropertiesSet()
方法中配置了一个 LocaleContextMessageInterpolator
。
因此,支持此要求所需的唯一更改是 LocaleContextHolder.setLocale(Locale locale)
在 ItemProcessor
中的验证调用之前添加。 LocalContextHolder
保留一个 ThreadLocal<LocaleContext>
,它允许每个线程 (job/step) 保留它自己的当前正在使用的 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);
}
}
};
我正在一个平台上工作,该平台运行 Spring 批处理作业,负责从第三方应用程序检索一组 object,执行 bean 验证和 returns任何违反约束的行为都会反馈给第三方应用程序供用户纠正(没有违反约束的项目会被转换并传递给另一个应用程序)。现在,我们使用由 Spring Boot 配置的 Validator
,这在英语中运行良好。
我们正在扩展哪些用户可以访问第三方应用程序,现在需要以适合创建 object 的用户的语言提供约束验证。我有办法查找特定 object 所需的 language/locale,但我缺少的是如何告诉 Validator
Set<ConstraintViolation<T>>
中消息的语言环境由 validate(<T> object)
方法返回。此外,可能同时有多个作业 运行,每个作业都验证自己的类型 object 并需要以不同的语言报告违规行为。理想情况下,有一个 validate(<T> object, Locale locale)
方法会很好,但 Validator
接口中不存在该方法。
我的第一个想法是编写自定义 MessageInterpolator
,并在每次验证之前设置适当的 Locale
(请参阅下面的 ValueMessageInterpolator
和 DemoJobConfig
),但事实并非如此thread-safe,所以我们最终可能会收到错误语言的消息。
我还考虑过是否可以使用 LocaleResolver
界面来提供帮助,但我没有看到不会出现与 MessageInterpolator
相同问题的解决方案。
根据我目前所确定的,似乎我唯一的解决方案是:
- 为需要一个的每个批次 job/step 实例化单独的
Validator
s 和MessageInterpolator
s 并使用已经介绍的方法。由于循环通过这些 objects,这种方法似乎相当低效。 - 创建一个包含 collection 个
Validator
的服务 bean,一个用于每个需要的 Locale。然后每个批次 job/step 可以引用这个新服务,并且该服务将负责委托给适当的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();
- 与其直接调用
Validator
,不如创建一个只接受 object 进行验证的微服务,然后通过 Accept-Language header。虽然我可能只拥有一个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();
}
}
Spring Boot 中的 ValidationAutoConfiguration
创建了一个 LocalValidatorFactoryBean
,其中,在 afterPropertiesSet()
方法中配置了一个 LocaleContextMessageInterpolator
。
因此,支持此要求所需的唯一更改是 LocaleContextHolder.setLocale(Locale locale)
在 ItemProcessor
中的验证调用之前添加。 LocalContextHolder
保留一个 ThreadLocal<LocaleContext>
,它允许每个线程 (job/step) 保留它自己的当前正在使用的 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);
}
}
};