Bahar MVC: Doğrulama nasıl yapılır?


156

Kullanıcı girişlerinin form doğrulamasını yapmanın en temiz ve en iyi yolunun ne olduğunu bilmek istiyorum. Bazı geliştiricilerin uygulandığını gördüm org.springframework.validation.Validator. Bununla ilgili bir soru: Bir sınıfı doğruladığını gördüm. Sınıfın manuel olarak kullanıcı girişindeki değerlerle doldurulması ve doğrulayıcıya aktarılması gerekiyor mu?

Kullanıcı girişini doğrulamanın en temiz ve en iyi yolu hakkında kafam karıştı. Geleneksel kullanım yöntemini biliyorum request.getParameter()ve sonra manuel olarak kontrol nullsediyorum, ancak tüm doğrulamayı yapmak istemiyorum Controller. Bu alanda bazı iyi tavsiyeler büyük takdir edilecektir. Bu uygulamada Hazırda Beklet'i kullanmıyorum.


Yanıtlar:


322

Spring MVC ile, doğrulama yapmanın 3 farklı yolu vardır: ek açıklama kullanma, manuel olarak veya her ikisinin bir karışımını kullanma. Doğrulamanın benzersiz bir "en temiz ve en iyi yolu" yoktur, ancak muhtemelen projenize / probleminize / içeriğinize daha iyi uyan bir yöntem vardır.

Bir Kullanıcı edelim:

public class User {

    private String name;

    ...

}

Yöntem 1: Bahar 3.x + ve basit doğrulama varsa, javax.validation.constraintsek açıklamaları (JSR-303 ek açıklamaları olarak da bilinir) kullanın.

public class User {

    @NotNull
    private String name;

    ...

}

Referans uygulaması olan Hibernate Validator gibi kütüphanelerinizde bir JSR-303 sağlayıcısına ihtiyacınız olacaktır (bu kütüphanenin veritabanları ve ilişkisel eşleme ile ilgisi yoktur, sadece doğrulama :-).

Sonra kumandanızda şöyle bir şey olurdu:

@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 öğesine dikkat edin: Kullanıcının boş bir adı varsa, sonuç.hasErrors () doğru olacaktır.

Yöntem 2: Karmaşık doğrulamanız varsa (büyük işletme doğrulama mantığı, birden çok alanda koşullu doğrulama vb.) Veya herhangi bir nedenle yöntem 1'i kullanamıyorsanız manuel doğrulama kullanın. Denetleyicinin kodunu doğrulama mantığından ayırmak iyi bir uygulamadır. Doğrulama sınıflarınızı sıfırdan yaratmayın, Spring kullanışlı bir org.springframework.validation.Validatorarayüz sağlar (2. Bahardan beri).

Diyelim ki var

public class User {

    private String name;

    private Integer birthYear;
    private User responsibleUser;
    ...

}

ve "karmaşık" bir doğrulama yapmak istersiniz: kullanıcının yaşı 18'in altındaysa, sorumluKullanıcı boş olmamalı ve sorumluKullanıcı yaşı 21 yaşından büyük olmalıdır.

Böyle bir şey yapacaksın

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

    }

}

Sonra kumandanızda:

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

Doğrulama hataları varsa, sonuç.hasErrors () doğru olacaktır.

Not: Doğrulayıcıyı denetleyicinin @InitBinder yönteminde "binder.setValidator (...)" ile de ayarlayabilirsiniz (bu durumda yöntem 1 ve 2'nin karma kullanımı mümkün olmaz, çünkü varsayılanı değiştirirsiniz) doğrulayıcı). Veya denetleyicinin varsayılan yapıcısında başlatabilirsiniz. Ya da kumandanıza enjekte ettiğiniz @ @ Bileşen / @ Servis UserValidator'ınız olsun: çok yararlı, çünkü çoğu validatör tektonludur + birim test alayları daha kolay hale gelir + validatörünüz diğer Spring bileşenlerini çağırabilir.

Yöntem 3: Neden her iki yöntemin birleşimini kullanmıyorsunuz? "Name" özelliği gibi basit şeyleri ek açıklamalarla doğrulayın (hızlı, özlü ve daha okunaklı). Doğrulayıcılar için ağır doğrulamaları saklayın (özel karmaşık doğrulama ek açıklamalarının kodlanması saatler alacaksa veya yalnızca ek açıklamaların kullanılması mümkün olmadığında). Bunu eski bir projede yaptım, hızlı ve kolay bir cazibe gibi çalıştı.

Uyarı: Kural dışı durum işleme için doğrulama işlemesinde hata yapmamalısınız . Ne zaman kullanılacağını bilmek için bu yayını okuyun .

Referanslar :


servlet.xml dosyamın bu yapılandırma için nelere sahip olması gerektiğini söyleyebilir misiniz? Hataları tekrar görünüme getirmek istiyorum
devdar

@dev_darin JSR-303 doğrulaması için yapılandırmayı mı kastediyorsunuz?
Jerome Dalbert

2
@dev_marin Doğrulama için, Bahar 3.x + 'da, "servlet.xml" veya "[servlet-adı] -servlet.xml dosyalarında özel bir şey yoktur. Yöntem 3'ü kullanırsanız uyarı: varsayılan olarak, her denetleyicinin bir JSR-303 doğrulayıcısına erişimi vardır, bu nedenle "setValidator" ile geçersiz kılmamaya dikkat edin. üstte, sadece başlatın ve kullanın veya enjekte edin (eğer bir Spring bileşeniyse) google ve Spring doc'ı kontrol ettikten sonra hala sorun yaşıyorsanız, yeni bir soru göndermelisiniz.
Jerome Dalbert

2
Yöntem 1 ve 2'nin karma kullanımı için, @InitBinder kullanmanın bir yolu vardır. "Binder.setValidator (...)" yerine "binder.addValidators (...)" kullanabilirsiniz
jasonfungsing

1
Yanılıyorsam beni düzeltin, ancak @InitBinder ek açıklamasını kullanırken doğrulamayı JSR-303 ek açıklamaları (Yöntem 1) ve özel doğrulama (Yöntem 2) ile karıştırabilirsiniz. Sadece binder.setValidator (userValidator) yerine binder.addValidators (userValidator) kullanın ve her iki Doğrulama yöntemi de etkili olacaktır.
SebastianRiemer

31

Kullanıcı girişini doğrulamanın iki yolu vardır: ek açıklamalar ve Spring'in Doğrulayıcı sınıfını devralarak. Basit durumlar için ek açıklamalar iyidir. Karmaşık doğrulamalara ihtiyacınız varsa (alanlar arası doğrulama gibi, örneğin "e-posta adresini doğrula" alanı) veya modeliniz uygulamanızda farklı kurallarla birden fazla yerde doğrulanıyorsa veya model nesnesine detaylandırmalar ekleyerek, Spring'in mirasa dayalı Validator'ı gitmenin yoludur. Her ikisine de örnek göstereceğim.

Gerçek doğrulama bölümü, kullandığınız doğrulama türünden bağımsız olarak aynıdır:

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

Ek açıklamalar kullanıyorsanız, Foosınıfınız şöyle görünebilir:

public class Foo {

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

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

    // getters, setters
}

Yukarıdaki javax.validation.constraintsek açıklamalar , ek açıklamalardır. Hazırda Bekletme modlarını da kullanabilirsiniz org.hibernate.validator.constraints, ancak Hazırda Bekletme modunu kullanıyormuşsunuz gibi görünmüyor.

Alternatif olarak, Spring'in Doğrulayıcısını uygularsanız, aşağıdaki gibi bir sınıf oluşturabilirsiniz:

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

Yukarıdaki doğrulayıcıyı kullanıyorsanız, doğrulayıcıyı Yay denetleyicisine de bağlamanız gerekir (ek açıklamalar kullanılıyorsa gerekli değildir):

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

Ayrıca bakınız Bahar dokümanlar .

Umarım yardımcı olur.


Spring Validator kullanırken pojo'yu kontrolörden ayarlayıp sonra doğrulamam gerekir mi?
devdar

Sorunu anladığımdan emin değilim. Denetleyici kod snippet'ini görürseniz, Bahar gönderilen formu otomatik olarak Fooişleyici yöntemindeki parametreye bağlar . Açıklayabilir misin?
stephen.hanson

Tamam ne diyorsun kullanıcı kullanıcı girişleri Denetleyici http isteği almak gönderir, oradan ne olur tüm kullanıcı parametreleri almak için request.getParameter () kullanmak sonra POJO değerleri ayarlamak olduğunu geçmek doğrulama nesnesine sınıf. Doğrulama sınıfı, varsa hataları içeren görünüme geri gönderir. Bu şekilde mi?
devdar

1
Böyle olur ama daha basit bir yol var ... JSP ve bir <form: form commandName = "user"> gönderimi kullanırsanız, veriler otomatik olarak denetleyicideki @ModelAttribute ("kullanıcı") kullanıcısına yerleştirilir yöntem. Dokümana
Jerome Dalbert

+1 çünkü @ModelAttribute kullanan ilk bulduğum örnek; O olmadan öğretici hiçbiri çalışır bulundu.
Riccardo Cossu

12

Jerome Dalbert'in güzel cevabını uzatmak istiyorum. Kendi ek açıklama doğrulayıcılarınızı JSR-303 biçiminde yazmak çok kolay buldum. "Tek alan" doğrulaması ile sınırlı değilsiniz. Yazım düzeyinde kendi ek açıklamanızı oluşturabilir ve karmaşık doğrulamaya sahip olabilirsiniz (aşağıdaki örneklere bakın). Bu şekilde tercih ederim çünkü Jerome gibi farklı doğrulama türlerini (Spring ve JSR-303) karıştırmam gerekmiyor. Ayrıca bu doğrulayıcılar "Bahar farkında" olduğundan kutudan @ Inject / @ Autowire kullanabilirsiniz.

Özel nesne doğrulama örneği:

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

Eşdeğer alan eşitliği örneği:

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
Ayrıca bir denetleyicinin genellikle bir doğrulayıcıya sahip olduğunu merak ediyordum ve burada birden fazla doğrulayıcıya sahip olabileceğinizi gördüm, ancak bir nesne için tanımlanmış bir doğrulama kümeniz varsa ancak nesne üzerinde önceden oluşturmak istediğiniz işlem farklıdır, örneğin bir belirli bir doğrulama kümesi kaydetmek gerekir ve bir güncelleme farklı bir doğrulama kümesi gerekir. Doğrulama sınıfını işleme dayalı tüm doğrulamayı alacak şekilde yapılandırmanın bir yolu var mı veya birden fazla doğrulayıcı mı kullanmanız gerekiyor?
devdar

1
Ayrıca, yöntemle ilgili bir ek açıklama doğrulamanız da olabilir. Dolayısıyla, sorunuzu anlarsam kendi "alan adı doğrulamanızı" oluşturabilirsiniz. Bunun için belirtmelisiniz ElementType.METHODiçinde @Target.
michal.kreuzman

Ne dediğini anlıyorum, beni daha net bir resim için bir örneğe yönlendirebilir misin?
devdar

4

Farklı yöntem işleyicileri için aynı hata işleme mantığına sahipseniz, aşağıdaki kod desenine sahip çok sayıda işleyici ile sonuçlanırsınız:

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

RESTful hizmetleri oluşturduğunuzu ve 400 Bad Requesther doğrulama hatası durumu için hata mesajlarıyla birlikte geri dönmek istediğinizi varsayalım . Ardından, hata işleme bölümü, doğrulama gerektiren her bir REST uç noktası için aynı olacaktır. Aynı mantığı her bir işleyicide tekrarlamak o kadar KURU ish değildir !

Bu sorunu çözmenin bir yolu, BindingResulther To-Be-Doğrulanmış fasulyeden hemen sonra bırakmaktır . Şimdi, işleyiciniz şöyle olurdu:

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

Bu şekilde, bağlı fasulye geçerli değilse, MethodArgumentNotValidExceptionBahar tarafından atılır. ControllerAdviceBu istisnayı işleyen aynı hata işleme mantığıyla tanımlayabilirsiniz :

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

Hala altında yatan yöntemi BindingResultkullanarak inceleyebilirsiniz .getBindingResultMethodArgumentNotValidException


1

Spring Mvc Validation'ın tam örneğini bulun

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

Bu fasulyeyi yapılandırma sınıfınıza koyun.

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

ve sonra kullanabilirsiniz

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

bir fasulyeyi manuel olarak doğrulamak için. Sonra tüm sonucu BindingResult alacak ve oradan alabilirsiniz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.