Spring MVC : 유효성 검사를 수행하는 방법?


156

사용자 입력의 양식 유효성 검사를 수행하는 가장 깨끗하고 가장 좋은 방법이 무엇인지 알고 싶습니다. 일부 개발자가 구현하는 것을 보았습니다 org.springframework.validation.Validator. 그것에 대한 질문 : 나는 그것이 클래스를 검증하는 것을 보았습니다. 클래스는 사용자 입력의 값으로 수동으로 채워져 유효성 검사기로 전달되어야합니까?

사용자 입력을 확인하는 가장 깨끗하고 가장 좋은 방법에 대해 혼란스러워합니다. 나는을 사용 request.getParameter()하고 수동으로 검사 하는 전통적인 방법에 대해 알고 nulls있지만 내 모든 유효성 검사를하고 싶지 않습니다 Controller. 이 분야에 대한 좋은 조언은 크게 감사하겠습니다. 이 응용 프로그램에서 최대 절전 모드를 사용하지 않습니다.


답변:


322

Spring MVC를 사용하면 주석을 사용하거나 수동으로 사용하거나 두 가지를 혼합하여 유효성 검사를 수행하는 세 가지 방법이 있습니다. 검증 할 수있는 독창적 인 "깨끗하고 최상의 방법"은 없지만 프로젝트 / 문제 / 문맥에 더 잘 맞는 방법이있을 것입니다.

사용자를 갖자 :

public class User {

    private String name;

    ...

}

방법 1 : Spring 3.x +를 사용하고 간단한 유효성 검사를 수행하는 경우 javax.validation.constraints주석 (JSR-303 주석이라고도 함)을 사용하십시오.

public class User {

    @NotNull
    private String name;

    ...

}

참조 구현 인 Hibernate Validator 와 같은 라이브러리에 JSR-303 공급자가 필요합니다 (이 라이브러리는 데이터베이스 및 관계형 매핑과 관련이 없으며 유효성 검사 만 수행합니다 :-).

그런 다음 컨트롤러에 다음과 같은 것이 있습니다.

@RequestMapping(value="/user", method=RequestMethod.POST)
public createUser(Model model, @Valid @ModelAttribute("user") User user, BindingResult result){
    if (result.hasErrors()){
      // do something
    }
    else {
      // do something else
    }
}

@Valid : 사용자가 null 이름을 가지면 result.hasErrors ()는 true입니다.

방법 2 : 복잡한 유효성 검사 (큰 비즈니스 유효성 검사 논리, 여러 필드의 조건부 유효성 검사 등)가 있거나 어떤 이유로 든 방법 1을 사용할 수없는 경우 수동 유효성 검사를 사용하십시오. 컨트롤러 코드와 유효성 검사 논리를 분리하는 것이 좋습니다. Spring은 검증 클래스를 처음부터 작성하지 마십시오. Spring은 org.springframework.validation.ValidatorSpring 2부터 편리한 인터페이스를 제공합니다 .

그래서 당신이 있다고 가정 해 봅시다

public class User {

    private String name;

    private Integer birthYear;
    private User responsibleUser;
    ...

}

사용자의 연령이 18 세 미만인 경우 책임 사용자는 null이 아니고 책임 사용자의 나이는 21 세 이상이어야합니다.

당신은 이런 식으로 할 것입니다

public class UserValidator implements Validator {

    @Override
    public boolean supports(Class clazz) {
      return User.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
      User user = (User) target;

      if(user.getName() == null) {
          errors.rejectValue("name", "your_error_code");
      }

      // do "complex" validation here

    }

}

그런 다음 컨트롤러에 다음이 있습니다.

@RequestMapping(value="/user", method=RequestMethod.POST)
    public createUser(Model model, @ModelAttribute("user") User user, BindingResult result){
        UserValidator userValidator = new UserValidator();
        userValidator.validate(user, result);

        if (result.hasErrors()){
          // do something
        }
        else {
          // do something else
        }
}

유효성 검사 오류가있는 경우 result.hasErrors ()는 true입니다.

참고 : "binder.setValidator (...)"를 사용하여 컨트롤러의 @InitBinder 메소드에서 유효성 검사기를 설정할 수도 있습니다 (이 경우 기본값을 바꾸므로 메소드 1과 2를 혼합하여 사용할 수 없음) 유효성 검사기). 또는 컨트롤러의 기본 생성자에서 인스턴스를 생성 할 수 있습니다. 또는 컨트롤러에 @Autowired를 주입하는 @ Component / @ Service UserValidator를 사용하십시오. 대부분의 유효성 검사기는 싱글 톤 + 단위 테스트 조롱이 쉬워지고 유효성 검사기는 다른 Spring 구성 요소를 호출 할 수 있기 때문에 매우 유용합니다.

방법 3 : 두 방법을 함께 사용하지 않는 이유는 무엇입니까? 주석을 사용하여 "name"속성과 같은 간단한 항목의 유효성을 검사하십시오 (빠르고 간결하며 읽기 쉽습니다). 사용자 지정 복잡한 유효성 검사 주석을 코딩하는 데 몇 시간이 걸리거나 주석을 사용할 수없는 경우 유효성 검사기에 대한 강력한 유효성 검사를 유지하십시오. 나는 이전 프로젝트 에서이 작업을 수행했으며 매력적이고 빠르고 쉽게 작동했습니다.

경고 : 예외 처리를 위해 유효성 검사 처리 를 착각해서는 안됩니다 . 사용시기를 알려면 이 게시물읽으십시오 .

참고 문헌 :


내 servlet.xml이이 구성에 대해 가져야 할 것을 말해 줄 수 있습니까? 오류를 다시보기로 전달하고 싶습니다
devdar

@dev_darin JSR-303 검증을위한 설정을 의미합니까?
Jerome Dalbert

2
@dev_marin Spring 3.x +에서는 유효성 검사를 위해 "servlet.xml"또는 "[servlet-name] -servlet.xml에 특별한 것이 없으며 프로젝트 라이브러리 (또는 Maven을 통해)에 최대 절전 모드 항아리가 필요합니다. 방법 3을 사용하는 경우 경고 : 기본적으로 각 컨트롤러는 JSR-303 유효성 검사기에 액세스 할 수 있으므로 "setValidator"로 재정의하지 않도록주의하십시오. 맨위로, 인스턴스화하여 사용하거나 주입하십시오 (Spring 구성 요소 인 경우). Google 및 Spring Doc을 확인한 후에도 여전히 문제가 발생하면 새로운 질문을 게시해야합니다.
Jerome Dalbert

2
방법 1과 2의 혼합 사용에는 @InitBinder를 사용하는 방법이 있습니다. 대신 "binder.setValidator (...)"의 "binder.addValidators는 (...)"를 사용할 수 있습니다
jasonfungsing

1
내가 틀렸다면 정정하지만 @InitBinder 주석을 사용할 때 JSR-303 주석 (방법 1)과 사용자 정의 유효성 검사 (방법 2)를 통해 유효성 검사를 혼합 할 수 있습니다. binder.setValidator (userValidator) 대신 binder.addValidators (userValidator)를 사용하면 두 가지 유효성 검사 방법이 모두 적용됩니다.
SebastianRiemer

31

사용자 입력을 확인하는 두 가지 방법이 있습니다 : 주석과 Spring의 Validator 클래스를 상속하는 것. 간단한 경우에는 주석이 좋습니다. 필드 간 검증 (예 : "이메일 주소 확인"필드)과 같은 복잡한 검증이 필요한 경우 또는 다른 규칙을 사용하여 응용 프로그램의 여러 위치에서 모델이 검증 된 경우 또는 Spring의 상속 기반 유효성 검사기가 주석을 달아 모델 객체를 모델링하는 방법입니다. 두 가지 예를 보여 드리겠습니다.

실제 유효성 검사 부분은 사용중인 유효성 검사 유형에 관계없이 동일합니다.

RequestMapping(value="fooPage", method = RequestMethod.POST)
public String processSubmit(@Valid @ModelAttribute("foo") Foo foo, BindingResult result, ModelMap m) {
    if(result.hasErrors()) {
        return "fooPage";
    }
    ...
    return "successPage";
}

주석을 사용하는 경우 Foo클래스는 다음과 같습니다.

public class Foo {

    @NotNull
    @Size(min = 1, max = 20)
    private String name;

    @NotNull
    @Min(1)
    @Max(110)
    private Integer age;

    // getters, setters
}

위의 javax.validation.constraints주석은 주석입니다. Hibernate 's를 사용할 수도 있지만 Hibernate를 사용하는 org.hibernate.validator.constraints것처럼 보이지 않습니다.

또는 Spring의 Validator를 구현하면 다음과 같이 클래스를 작성합니다.

public class FooValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return Foo.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {

        Foo foo = (Foo) target;

        if(foo.getName() == null) {
            errors.rejectValue("name", "name[emptyMessage]");
        }
        else if(foo.getName().length() < 1 || foo.getName().length() > 20){
            errors.rejectValue("name", "name[invalidLength]");
        }

        if(foo.getAge() == null) {
            errors.rejectValue("age", "age[emptyMessage]");
        }
        else if(foo.getAge() < 1 || foo.getAge() > 110){
            errors.rejectValue("age", "age[invalidAge]");
        }
    }
}

위의 유효성 검사기를 사용하는 경우 유효성 검사기를 스프링 컨트롤러에 바인딩해야합니다 (주석을 사용하는 경우 필요하지 않음).

@InitBinder("foo")
protected void initBinder(WebDataBinder binder) {
    binder.setValidator(new FooValidator());
}

Spring 문서 도 참조하십시오 .

희망이 도움이됩니다.


Spring의 Validator를 사용할 때 컨트롤러에서 pojo를 설정 한 다음 유효성을 검사해야합니까?
devdar

귀하의 질문을 이해하지 못했습니다. 컨트롤러 코드 스 니펫이 보이면 Spring은 제출 된 양식을 Foo핸들러 메소드 의 매개 변수에 자동으로 바인딩합니다 . 당신은 명확히 할 수 있습니까?
stephen.hanson

좋아, 내가 말하는 것은 사용자가 사용자 입력을 제출하면 Controller가 http 요청을 얻는다는 것입니다. 거기에서 request.getParameter ()를 사용하여 모든 사용자 매개 변수를 얻은 다음 POJO의 값을 설정하고 전달합니다. 검증 객체에 대한 클래스 유효성 검사 클래스는 오류가 발견되면 오류를 다시보기로 보냅니다. 이것이 일어나는 방식입니까?
devdar

1
JSP와 <form : form commandName = "user"> 제출을 사용하면 컨트롤러의 @ModelAttribute ( "user") 사용자에게 데이터가 자동으로 저장됩니다. 방법. 다음 문서를 참조하십시오 : static.springsource.org/spring/docs/3.0.x/…
Jerome Dalbert

@ModelAttribute를 사용하는 첫 번째 예제이기 때문에 +1; 그것 없이는 내가 찾은 튜토리얼이 없습니다.
Riccardo Cossu

12

Jerome Dalbert의 좋은 답변을 연장하고 싶습니다. JSR-303 방식으로 주석 유효성 검사기를 작성하는 것이 매우 쉽다는 것을 알았습니다. "하나의 필드"유효성 검사로 제한되지 않습니다. 유형 수준에서 고유 한 주석을 만들고 복잡한 유효성 검사를 수행 할 수 있습니다 (아래 예 참조). Jerome처럼 다른 유형의 유효성 검사 (Spring 및 JSR-303)가 필요하지 않기 때문에이 방법을 선호합니다. 또한이 유효성 검사기는 "봄을 인식"하므로 @ Inject / @ Autowire를 즉시 사용할 수 있습니다.

사용자 정의 개체 유효성 검사의 예 :

@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = { YourCustomObjectValidator.class })
public @interface YourCustomObjectValid {

    String message() default "{YourCustomObjectValid.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

public class YourCustomObjectValidator implements ConstraintValidator<YourCustomObjectValid, YourCustomObject> {

    @Override
    public void initialize(YourCustomObjectValid constraintAnnotation) { }

    @Override
    public boolean isValid(YourCustomObject value, ConstraintValidatorContext context) {

        // Validate your complex logic 

        // Mark field with error
        ConstraintViolationBuilder cvb = context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate());
        cvb.addNode(someField).addConstraintViolation();

        return true;
    }
}

@YourCustomObjectValid
public YourCustomObject {
}

일반 필드 평등의 예 :

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = { FieldsEqualityValidator.class })
public @interface FieldsEquality {

    String message() default "{FieldsEquality.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    /**
     * Name of the first field that will be compared.
     * 
     * @return name
     */
    String firstFieldName();

    /**
     * Name of the second field that will be compared.
     * 
     * @return name
     */
    String secondFieldName();

    @Target({ TYPE, ANNOTATION_TYPE })
    @Retention(RUNTIME)
    public @interface List {
        FieldsEquality[] value();
    }
}




import java.lang.reflect.Field;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ReflectionUtils;

public class FieldsEqualityValidator implements ConstraintValidator<FieldsEquality, Object> {

    private static final Logger log = LoggerFactory.getLogger(FieldsEqualityValidator.class);

    private String firstFieldName;
    private String secondFieldName;

    @Override
    public void initialize(FieldsEquality constraintAnnotation) {
        firstFieldName = constraintAnnotation.firstFieldName();
        secondFieldName = constraintAnnotation.secondFieldName();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (value == null)
            return true;

        try {
            Class<?> clazz = value.getClass();

            Field firstField = ReflectionUtils.findField(clazz, firstFieldName);
            firstField.setAccessible(true);
            Object first = firstField.get(value);

            Field secondField = ReflectionUtils.findField(clazz, secondFieldName);
            secondField.setAccessible(true);
            Object second = secondField.get(value);

            if (first != null && second != null && !first.equals(second)) {
                    ConstraintViolationBuilder cvb = context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate());
          cvb.addNode(firstFieldName).addConstraintViolation();

          ConstraintViolationBuilder cvb = context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate());
          cvb.addNode(someField).addConstraintViolation(secondFieldName);

                return false;
            }
        } catch (Exception e) {
            log.error("Cannot validate fileds equality in '" + value + "'!", e);
            return false;
        }

        return true;
    }
}

@FieldsEquality(firstFieldName = "password", secondFieldName = "confirmPassword")
public class NewUserForm {

    private String password;

    private String confirmPassword;

}

1
또한 컨트롤러에 일반적으로 하나의 유효성 검사기가 있고 궁금한 점이 있지만 여러 유효성 검사기가있는 곳을 보았지만 한 개체에 대해 일련의 유효성 검사 세트가 정의되어 있지만 개체에 대해 수행하려는 작업이 다른 경우 예를 들어 저장 / 업데이트 특정 유효성 검사 세트를 저장하고 다른 유효성 검사 세트를 업데이트해야합니다. 작업을 기반으로 모든 유효성 검사를 보유하도록 유효성 검사기 클래스를 구성하는 방법이 있습니까? 아니면 여러 유효성 검사기를 사용해야합니까?
devdar

1
메소드에 대한 주석 유효성 검증을 수행 할 수도 있습니다. 귀하의 질문을 이해하면 자신의 "도메인 유효성 검사"를 만들 수 있습니다. 이를 위해 지정해야합니다 ElementType.METHOD에서 @Target.
michal.kreuzman

나는 당신이 무엇을 말하는지 이해합니다.
devdar

4

다른 메소드 핸들러에 대해 동일한 오류 처리 로직이 있으면 다음 코드 패턴을 가진 많은 핸들러가 생깁니다.

if (validation.hasErrors()) {
  // do error handling
}
else {
  // do the actual business logic
}

RESTful 서비스를 작성하고 400 Bad Request모든 유효성 검증 오류 케이스에 대해 오류 메시지와 함께 리턴하려고한다고 가정하십시오 . 그러면 오류 처리 부분은 유효성 검사가 필요한 모든 단일 REST 엔드 포인트에 대해 동일합니다. 모든 단일 핸들러에서 매우 동일한 로직을 반복하는 것이 그렇게 건조 하지는 않습니다 !

이 문제점을 해결하는 한 가지 방법은 BindingResultTo-Be-Validated Bean 직후에 즉시 삭제하는 것 입니다. 이제 핸들러는 다음과 같습니다.

@RequestMapping(...)
public Something doStuff(@Valid Somebean bean) { 
    // do the actual business logic
    // Just the else part!
}

이 방법으로 바운드 빈이 유효하지 않은 경우 MethodArgumentNotValidExceptionSpring 에서 a 를 던집니다. ControllerAdvice동일한 오류 처리 로직으로이 예외를 처리하는를 정의 할 수 있습니다 .

@ControllerAdvice
public class ErrorHandlingControllerAdvice {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public SomeErrorBean handleValidationError(MethodArgumentNotValidException ex) {
        // do error handling
        // Just the if part!
    }
}

의 기본 BindingResult사용 getBindingResult방법을 여전히 확인할 수 있습니다 MethodArgumentNotValidException.


1

Spring Mvc Validation의 전체 예를 찾으십시오.

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import com.technicalkeeda.bean.Login;

public class LoginValidator implements Validator {
    public boolean supports(Class aClass) {
        return Login.class.equals(aClass);
    }

    public void validate(Object obj, Errors errors) {
        Login login = (Login) obj;
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName",
                "username.required", "Required field");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userPassword",
                "userpassword.required", "Required field");
    }
}


public class LoginController extends SimpleFormController {
    private LoginService loginService;

    public LoginController() {
        setCommandClass(Login.class);
        setCommandName("login");
    }

    public void setLoginService(LoginService loginService) {
        this.loginService = loginService;
    }

    @Override
    protected ModelAndView onSubmit(Object command) throws Exception {
        Login login = (Login) command;
        loginService.add(login);
        return new ModelAndView("loginsucess", "login", login);
    }
}

0

이 Bean을 구성 클래스에 넣으십시오.

 @Bean
  public Validator localValidatorFactoryBean() {
    return new LocalValidatorFactoryBean();
  }

그리고 당신은 사용할 수 있습니다

 <T> BindingResult validate(T t) {
    DataBinder binder = new DataBinder(t);
    binder.setValidator(validator);
    binder.validate();
    return binder.getBindingResult();
}

빈을 수동으로 검증합니다. 그런 다음 BindingResult에 모든 결과가 표시되며 거기서 검색 할 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.