使用 JSR-303 和 Spring 的验证器的组合为 spring 启动端点实现自定义验证逻辑
Implementing custom validation logic for a spring boot endpoint using a combination of JSR-303 and Spring's Validator
我正在尝试使用 JSR-303 Bean Validation API
和 Spring's Validator
的组合为 spring 启动端点实施一些自定义验证逻辑。
基于验证器 class 图,似乎可以扩展 CustomValidatorBean
、SpringValidatorAdapter
或 LocalValidatorFactoryBean
之一,以将一些自定义验证逻辑添加到重写的方法 validate(Object target, Errors errors)
.
.
但是,如果我创建一个扩展这三个 classes 中的任何一个的验证器并使用 @InitBinder
注册它,它的 validate(Object target, Errors errors)
方法将永远不会被调用并且不会执行任何验证。如果我删除 @InitBinder
然后默认 spring 验证器执行 JSR-303 Bean Validation
.
休息控制器:
@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);
}
}
自定义验证器:
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);
}
}
如果我的验证器实现了 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);
}
}
我已经研究过使用自定义 JSR-303 约束来避免一起使用 org.springframework.validation.Validator
,但必须有一种方法可以使自定义验证器工作。
Spring validation documentation 将两者结合起来不是很清楚:
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.
稍后会涉及到配置多个 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 ???.
我正在使用 spring boot 1.4.0.
Per @M.Deinum - 使用 addValidators() 而不是 setValidator() 就可以了。我也同意使用 JSR-303,@AssertTrue 基于方法的注释专门用于跨字段验证,可能是一个更干净的解决方案。 https://github.com/pavelfomin/spring-boot-rest-example/tree/feature/custom-validator 处提供了代码示例。在示例中,中间名验证通过自定义 spring 验证器执行,而姓氏验证由默认的 jsr 303 验证器处理。
这个问题可以通过扩展 LocalValidatorFactoryBean 来解决,您可以覆盖此 class 中的 validate
方法,提供您想要的任何行为。
在我的例子中,我需要在同一个控制器的不同方法中为同一个模型使用 JSR-303 和自定义验证器,通常建议使用@InitBinder,但这对我来说是不够的,因为 InitBinder 在两者之间进行绑定模型和验证器(如果您使用@RequestBody InitBinder 仅适用于一个模型和每个控制器一个验证器)。
控制器
@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;
}
}
自定义验证器
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待验证
public class Test {
@Size(min = 3)
private String field2;
@NotNull
@NotEmpty
private String field1;
@NotNull
@Past
private LocalDateTime field3;
//...
//getter/setter
//...
}
CustomLocalValidatorFactoryBean
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();
}
}
配置应用程序
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public javax.validation.Validator localValidatorFactoryBean() {
return new CustomLocalValidatorFactoryBean();
}
}
输入到 /test
端点:
{
"field1": "",
"field2": "aaaa",
"field3": "2018-04-15T15:10:24"
}
来自 /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"
}
输入到 /test2
端点:
{
"field1": "",
"field2": "aaaa",
"field3": "2018-04-15T15:10:24"
}
输出到/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"
}
希望对您有所帮助。
我正在尝试使用 JSR-303 Bean Validation API
和 Spring's Validator
的组合为 spring 启动端点实施一些自定义验证逻辑。
基于验证器 class 图,似乎可以扩展 CustomValidatorBean
、SpringValidatorAdapter
或 LocalValidatorFactoryBean
之一,以将一些自定义验证逻辑添加到重写的方法 validate(Object target, Errors errors)
.
但是,如果我创建一个扩展这三个 classes 中的任何一个的验证器并使用 @InitBinder
注册它,它的 validate(Object target, Errors errors)
方法将永远不会被调用并且不会执行任何验证。如果我删除 @InitBinder
然后默认 spring 验证器执行 JSR-303 Bean Validation
.
休息控制器:
@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);
}
}
自定义验证器:
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);
}
}
如果我的验证器实现了 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);
}
}
我已经研究过使用自定义 JSR-303 约束来避免一起使用 org.springframework.validation.Validator
,但必须有一种方法可以使自定义验证器工作。
Spring validation documentation 将两者结合起来不是很清楚:
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.
稍后会涉及到配置多个 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 ???.
我正在使用 spring boot 1.4.0.
Per @M.Deinum - 使用 addValidators() 而不是 setValidator() 就可以了。我也同意使用 JSR-303,@AssertTrue 基于方法的注释专门用于跨字段验证,可能是一个更干净的解决方案。 https://github.com/pavelfomin/spring-boot-rest-example/tree/feature/custom-validator 处提供了代码示例。在示例中,中间名验证通过自定义 spring 验证器执行,而姓氏验证由默认的 jsr 303 验证器处理。
这个问题可以通过扩展 LocalValidatorFactoryBean 来解决,您可以覆盖此 class 中的 validate
方法,提供您想要的任何行为。
在我的例子中,我需要在同一个控制器的不同方法中为同一个模型使用 JSR-303 和自定义验证器,通常建议使用@InitBinder,但这对我来说是不够的,因为 InitBinder 在两者之间进行绑定模型和验证器(如果您使用@RequestBody InitBinder 仅适用于一个模型和每个控制器一个验证器)。
控制器
@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;
}
}
自定义验证器
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待验证
public class Test {
@Size(min = 3)
private String field2;
@NotNull
@NotEmpty
private String field1;
@NotNull
@Past
private LocalDateTime field3;
//...
//getter/setter
//...
}
CustomLocalValidatorFactoryBean
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();
}
}
配置应用程序
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Bean
public javax.validation.Validator localValidatorFactoryBean() {
return new CustomLocalValidatorFactoryBean();
}
}
输入到 /test
端点:
{
"field1": "",
"field2": "aaaa",
"field3": "2018-04-15T15:10:24"
}
来自 /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"
}
输入到 /test2
端点:
{
"field1": "",
"field2": "aaaa",
"field3": "2018-04-15T15:10:24"
}
输出到/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"
}
希望对您有所帮助。