Spring MVC:検証を行う方法は?


156

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

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


回答:


322

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歳未満の場合、responseUserはnullであってはならず、responseUserの年齢は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を組み合わせて使用​​することはできません。デフォルトを置き換えるためです)バリデータ)。または、コントローラのデフォルトコンストラクタでインスタンス化することもできます。または、コントローラーに@ Component / @ Service UserValidatorを注入(@Autowired)します。ほとんどのバリデーターはシングルトンであるため、非常に便利です+単体テストのモッキングが簡単になります+バリデーターは他のSpringコンポーネントを呼び出すことができます。

方法3: 両方の方法を組み合わせて使用​​しないのはなぜですか?「name」属性のような単純なものを、注釈を付けて検証します(これは素早く、簡潔で読みやすくなります)。バリデーターの重い検証を保持します(カスタムの複雑な検証注釈のコーディングに数時間かかる場合、または注釈を使用できない場合のみ)。私は以前のプロジェクトでこれを行いました、それは魅力的で素早く簡単に機能しました。

警告:検証処理例外処理を混同しないでください。それらをいつ使用するかを知るには、この投稿読んでください

参照:


この構成でservlet.xmlに何を設定する必要があるか教えてください。エラーをビューに戻したい
devdar

@dev_darin JSR-303検証の構成を意味しますか?
Jerome Dalbert、2012

2
@dev_marin検証のために、Spring 3.x以降では、「servlet.xml」または「[servlet-name] -servlet.xml」に特別なものはありません。プロジェクトライブラリに(またはMavenを介して)hibernate-validator jarが必要です。方法3を使用する場合の警告:デフォルトでは、各コントローラーはJSR-303バリデーターにアクセスできるため、「setValidator」でオーバーライドしないように注意してください。カスタムバリデーターをトップ、ちょうどそれをインスタンス化し、それを使用するか、(それは春のコンポーネントである場合)、それを注入それでもGoogleと春のドキュメントを確認した後に問題が発生した場合は、新しい質問を投稿する必要があります。。
ジェロームDalbert

2
メソッド1と2を組み合わせて使用​​する場合、@ InitBinderを使用する方法があります。「binder.setValidator(...)」の代わりに、「binder.addValidators(...)」を使用できます
jasonfungsing

1
私が間違っている場合は修正してください。ただし、@ InitBinderアノテーションを使用する場合は、JSR-303アノテーションによる検証(メソッド1)とカスタム検証(メソッド2)を混在させることができます。単にバインダー.setValidator(userValidator)の代わりにバインダー.addValidators(userValidator)を使用すると、両方の検証メソッドが有効になります。
SebastianRiemer

31

ユーザー入力を検証するには、注釈を使用する方法と、SpringのValidatorクラスを継承する方法の2つの方法があります。単純なケースでは、注釈は適切です。複雑な検証が必要な場合(「メールアドレスの検証」フィールドなどのフィールド間検証など)、またはモデルがアプリケーションの複数の場所で異なるルールで検証されている場合、またはモデルを変更する機能がない場合モデルオブジェクトに注釈を配置することで、Springの継承ベースのValidatorを使用できます。両方の例を示します。

実際の検証部分は、使用している検証のタイプに関係なく同じです。

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

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

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

Spring docsも参照してください。

お役に立てば幸いです。


SpringのValidatorを使用する場合、コントローラーからpojoを設定して検証する必要がありますか?
devdar 2012

私はあなたの質問を理解しているのかわかりません。コントローラーのコードスニペットが表示される場合、Springは送信されたフォームをFooハンドラーメソッドのパラメーターに自動的にバインドしています。明確にできますか?
stephen.hanson 2012

私が言っているのは、ユーザーがユーザー入力を送信すると、コントローラーがhttpリクエストを取得するときです。そこから、request.getParameter()を使用してすべてのユーザーパラメーターを取得し、POJOに値を設定して渡すとどうなりますか。検証オブジェクトへのクラス。検証クラスは、エラーが見つかった場合はエラーとともにビューに送り返します。これはそれが起こる方法ですか?
devdar 2012

1
このようになりますが、もっと簡単な方法があります... JSPと<form:form commandName = "user">送信を使用する場合、データはコントローラーの@ModelAttribute( "user")ユーザーに自動的に配置されます方法。:ドキュメントを参照してくださいstatic.springsource.org/spring/docs/3.0.x/...
ジェロームDalbert

+1は、@ ModelAttributeを使用する最初の例です。それがなければ、私が見つけたチュートリアルはどれもうまくいきませんでした。
Riccardo Cossu 14

12

ジェローム・ダルバートの素敵な答えを広げたいと思います。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;

}

1
また、コントローラーには通常1つのバリデーターがあり、複数のバリデーターを使用できることも知りましたが、1つのオブジェクトに対して一連の検証を定義している場合、オブジェクトに対して実行する操作が異なります。特定の検証セットを保存する必要があり、更新には別の検証セットが必要です。操作に基づいてすべての検証を保持するようにバリデータークラスを構成する方法はありますか、または複数のバリデーターを使用する必要がありますか?
devdar 2013年

1
メソッドでアノテーション検証を行うこともできます。質問が理解できれば、独自の「ドメイン検証」を作成できます。このためには、指定する必要がありますElementType.METHODの中で@Target
michal.kreuzman 2013年

私はあなたの言っていることが理解できます。もっと明確な写真の例を私に教えてもらえますか。
devdar

4

異なるメソッドハンドラーに対して同じエラー処理ロジックがある場合、次のコードパターンのハンドラーが多数作成されます。

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

RESTfulサービスを作成400 Bad Requestしていて、検証エラーのケースごとにエラーメッセージとともに返るとします。次に、エラー処理の部分は、検証が必要なすべてのRESTエンドポイントで同じになります。まったく同じロジックをすべてのハンドラーで繰り返すことは、それほどドライではありません!

この問題を解決する1つの方法はBindingResult、各検証対象 Beanの直後を削除することです。これで、ハンドラーは次のようになります。

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

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

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

のメソッドをBindingResult使用して、基になるものを調べることができます。getBindingResultMethodArgumentNotValidException


1

Spring Mvc検証の完全な例を見つける

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

Beanを手動で検証するため。次に、BindingResultのすべての結果を取得し、そこから取得できます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.