복붙노트

[SPRING] JSR-303과 Spring의 Validator를 함께 사용하여 스프링 부트 엔드 포인트에 대한 사용자 정의 유효성 검증 로직 구현

SPRING

JSR-303과 Spring의 Validator를 함께 사용하여 스프링 부트 엔드 포인트에 대한 사용자 정의 유효성 검증 로직 구현

JSR-303 Bean Validation API와 Spring의 Validator를 함께 사용하여 스프링 부트 엔드 포인트에 대한 사용자 정의 유효성 검증 로직을 구현하려고합니다.

Validator 클래스 다이어그램을 기반으로 CustomValidatorBean, SpringValidatorAdapter 또는 LocalValidatorFactoryBean 중 하나를 확장하여 재정의 된 메서드 validate (Object target, Errors 오류)에 사용자 지정 유효성 검사 논리를 추가 할 수 있습니다.

.

그러나 이러한 3 개의 클래스 중 하나를 확장하여 @InitBinder를 사용하여 유효성 검사기를 만드는 경우 validate (Object target, Errors errors) 메서드는 호출되지 않으며 유효성 검사가 수행되지 않습니다. @InitBinder를 제거하면 기본 스프링 유효성 검사기가 JSR-303 Bean 유효성 검사를 수행합니다.

나머지 컨트롤러 :

@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 오류) 메서드가 호출되지만 JSR-303 Bean 유효성 검사는 먼저 수행되지 않습니다. SpringValidatorAdapter가 JSR-303 Bean 유효성 검사를 구현하는 것과 비슷한 사용자 정의 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 제약을 사용했지만 사용자 정의 검사기를 작동시키는 방법이 있어야합니다.

스프링 검증 문서는 두 가지를 결합 할 때 명확하지 않습니다.

그런 다음 나중에 여러 Validator 인스턴스를 구성하는 방법에 대해 알아 봅니다.

나는 스프링 부트 1.4.0을 사용하고있다.

해결법

  1. ==============================

    1.Per @ M.Deinum - setValidator () 대신 addValidators ()를 사용하여 트릭을 수행했습니다. 또한 JSR-303을 사용하여 @AssertTrue 메소드 기반 주석을 크로스 필드 유효성 검사에 사용하는 것이 더 깔끔한 솔루션 일 것입니다. 코드 예제는 https://github.com/pavelfomin/spring-boot-rest-example/tree/feature/custom-validator에서 확인할 수 있습니다. 이 예제에서 중간 이름 유효성 검사는 사용자 정의 스프링 유효성 검사기를 통해 수행되는 반면 성 성 검증은 기본 jsr 303 유효성 검사기가 처리합니다.

    Per @ M.Deinum - setValidator () 대신 addValidators ()를 사용하여 트릭을 수행했습니다. 또한 JSR-303을 사용하여 @AssertTrue 메소드 기반 주석을 크로스 필드 유효성 검사에 사용하는 것이 더 깔끔한 솔루션 일 것입니다. 코드 예제는 https://github.com/pavelfomin/spring-boot-rest-example/tree/feature/custom-validator에서 확인할 수 있습니다. 이 예제에서 중간 이름 유효성 검사는 사용자 정의 스프링 유효성 검사기를 통해 수행되는 반면 성 성 검증은 기본 jsr 303 유효성 검사기가 처리합니다.

  2. ==============================

    2.이 문제는 LocalValidatorFactoryBean을 확장하여 해결할 수 있습니다.이 클래스 내부의 validate 메소드를 재정 의하여 원하는 동작을 제공 할 수 있습니다.

    이 문제는 LocalValidatorFactoryBean을 확장하여 해결할 수 있습니다.이 클래스 내부의 validate 메소드를 재정 의하여 원하는 동작을 제공 할 수 있습니다.

    필자의 경우, 동일한 컨트롤러의 다른 메소드에서 동일한 모델에 대해 JSR-303 및 사용자 정의 유효성 검사기를 사용해야하지만 일반적으로 @InitBinder를 사용하는 것이 좋지만 InitBinder가 Model과 Validator 사이에 바인딩을 만들기 때문에 필자의 경우에는 충분하지 않습니다 (if @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());
         }
    }
    

    유효성을 검사 할 클래스

    public class Test {
    
        @Size(min = 3)
        private String field2;
    
        @NotNull
        @NotEmpty
        private String field1;
    
        @NotNull
        @Past
        private LocalDateTime field3;
    
        //...
        //getter/setter
        //...
    }
    

    커스텀 LocalValidatorFactoryBean

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

    끝점 입력 / 테스트 :

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

    / 테스트 엔드 포인트의 출력 :

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

    나는이 도움을 바랍니다.

  3. from https://stackoverflow.com/questions/39001106/implementing-custom-validation-logic-for-a-spring-boot-endpoint-using-a-combinat by cc-by-sa and MIT license