Java 使用 JSR-303 和 Spring 的验证器的组合为 Spring Boot 端点实现自定义验证逻辑
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/39001106/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Implementing custom validation logic for a spring boot endpoint using a combination of JSR-303 and Spring's Validator
提问by pavel
I'm trying to implement some custom validation logic for a spring boot endpoint using a combination of JSR-303 Bean Validation API
and Spring's Validator
.
我试图实现使用的组合弹簧引导终点一些自定义的验证逻辑JSR-303 Bean Validation API
和Spring's Validator
。
Based on the Validator class diagram it appears to be possible to extend one of CustomValidatorBean
, SpringValidatorAdapter
or LocalValidatorFactoryBean
to add some custom validation logic into an overridden method validate(Object target, Errors errors)
.
根据 Validator 类图,似乎可以扩展其中之一CustomValidatorBean
,SpringValidatorAdapter
或者LocalValidatorFactoryBean
将一些自定义验证逻辑添加到重写的方法中validate(Object target, Errors errors)
。
However, if I create a validator extending any of these three classes and register it using @InitBinder
its validate(Object target, Errors errors)
method is never invoked and no validation is performed. If I remove @InitBinder
then a default spring validator performs the JSR-303 Bean Validation
.
但是,如果我创建一个扩展这三个类中的任何一个的验证器并使用@InitBinder
其validate(Object target, Errors errors)
方法注册它,则永远不会调用并且不执行任何验证。如果我删除,@InitBinder
那么默认的 spring 验证器会执行JSR-303 Bean Validation
.
Rest controller:
休息控制器:
@RestController
public class PersonEndpoint {
@InitBinder("person")
protected void initBinder(WebDataBinder binder) {
binder.setValidator(new PersonValidator());
}
@RequestMapping(path = "/person", method = RequestMethod.PUT)
public ResponseEntity<Person> add(@Valid @RequestBody Person person) {
person = personService.save(person);
return ResponseEntity.ok().body(person);
}
}
Custom validator:
自定义验证器:
public class PersonValidator extends CustomValidatorBean {
@Override
public boolean supports(Class<?> clazz) {
return Person.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
super.validate(target, errors);
System.out.println("PersonValidator.validate() target="+ target +" errors="+ errors);
}
}
If my validator implements org.springframework.validation.Validator
then its validate(Object target, Errors errors)
method is called but JSR-303 Bean Validation
is not performed prior to it. I can implement my custom JSR-303 validation similar to the way SpringValidatorAdapter
implements its JSR-303 Bean Validation
but there has to be a way to extend it instead:
如果我的验证器实现了org.springframework.validation.Validator
它的validate(Object target, Errors errors)
方法,那么它的 方法会被调用,但JSR-303 Bean Validation
不会在它之前执行。我可以实现我的自定义 JSR-303 验证,类似于SpringValidatorAdapter
实现它的方式,JSR-303 Bean Validation
但必须有一种方法来扩展它:
@Override
public void validate(Object target, Errors errors) {
if (this.targetValidator != null) {
processConstraintViolations(this.targetValidator.validate(target), errors);
}
}
I have looked at using custom JSR-303 constraints to avoid using org.springframework.validation.Validator
all together but there must be a way to make a custom validator work.
我已经研究过使用自定义 JSR-303 约束来避免同时使用org.springframework.validation.Validator
所有约束,但必须有一种方法可以使自定义验证器工作。
Spring validation documentationis not super clear on combining the two:
Spring验证文档对两者的结合并不是很清楚:
An application can also register additional Spring Validator instances per DataBinder instance, as described in Section 9.8.3, “Configuring a DataBinder”. This may be useful for plugging in validation logic without the use of annotations.
应用程序还可以为每个 DataBinder 实例注册额外的 Spring Validator 实例,如第 9.8.3 节“配置 DataBinder”中所述。这对于在不使用注释的情况下插入验证逻辑可能很有用。
And then later on it touches on configuring multiple Validator instances
然后后面会涉及到配置多个 Validator 实例
A DataBinder can also be configured with multiple Validator instances via dataBinder.addValidators and dataBinder.replaceValidators. This is useful when combining globally configured Bean Validation with a Spring Validator configured locally on a DataBinder instance. See ???.
一个 DataBinder 也可以通过 dataBinder.addValidators 和 dataBinder.replaceValidators 配置多个 Validator 实例。这在将全局配置的 Bean 验证与在 DataBinder 实例上本地配置的 Spring 验证器结合使用时非常有用。看 ???。
I'm using spring boot 1.4.0.
我正在使用弹簧靴 1.4.0。
采纳答案by pavel
Per @M.Deinum - using addValidators() instead of setValidator() did the trick. I also agree that using JSR-303, @AssertTrue method-based annotation specifically for cross fields validation, is probably a cleaner solution. A code example is available at https://github.com/pavelfomin/spring-boot-rest-example/tree/feature/custom-validator. In the example, the middle name validation is performed via custom spring validator while last name validation is handled by the default jsr 303 validator.
每@M.Deinum - 使用 addValidators() 而不是 setValidator() 就可以了。我也同意使用 JSR-303,@AssertTrue 基于方法的注释专门用于跨字段验证,可能是一个更清晰的解决方案。代码示例可在https://github.com/pavelfomin/spring-boot-rest-example/tree/feature/custom-validator 获得。在示例中,中间名验证是通过自定义 spring 验证器执行的,而姓氏验证由默认的 jsr 303 验证器处理。
回答by Marco Blos
This problem can be solved extending the LocalValidatorFactoryBean, you can override the validate
method inside this class giving any behavior that you want.
这个问题可以通过扩展 LocalValidatorFactoryBean 来解决,你可以覆盖validate
这个类中的方法,给出你想要的任何行为。
In my case I need to use JSR-303 AND custom validators for same model in different methods in same Controller, normally is recommended to use @InitBinder, but it is not sufficient for my case because InitBinder make a bind between Model and Validator (if you use @RequestBody InitBinder is just for one model and one validator per Controller).
在我的情况下,我需要在同一个控制器的不同方法中为同一个模型使用 JSR-303 和自定义验证器,通常建议使用@InitBinder,但这对我来说还不够,因为 InitBinder 在模型和验证器之间进行绑定(如果您使用 @RequestBody InitBinder 仅适用于一个模型和每个控制器一个验证器)。
Controller
控制器
@RestController
public class LoginController {
@PostMapping("/test")
public Test test(@Validated(TestValidator.class) @RequestBody Test test) {
return test;
}
@PostMapping("/test2")
public Test test2(@Validated @RequestBody Test test) {
return test;
}
}
Custom Validator
自定义验证器
public class TestValidator implements org.springframework.validation.Validator {
@Override
public boolean supports(Class<?> clazz) {
return Test.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
Test test = (Test) target;
errors.rejectValue("field3", "weird");
System.out.println(test.getField1());
System.out.println(test.getField2());
System.out.println(test.getField3());
}
}
Class to be validate
要验证的类
public class Test {
@Size(min = 3)
private String field2;
@NotNull
@NotEmpty
private String field1;
@NotNull
@Past
private LocalDateTime field3;
//...
//getter/setter
//...
}
CustomLocalValidatorFactoryBean
自定义本地验证器工厂Bean
public class CustomLocalValidatorFactoryBean extends LocalValidatorFactoryBean {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public void validate(@Nullable Object target, Errors errors, @Nullable Object... validationHints) {
Set<Validator> concreteValidators = new LinkedHashSet<>();
Set<Class<?>> interfaceGroups = new LinkedHashSet<>();
extractConcreteValidatorsAndInterfaceGroups(concreteValidators, interfaceGroups, validationHints);
proccessConcreteValidators(target, errors, concreteValidators);
processConstraintViolations(super.validate(target, interfaceGroups.toArray(new Class<?>[interfaceGroups.size()])), errors);
}
private void proccessConcreteValidators(Object target, Errors errors, Set<Validator> concreteValidators) {
for (Validator validator : concreteValidators) {
validator.validate(target, errors);
}
}
private void extractConcreteValidatorsAndInterfaceGroups(Set<Validator> concreteValidators, Set<Class<?>> groups, Object... validationHints) {
if (validationHints != null) {
for (Object hint : validationHints) {
if (hint instanceof Class) {
if (((Class<?>) hint).isInterface()) {
groups.add((Class<?>) hint);
} else {
Optional<Validator> validatorOptional = getValidatorFromGenericClass(hint);
if (validatorOptional.isPresent()) {
concreteValidators.add(validatorOptional.get());
}
}
}
}
}
}
@SuppressWarnings("unchecked")
private Optional<Validator> getValidatorFromGenericClass(Object hint) {
try {
Class<Validator> clazz = (Class<Validator>) Class.forName(((Class<?>) hint).getName());
return Optional.of(clazz.newInstance());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
logger.info("There is a problem with the class that you passed to "
+ " @Validated annotation in the controller, we tried to "
+ " cast to org.springframework.validation.Validator and we cant do this");
}
return Optional.empty();
}
}
Configure application
配置应用程序
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public javax.validation.Validator localValidatorFactoryBean() {
return new CustomLocalValidatorFactoryBean();
}
}
Input to /test
endpoint:
/test
端点的输入:
{
"field1": "",
"field2": "aaaa",
"field3": "2018-04-15T15:10:24"
}
Output from /test
endpoint:
/test
端点输出:
{
"timestamp": "2018-04-16T17:34:28.532+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"weird.test.field3",
"weird.field3",
"weird.java.time.LocalDateTime",
"weird"
],
"arguments": null,
"defaultMessage": null,
"objectName": "test",
"field": "field3",
"rejectedValue": "2018-04-15T15:10:24",
"bindingFailure": false,
"code": "weird"
},
{
"codes": [
"NotEmpty.test.field1",
"NotEmpty.field1",
"NotEmpty.java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"test.field1",
"field1"
],
"arguments": null,
"defaultMessage": "field1",
"code": "field1"
}
],
"defaultMessage": "N?o pode estar vazio",
"objectName": "test",
"field": "field1",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotEmpty"
}
],
"message": "Validation failed for object='test'. Error count: 2",
"path": "/user/test"
}
Input to /test2
endpoint:
/test2
端点的输入:
{
"field1": "",
"field2": "aaaa",
"field3": "2018-04-15T15:10:24"
}
Output to /test2
endpoint:
输出到/test2
端点:
{
"timestamp": "2018-04-16T17:37:30.889+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotEmpty.test.field1",
"NotEmpty.field1",
"NotEmpty.java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"test.field1",
"field1"
],
"arguments": null,
"defaultMessage": "field1",
"code": "field1"
}
],
"defaultMessage": "N?o pode estar vazio",
"objectName": "test",
"field": "field1",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotEmpty"
}
],
"message": "Validation failed for object='test'. Error count: 1",
"path": "/user/test2"
}
I hope this help.
我希望这会有所帮助。