Spring MVC: Bagaimana cara melakukan validasi?

156

Saya ingin tahu apa cara terbersih dan terbaik untuk melakukan validasi formulir input pengguna. Saya telah melihat beberapa pengembang mengimplementasikan org.springframework.validation.Validator. Sebuah pertanyaan tentang itu: Saya melihatnya memvalidasi kelas. Apakah kelas harus diisi secara manual dengan nilai-nilai dari input pengguna, dan kemudian diteruskan ke validator?

Saya bingung tentang cara terbersih dan terbaik untuk memvalidasi input pengguna. Saya tahu tentang metode tradisional menggunakan request.getParameter()dan kemudian memeriksa secara manual nulls, tapi saya tidak ingin melakukan semua validasi di Controller. Beberapa saran bagus tentang bidang ini akan sangat dihargai. Saya tidak menggunakan Hibernate dalam aplikasi ini.

devdar
sumber

Jawaban:

322

Dengan Spring MVC, ada 3 cara berbeda untuk melakukan validasi: menggunakan anotasi, secara manual, atau campuran keduanya. Tidak ada "cara terbersih dan terbaik" unik untuk memvalidasi, tetapi mungkin ada satu yang lebih cocok dengan proyek / masalah / konteks Anda.

Mari Punya Pengguna:

public class User {

    private String name;

    ...

}

Metode 1: Jika Anda memiliki Spring 3.x + dan validasi sederhana untuk dilakukan, gunakan javax.validation.constraintsanotasi (juga dikenal sebagai anotasi JSR-303).

public class User {

    @NotNull
    private String name;

    ...

}

Anda akan membutuhkan penyedia JSR-303 di perpustakaan Anda, seperti Hibernate Validator yang merupakan implementasi referensi (perpustakaan ini tidak ada hubungannya dengan database dan pemetaan relasional, itu hanya validasi :-).

Kemudian di controller Anda, Anda akan memiliki sesuatu seperti:

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

Perhatikan @Valid: jika pengguna memiliki nama nol, result.hasErrors () akan benar.

Metode 2: Jika Anda memiliki validasi yang kompleks (seperti logika validasi bisnis besar, validasi bersyarat di berbagai bidang, dll.), Atau karena alasan tertentu Anda tidak dapat menggunakan metode 1, gunakan validasi manual. Ini adalah praktik yang baik untuk memisahkan kode pengontrol dari logika validasi. Jangan membuat kelas validasi Anda dari awal, Spring menyediakan org.springframework.validation.Validatorantarmuka praktis (sejak Spring 2).

Jadi katakanlah Anda punya

public class User {

    private String name;

    private Integer birthYear;
    private User responsibleUser;
    ...

}

dan Anda ingin melakukan validasi "kompleks" seperti: jika usia pengguna di bawah 18, pengguna yang bertanggung jawab tidak boleh nol dan usia pengguna yang bertanggung jawab harus di atas 21.

Anda akan melakukan sesuatu seperti ini

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

    }

}

Kemudian di controller Anda, Anda akan memiliki:

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

Jika ada kesalahan validasi, result.hasErrors () akan benar.

Catatan: Anda juga dapat mengatur validator dalam metode @InitBinder pada controller, dengan "binder.setValidator (...)" (dalam hal ini campuran penggunaan metode 1 dan 2 tidak akan mungkin, karena Anda mengganti default validator). Atau Anda bisa instantiate di konstruktor default controller. Atau memiliki @ Component / @ Service UserValidator yang Anda masukkan (@Autowired) di controller Anda: sangat berguna, karena sebagian besar validator singletons + mengejek unit test menjadi lebih mudah + validator Anda dapat memanggil komponen Spring lainnya.

Metode 3: Mengapa tidak menggunakan kombinasi kedua metode? Validasi hal-hal sederhana, seperti atribut "nama", dengan anotasi (cepat dilakukan, ringkas, dan lebih mudah dibaca). Pertahankan validasi berat untuk validator (saat itu akan memakan waktu berjam-jam untuk kode anotasi validasi kustom kompleks, atau hanya ketika itu tidak mungkin untuk menggunakan anotasi). Saya melakukan ini pada proyek sebelumnya, itu bekerja seperti pesona, cepat & mudah.

Peringatan: Anda tidak boleh salah mengartikan penanganan validasi untuk penanganan pengecualian . Baca posting ini untuk mengetahui kapan menggunakannya.

Referensi :

Jerome Dalbert
sumber
dapatkah Anda memberi tahu saya apa yang seharusnya dimiliki servlet.xml untuk konfigurasi ini. Saya ingin meneruskan kesalahan kembali ke tampilan
devdar
@dev_darin Maksud Anda konfigurasi untuk validasi JSR-303?
Jerome Dalbert
2
@dev_marin Untuk validasi, di Musim Semi 3.x +, tidak ada yang istimewa di "servlet.xml" atau "[servlet-name] -servlet.xml. Anda hanya perlu jar hibernate-validator di perpustakaan proyek Anda (atau melalui Maven). Itu saja, itu harus bekerja kemudian. Peringatan jika Anda menggunakan Metode 3: secara default, setiap controller memiliki akses ke validator JSR-303, jadi berhati-hatilah untuk tidak menimpanya dengan "setValidator". Jika Anda ingin menambahkan validator khusus pada atas, cukup instantiate dan gunakan, atau suntikkan (jika itu adalah komponen Spring). Jika Anda masih memiliki masalah setelah memeriksa google dan Spring doc, Anda harus memposting pertanyaan baru.
Jerome Dalbert
2
Untuk penggunaan campuran metode 1 & 2, ada cara untuk menggunakan @InitBinder. Alih-alih "binder.setValidator (...)", dapat menggunakan "binder.addValidators (...)"
jasonfungsing
1
Perbaiki saya jika saya salah, tetapi Anda dapat mencampur validasi melalui anotasi JSR-303 (Metode 1) dan validasi khusus (Metode 2) saat menggunakan anotasi @InitBinder. Cukup gunakan binder.addValidators (userValidator) alih-alih binder.setValidator (userValidator) dan kedua metode Validasi akan berlaku.
SebastianRiemer
31

Ada dua cara untuk memvalidasi input pengguna: anotasi dan dengan mewarisi kelas Validator Musim Semi. Untuk kasus-kasus sederhana, penjelasannya bagus. Jika Anda memerlukan validasi kompleks (seperti validasi lintas-bidang, mis. Bidang "verifikasi alamat email"), atau jika model Anda divalidasi di banyak tempat dalam aplikasi Anda dengan aturan yang berbeda, atau jika Anda tidak memiliki kemampuan untuk memodifikasi memodelkan objek dengan menempatkan anotasi di atasnya, Validator berbasis warisan Spring adalah cara untuk pergi. Saya akan menunjukkan contoh keduanya.

Bagian validasi yang sebenarnya adalah sama, apa pun jenis validasinya yang Anda gunakan:

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

Jika Anda menggunakan anotasi, Fookelas Anda mungkin terlihat seperti:

public class Foo {

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

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

    // getters, setters
}

Anotasi di atas adalah javax.validation.constraintsanotasi. Anda juga dapat menggunakan Hibernate org.hibernate.validator.constraints, tetapi sepertinya Anda tidak menggunakan Hibernate.

Atau, jika Anda menerapkan Validator Musim Semi, Anda akan membuat kelas sebagai berikut:

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

Jika menggunakan validator di atas, Anda juga harus mengikat validator ke controller Spring (tidak perlu jika menggunakan anotasi):

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

Lihat juga dokumen Spring .

Semoga itu bisa membantu.

stephen.hanson
sumber
ketika menggunakan Spring's Validator saya harus mengatur pojo dari controller dan kemudian memvalidasinya
devdar
Saya tidak yakin saya mengerti pertanyaan Anda. Jika Anda melihat potongan kode pengontrol, Spring secara otomatis mengikat formulir yang dikirimkan ke Fooparameter dalam metode handler. Bisakah Anda mengklarifikasi?
stephen.hanson
ok apa yang saya katakan adalah ketika pengguna mengirimkan input pengguna Controller mendapatkan permintaan http, dari sana yang terjadi adalah Anda menggunakan request.getParameter () untuk mendapatkan semua parameter pengguna kemudian mengatur nilai-nilai dalam POJO kemudian lulus kelas ke objek validasi. Kelas validasi akan mengirim kesalahan kembali ke tampilan dengan kesalahan jika ada yang ditemukan. Apakah ini yang terjadi?
devdar
1
Itu akan terjadi seperti ini tetapi ada cara yang lebih sederhana ... Jika Anda menggunakan JSP dan pengiriman <form: form commandName = "user">, data secara otomatis dimasukkan ke pengguna @ModelAttribute ("user") di controller metode. Lihat dokumen: static.springsource.org/spring/docs/3.0.x/...
Jerome Dalbert
+1 karena itulah contoh pertama yang saya temukan yang menggunakan @ModelAttribute; tanpa itu tidak ada tutorial yang saya temukan berhasil.
Riccardo Cossu
12

Saya ingin menyampaikan jawaban yang bagus dari Jerome Dalbert. Saya menemukan sangat mudah untuk menulis validator anotasi Anda sendiri dengan cara JSR-303. Anda tidak dibatasi untuk memiliki validasi "satu bidang". Anda dapat membuat anotasi Anda sendiri pada level tipe dan memiliki validasi yang kompleks (lihat contoh di bawah). Saya lebih suka cara ini karena saya tidak perlu mencampur berbagai jenis validasi (Spring dan JSR-303) seperti yang dilakukan Jerome. Validator ini juga "Pegas sadar" sehingga Anda dapat menggunakan @ Suntikkan / @ Autowire di luar kotak.

Contoh validasi objek khusus:

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

Contoh persamaan bidang generik:

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;

}
michal.kreuzman
sumber
1
Saya juga bertanya-tanya controller biasanya memiliki satu validator dan saya melihat di mana Anda dapat memiliki beberapa validator namun jika Anda memiliki satu set validasi yang ditentukan untuk satu objek namun operasi yang ingin Anda lakukan sebelumnya pada objek berbeda misalnya menyimpan / memperbarui untuk menyimpan set validasi tertentu diperlukan dan pembaruan set validasi yang berbeda diperlukan. Apakah ada cara untuk mengonfigurasi kelas validator untuk menampung semua validasi berdasarkan operasi atau akankah Anda diharuskan menggunakan beberapa validator?
devdar
1
Anda juga dapat memiliki validasi anotasi pada metode. Jadi, Anda dapat membuat "validasi domain" sendiri jika saya memahami pertanyaan Anda. Untuk ini, Anda harus menentukan ElementType.METHODdi @Target.
michal.kreuzman
saya mengerti apa yang Anda katakan dapatkah Anda juga mengarahkan saya ke contoh untuk gambar yang lebih jelas.
devdar
4

Jika Anda memiliki logika penanganan kesalahan yang sama untuk penangan metode yang berbeda, maka Anda akan berakhir dengan banyak penangan dengan pola kode berikut:

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

Misalkan Anda membuat layanan RESTful dan ingin kembali 400 Bad Requestbersama dengan pesan kesalahan untuk setiap kasus kesalahan validasi. Kemudian, bagian penanganan kesalahan akan sama untuk setiap titik akhir REST tunggal yang membutuhkan validasi. Mengulangi logika yang sama di setiap handler tidak terlalu kering !

Salah satu cara untuk mengatasi masalah ini adalah dengan menjatuhkan segera BindingResultsetelah setiap kacang To-Be-Validated . Sekarang, pawang Anda akan seperti ini:

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

Dengan cara ini, jika kacang yang diikat tidak valid, MethodArgumentNotValidExceptionakan dibuang oleh Spring. Anda bisa mendefinisikan ControllerAdviceyang menangani pengecualian ini dengan logika penanganan kesalahan yang sama:

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

Anda masih dapat memeriksa metode BindingResultmenggunakan yang mendasarinya .getBindingResultMethodArgumentNotValidException

Ali Dehghani
sumber
1

Temukan contoh lengkap 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);
    }
}
Vicky
sumber
0

Masukkan kacang ini di kelas konfigurasi Anda.

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

dan kemudian Anda dapat menggunakan

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

untuk memvalidasi kacang secara manual. Kemudian Anda akan mendapatkan semua hasil dalam BindingResult dan Anda dapat mengambilnya dari sana.

Praveen Jain
sumber