web-dev-qa-db-ja.com

Spring MVC:検証方法は?

ユーザー入力のフォーム検証を実行するための最もクリーンで最良の方法は何かを知りたいです。一部の開発者が org.springframework.validation.Validator を実装しているのを見ました。それについての質問:私はそれがクラスを検証するのを見ました。ユーザー入力からの値をクラスに手動で入力してからバリデータに渡す必要がありますか?

ユーザー入力を検証する最もクリーンで最良の方法について私は混乱しています。私はrequest.getParameter()を使って手動でnullsをチェックする伝統的な方法を知っていますが、私はControllerですべての検証をしたくありません。この分野に関するいくつかの良いアドバイスは大歓迎です。このアプリケーションではHibernateを使用していません。

147
devdar

Spring MVCでは、検証を実行するための3つの異なる方法があります。アノテーションを使用するか、手動で行うか、または両方を組み合わせることです。検証するための独自の「最もクリーンで最良の方法」はありませんが、おそらくあなたのプロジェクト/問題/コンテキストによく合うものがあるでしょう。

ユーザーを作りましょう:

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は便利なorg.springframework.validation.Validatorインターフェースを提供します(Spring 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になります。

注: "binding.setValidator(...)"を使用して、コントローラの@InitBinderメソッドにバリデータを設定することもできます(この場合、デフォルトのメソッドを置き換えるため、方法1と方法2を混在させることはできません)。バリデータ)あるいは、コントローラのデフォルトコンストラクタでそれをインスタンス化することもできます。または、@ Component/@ Service UserValidatorをコントローラに挿入する(@Autowired):非常に便利です。ほとんどのバリデータはシングルトンなので、+単体テストモックが簡単になります。+バリデータが他のSpringコンポーネントを呼び出すこともできます。

方法3:なぜ両方の方法を組み合わせて使用​​しないのですか? "name"属性のような単純なものをアノテーションで検証してください(素早く、簡潔で読みやすくなります)。バリデータのための重い検証を維持します(カスタムの複雑な検証アノテーションをコーディングするのに何時間もかかる場合、またはアノテーションを使用することが不可能な場合のみ)。私は前のプロジェクトでこれをしました、それは魅力的で、速くて簡単のように働きました。

警告:間違えてはいけません検証処理例外処理です。 この記事を読む それらをいつ使うべきかを知るために。

参考文献:

313
Jerome Dalbert

ユーザー入力を検証する方法は2つあります。アノテーションとSpringのValidatorクラスを継承することです。単純な場合では、注釈はNiceです。複雑な検証が必要な場合(クロスフィールド検証、たとえば「電子メールアドレスの検証」フィールドなど)、モデルがアプリケーション内の複数の場所でさまざまな規則で検証されている場合、または自分のモデルを変更できないそれにアノテーションを配置することによってモデルオブジェクト、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のorg.hibernate.validator.constraintsを使うこともできますが、Hibernateを使っているようには見えません。

あるいは、Springのバリデータを実装する場合は、次のようにクラスを作成します。

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]");
        }
    }
}

上記のバリデータを使用する場合は、バリデータをSpringコントローラにバインドする必要があります(アノテーションを使用する場合は不要です)。

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

Spring docs もご覧ください。

それが役立つことを願っています。

29
stephen.hanson

Jerome DalbertのNice回答を拡張したいと思います。私はJSR-303の方法であなた自身のアノテーションバリデータを書くことが非常に簡単であるとわかりました。あなたは "1フィールド"検証を持っていることに制限されていません。独自の注釈を型レベルで作成して複雑な検証を行うことができます(下記の例を参照)。 Jeromeのように異なる種類の検証(SpringとJSR-303)を混在させる必要がないので、この方法を好みます。また、このバリデータは "Spring対応"なので、@ 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;

}
12
michal.kreuzman

異なるメソッドハンドラに対して同じエラー処理ロジックがある場合は、次のようなコードパターンのハンドラが多数存在することになります。

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

RESTfulサービスを作成していて、検証エラーが発生するたびにエラーメッセージと一緒に400 Bad Requestを返したいとします。その場合、エラー処理部分は、検証が必要なすべてのRESTエンドポイントで同じになります。すべてのハンドラで同じロジックを繰り返すのは、そうではありませんDRY ish!

この問題を解決する1つの方法は、各検証対象Beanの後にすぐにBindingResultをドロップすることです。今、あなたのハンドラはこのようになります:

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

このように、バインドされたBeanが無効な場合は、SpringによってMethodArgumentNotValidExceptionがスローされます。同じエラー処理ロジックを使って、この例外を処理するControllerAdviceを定義できます。

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

BindingResultgetBindingResultメソッドを使って、基礎となるMethodArgumentNotValidExceptionを調べることができます。

3
Ali Dehghani

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);
    }
}
1
Vicky

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

手動でBeanを検証します。そうすれば、すべての結果がBindingResultに返され、そこから取得できます。

0
praveen jain