web-dev-qa-db-ja.com

JSR-303とSpringのValidatorの組み合わせを使用したSpringブートエンドポイントのカスタム検証ロジックの実装

JSR-303 Bean Validation APISpring's Validatorの組み合わせを使用して、Spring Bootエンドポイントのカスタム検証ロジックを実装しようとしています。

Validatorクラス図に基づいて、CustomValidatorBeanSpringValidatorAdapterまたはLocalValidatorFactoryBeanのいずれかを拡張して、カスタム検証ロジックをオーバーライドされたメソッドに追加することが可能であるようですvalidate(Object target, Errors errors)

Validator class diagram

ただし、これら3つのクラスのいずれかを拡張するバリデーターを作成し、@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は実行されません。 SpringValidatorAdapterJSR-303 Bean Validationを実装する方法と同様に、カスタムJSR-303検証を実装できますが、代わりにそれを拡張する方法が必要です。

    @Override
    public void validate(Object target, Errors errors) {
        if (this.targetValidator != null) {
            processConstraintViolations(this.targetValidator.validate(target), errors);
        }
    }

私はorg.springframework.validation.Validatorを一緒に使用しないようにカスタムJSR-303制約を使用することを検討しましたが、カスタムバリデーターを機能させる方法が必要です。

Spring 検証ドキュメント は、2つを組み合わせると明確ではありません:

「DataBinderの設定」で説明されているように、アプリケーションはDataBinderインスタンスごとに追加のSpring Validatorインスタンスを登録することもできます。これは、注釈を使用せずに検証ロジックをプラグインする場合に役立ちます。

その後、複数のValidatorインスタンスの設定に触れます

DataBinderは、dataBinder.addValidatorsおよびdataBinder.replaceValidatorsを介して複数のValidatorインスタンスで構成することもできます。これは、グローバルに構成されたBean Validationを、DataBinderインスタンスでローカルに構成されたSpring Validatorと組み合わせる場合に役立ちます。見る ???。

Spring Boot 1.4.0を使用しています。

8
pavel

@ M.Deinumごと-setValidator()の代わりにaddValidators()を使用するとうまくいきました。また、JSR-303の@AssertTrueメソッドベースのアノテーションをフィールド間検証専用に使用することは、おそらくよりクリーンなソリューションであることにも同意します。コード例は https://github.com/pavelfomin/spring-boot-rest-example/tree/feature/custom-validator にあります。この例では、ミドルネームの検証はカスタムSpringバリデーターを介して実行され、姓の検証はデフォルトのjsr 303バリデーターによって処理されます。

5
pavel

この問題は、LocalValidatorFactoryBeanを拡張することで解決できます。このクラス内のvalidateメソッドをオーバーライドして、必要な動作を行うことができます。

私の場合、JSR-303とカスタムバリデーターを同じコントローラーの異なるメソッドで使用する必要があります。通常、@ InitBinderを使用することをお勧めしますが、InitBinderがモデルとバリデーター( @RequestBodyを使用する場合、InitBinderは、コントローラーごとに1つのモデルと1つのバリデーター用です。

コントローラ

@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());
     }
}

検証するクラス

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"
}

これがお役に立てば幸いです。

4
Marco Blos