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

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-11 20:48:33  来源:igfitidea点击:

Implementing custom validation logic for a spring boot endpoint using a combination of JSR-303 and Spring's Validator

javaspringvalidationspring-bootbean-validation

提问by pavel

I'm trying to implement some custom validation logic for a spring boot endpoint using a combination of JSR-303 Bean Validation APIand Spring's Validator.

我试图实现使用的组合弹簧引导终点一些自定义的验证逻辑JSR-303 Bean Validation APISpring's Validator

Based on the Validator class diagram it appears to be possible to extend one of CustomValidatorBean, SpringValidatorAdapteror LocalValidatorFactoryBeanto add some custom validation logic into an overridden method validate(Object target, Errors errors).

根据 Validator 类图,似乎可以扩展其中之一CustomValidatorBeanSpringValidatorAdapter或者LocalValidatorFactoryBean将一些自定义验证逻辑添加到重写的方法中validate(Object target, Errors errors)

Validator class diagram.

验证器类图.

However, if I create a validator extending any of these three classes and register it using @InitBinderits validate(Object target, Errors errors)method is never invoked and no validation is performed. If I remove @InitBinderthen a default spring validator performs the JSR-303 Bean Validation.

但是,如果我创建一个扩展这三个类中的任何一个的验证器并使用@InitBindervalidate(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.Validatorthen its validate(Object target, Errors errors)method is called but JSR-303 Bean Validationis not performed prior to it. I can implement my custom JSR-303 validation similar to the way SpringValidatorAdapterimplements its JSR-303 Bean Validationbut 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.Validatorall 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 validatemethod 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 /testendpoint:

/test端点的输入:

{
    "field1": "",
    "field2": "aaaa",
    "field3": "2018-04-15T15:10:24"
}

Output from /testendpoint:

/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 /test2endpoint:

/test2端点的输入:

{
    "field1": "",
    "field2": "aaaa",
    "field3": "2018-04-15T15:10:24"
}

Output to /test2endpoint:

输出到/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.

我希望这会有所帮助。