Validasi lintas bidang dengan Hibernate Validator (JSR 303)


236

Apakah ada implementasi (atau implementasi pihak ketiga untuk) validasi lintas bidang di Hibernate Validator 4.x? Jika tidak, apa cara terbersih untuk menerapkan validator lintas bidang?

Sebagai contoh, bagaimana Anda dapat menggunakan API untuk memvalidasi dua properti kacang sama (seperti memvalidasi bidang kata sandi cocok dengan bidang verifikasi kata sandi).

Dalam anotasi, saya mengharapkan sesuatu seperti:

public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  @Equals(property="pass")
  private String passVerify;
}

1
Lihat stackoverflow.com/questions/2781771/… untuk solusi tipe-safe dan refleksi API (lebih elegan) di tingkat kelas.
Karl Richter

Jawaban:


282

Setiap kendala bidang harus ditangani oleh anotasi validator yang berbeda, atau dengan kata lain tidak disarankan untuk melakukan pemeriksaan anotasi validasi satu bidang terhadap bidang lain; validasi lintas-bidang harus dilakukan di tingkat kelas. Selain itu, JSR-303 Bagian 2.2 cara yang disukai untuk mengekspresikan beberapa validasi dari jenis yang sama adalah melalui daftar anotasi. Ini memungkinkan pesan kesalahan ditentukan per pertandingan.

Misalnya, memvalidasi bentuk umum:

@FieldMatch.List({
        @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
        @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")
})
public class UserRegistrationForm  {
    @NotNull
    @Size(min=8, max=25)
    private String password;

    @NotNull
    @Size(min=8, max=25)
    private String confirmPassword;

    @NotNull
    @Email
    private String email;

    @NotNull
    @Email
    private String confirmEmail;
}

Anotasi:

package constraints;

import constraints.impl.FieldMatchValidator;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Target;

/**
 * Validation annotation to validate that 2 fields have the same value.
 * An array of fields and their matching confirmation fields can be supplied.
 *
 * Example, compare 1 pair of fields:
 * @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match")
 * 
 * Example, compare more than 1 pair of fields:
 * @FieldMatch.List({
 *   @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
 *   @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")})
 */
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy = FieldMatchValidator.class)
@Documented
public @interface FieldMatch
{
    String message() default "{constraints.fieldmatch}";

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

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

    /**
     * @return The first field
     */
    String first();

    /**
     * @return The second field
     */
    String second();

    /**
     * Defines several <code>@FieldMatch</code> annotations on the same element
     *
     * @see FieldMatch
     */
    @Target({TYPE, ANNOTATION_TYPE})
    @Retention(RUNTIME)
    @Documented
            @interface List
    {
        FieldMatch[] value();
    }
}

Validator:

package constraints.impl;

import constraints.FieldMatch;
import org.apache.commons.beanutils.BeanUtils;

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

public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object>
{
    private String firstFieldName;
    private String secondFieldName;

    @Override
    public void initialize(final FieldMatch constraintAnnotation)
    {
        firstFieldName = constraintAnnotation.first();
        secondFieldName = constraintAnnotation.second();
    }

    @Override
    public boolean isValid(final Object value, final ConstraintValidatorContext context)
    {
        try
        {
            final Object firstObj = BeanUtils.getProperty(value, firstFieldName);
            final Object secondObj = BeanUtils.getProperty(value, secondFieldName);

            return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
        }
        catch (final Exception ignore)
        {
            // ignore
        }
        return true;
    }
}

8
@AndyT: Ada ketergantungan eksternal pada Apache Commons BeanUtils.
GaryF

7
@ScriptAssert tidak memungkinkan Anda membuat pesan validasi dengan jalur yang disesuaikan. context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addNode(secondFieldName).addConstraintViolation().disableDefaultConstraintViolation(); Memberi kemungkinan menyoroti bidang yang benar (jika JSF mendukungnya).
Peter Davis

8
saya menggunakan contoh di atas tetapi tidak menampilkan pesan kesalahan, apa yang mengikat harus di jsp? saya memiliki ikatan untuk kata sandi dan hanya mengonfirmasi, apakah ada hal lain yang diperlukan? <form: password path = "password" /> <form: errors path = "password" cssClass = "errorz" /> <form: path password = "confirmPassword" /> <form: errors path = "confirmPassword" cssClass = " errorz "/>
Mahmoud Saleh

7
BeanUtils.getPropertymengembalikan sebuah string. Contohnya mungkin dimaksudkan untuk menggunakan PropertyUtils.getPropertyyang mengembalikan objek.
SingleShot

2
Jawaban yang bagus, tapi saya sudah menyelesaikannya dengan jawaban untuk pertanyaan ini: stackoverflow.com/questions/11890334/…
maxivis

164

Saya menyarankan Anda solusi lain yang mungkin. Mungkin kurang elegan, tetapi lebih mudah!

public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;

  @AssertTrue(message="passVerify field should be equal than pass field")
  private boolean isValid() {
    return this.pass.equals(this.passVerify);
  }
}

Itu isValid Metode dipanggil oleh validator secara otomatis.


12
Saya pikir ini adalah campuran kekhawatiran lagi. Inti dari Validasi Bean adalah mengeksternalisasi validasi ke dalam ConstraintValidators. Dalam hal ini Anda memiliki bagian dari logika validasi dalam kacang itu sendiri dan bagian dalam kerangka kerja Validator. Cara untuk pergi adalah batasan tingkat kelas. Hibernate Validator juga sekarang menawarkan @ScriptAssert yang membuat implementasi dependensi internal bean lebih mudah.
Hardy

10
Saya akan mengatakan ini lebih elegan, tidak kurang!
NickJ

8
Pendapat saya sejauh ini adalah bahwa Validasi Bean JSR adalah campuran dari keprihatinan.
Dmitry Minkovsky

3
@ GaneshKrishnan Bagaimana jika kita ingin memiliki beberapa @AssertTruemetode seperti itu ? Beberapa konvensi penamaan berlaku?
Stephane

3
mengapa ini bukan jawaban terbaik
funky-nd

32

Saya terkejut ini tidak tersedia di luar kotak. Bagaimanapun, inilah solusi yang mungkin.

Saya telah membuat validator tingkat kelas, bukan level bidang seperti yang dijelaskan dalam pertanyaan awal.

Berikut adalah kode penjelasannya:

package com.moa.podium.util.constraints;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

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 = MatchesValidator.class)
@Documented
public @interface Matches {

  String message() default "{com.moa.podium.util.constraints.matches}";

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

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

  String field();

  String verifyField();
}

Dan validator itu sendiri:

package com.moa.podium.util.constraints;

import org.mvel2.MVEL;

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

public class MatchesValidator implements ConstraintValidator<Matches, Object> {

  private String field;
  private String verifyField;


  public void initialize(Matches constraintAnnotation) {
    this.field = constraintAnnotation.field();
    this.verifyField = constraintAnnotation.verifyField();
  }

  public boolean isValid(Object value, ConstraintValidatorContext context) {
    Object fieldObj = MVEL.getProperty(field, value);
    Object verifyFieldObj = MVEL.getProperty(verifyField, value);

    boolean neitherSet = (fieldObj == null) && (verifyFieldObj == null);

    if (neitherSet) {
      return true;
    }

    boolean matches = (fieldObj != null) && fieldObj.equals(verifyFieldObj);

    if (!matches) {
      context.disableDefaultConstraintViolation();
      context.buildConstraintViolationWithTemplate("message")
          .addNode(verifyField)
          .addConstraintViolation();
    }

    return matches;
  }
}

Perhatikan bahwa saya telah menggunakan MVEL untuk memeriksa properti objek yang sedang divalidasi. Ini bisa diganti dengan API refleksi standar atau jika itu adalah kelas khusus yang Anda validasi, metode accessor itu sendiri.

Anotasi @Matches kemudian dapat digunakan digunakan pada kacang sebagai berikut:

@Matches(field="pass", verifyField="passRepeat")
public class AccountCreateForm {

  @Size(min=6, max=50)
  private String pass;
  private String passRepeat;

  ...
}

Sebagai penafian, saya menulis ini dalam 5 menit terakhir, jadi saya mungkin belum menyelesaikan semua bug. Saya akan memperbarui jawabannya jika ada masalah.


1
Ini bagus dan ini berfungsi untuk saya, kecuali bahwa addNote sudah usang dan saya mendapatkan AbstractMethodError jika saya menggunakan addPropertyNode sebagai gantinya. Google tidak membantu saya di sini. Apa solusinya? Apakah ada ketergantungan yang hilang di suatu tempat?
Paul Grenyer

29

Dengan Hibernate Validator 4.1.0. Terakhir saya sarankan menggunakan @ScriptAssert . Perkecualian dari JavaDoc-nya:

Ekspresi skrip dapat ditulis dalam bahasa scripting atau ekspresi apa pun, yang mesin JSR 223 ("Scripting untuk Platform JavaTM") dapat ditemukan di classpath.

Catatan: evaluasi sedang dilakukan oleh " mesin " scripting yang berjalan di Java VM, oleh karena itu pada Java "sisi server", tidak pada "sisi klien" seperti yang dinyatakan dalam beberapa komentar.

Contoh:

@ScriptAssert(lang = "javascript", script = "_this.passVerify.equals(_this.pass)")
public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;
}

atau dengan alias lebih pendek dan nol-aman:

@ScriptAssert(lang = "javascript", alias = "_",
    script = "_.passVerify != null && _.passVerify.equals(_.pass)")
public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;
}

atau dengan Java 7+ null-safe Objects.equals():

@ScriptAssert(lang = "javascript", script = "Objects.equals(_this.passVerify, _this.pass)")
public class MyBean {
  @Size(min=6, max=50)
  private String pass;

  private String passVerify;
}

Meskipun demikian, tidak ada yang salah dengan solusi @Matches validator tingkat kelas kustom .


1
Solusi yang menarik, apakah kita benar-benar menggunakan javascript di sini untuk menyelesaikan validasi ini? Itu sepertinya berlebihan untuk apa yang harus dicapai oleh anotasi berbasis java. Untuk mata saya perawan solusi oleh Nicko diusulkan di atas masih tampak lebih bersih baik dari sudut pandang kegunaan (penjelasannya mudah dibaca dan cukup fungsional vs javascript-> referensi java), dan dari sudut pandang skalabilitas (saya menganggap ada overhead yang masuk akal untuk menangani javascript, tapi mungkin Hibernate melakukan caching setidaknya kode yang dikompilasi?). Saya ingin tahu mengapa ini lebih disukai.
David Parks

2
Saya setuju bahwa implementasi Nicko bagus, tapi saya tidak melihat ada yang keberatan menggunakan JS sebagai bahasa ekspresi. Java 6 menyertakan Badak untuk aplikasi yang persis seperti itu. Saya suka @ScriptAssert karena hanya berfungsi tanpa harus membuat anotasi dan validator setiap kali saya melakukan tes jenis novel.

4
Seperti yang dikatakan, tidak ada yang salah dengan validator tingkat kelas. ScriptAssert hanyalah alternatif yang tidak mengharuskan Anda untuk menulis kode khusus. Saya tidak mengatakan bahwa itu adalah solusi yang disukai ;-)
Hardy

Jawaban yang bagus karena konfirmasi kata sandi bukan validasi kritis karena itu dapat dilakukan di sisi klien
peterchaula

19

Validasi lintas bidang dapat dilakukan dengan membuat batasan khusus.

Contoh: - Bandingkan kata sandi dan bidang konfirmasiPassword instance Pengguna.

Membandingkan Strings

@Target({TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy=CompareStringsValidator.class)
@Documented
public @interface CompareStrings {
    String[] propertyNames();
    StringComparisonMode matchMode() default EQUAL;
    boolean allowNull() default false;
    String message() default "";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

StringComparisonMode

public enum StringComparisonMode {
    EQUAL, EQUAL_IGNORE_CASE, NOT_EQUAL, NOT_EQUAL_IGNORE_CASE
}

CompareStringsValidator

public class CompareStringsValidator implements ConstraintValidator<CompareStrings, Object> {

    private String[] propertyNames;
    private StringComparisonMode comparisonMode;
    private boolean allowNull;

    @Override
    public void initialize(CompareStrings constraintAnnotation) {
        this.propertyNames = constraintAnnotation.propertyNames();
        this.comparisonMode = constraintAnnotation.matchMode();
        this.allowNull = constraintAnnotation.allowNull();
    }

    @Override
    public boolean isValid(Object target, ConstraintValidatorContext context) {
        boolean isValid = true;
        List<String> propertyValues = new ArrayList<String> (propertyNames.length);
        for(int i=0; i<propertyNames.length; i++) {
            String propertyValue = ConstraintValidatorHelper.getPropertyValue(String.class, propertyNames[i], target);
            if(propertyValue == null) {
                if(!allowNull) {
                    isValid = false;
                    break;
                }
            } else {
                propertyValues.add(propertyValue);
            }
        }

        if(isValid) {
            isValid = ConstraintValidatorHelper.isValid(propertyValues, comparisonMode);
        }

        if (!isValid) {
          /*
           * if custom message was provided, don't touch it, otherwise build the
           * default message
           */
          String message = context.getDefaultConstraintMessageTemplate();
          message = (message.isEmpty()) ?  ConstraintValidatorHelper.resolveMessage(propertyNames, comparisonMode) : message;

          context.disableDefaultConstraintViolation();
          ConstraintViolationBuilder violationBuilder = context.buildConstraintViolationWithTemplate(message);
          for (String propertyName : propertyNames) {
            NodeBuilderDefinedContext nbdc = violationBuilder.addNode(propertyName);
            nbdc.addConstraintViolation();
          }
        }    

        return isValid;
    }
}

ConstraintValidatorHelper

public abstract class ConstraintValidatorHelper {

public static <T> T getPropertyValue(Class<T> requiredType, String propertyName, Object instance) {
        if(requiredType == null) {
            throw new IllegalArgumentException("Invalid argument. requiredType must NOT be null!");
        }
        if(propertyName == null) {
            throw new IllegalArgumentException("Invalid argument. PropertyName must NOT be null!");
        }
        if(instance == null) {
            throw new IllegalArgumentException("Invalid argument. Object instance must NOT be null!");
        }
        T returnValue = null;
        try {
            PropertyDescriptor descriptor = new PropertyDescriptor(propertyName, instance.getClass());
            Method readMethod = descriptor.getReadMethod();
            if(readMethod == null) {
                throw new IllegalStateException("Property '" + propertyName + "' of " + instance.getClass().getName() + " is NOT readable!");
            }
            if(requiredType.isAssignableFrom(readMethod.getReturnType())) {
                try {
                    Object propertyValue = readMethod.invoke(instance);
                    returnValue = requiredType.cast(propertyValue);
                } catch (Exception e) {
                    e.printStackTrace(); // unable to invoke readMethod
                }
            }
        } catch (IntrospectionException e) {
            throw new IllegalArgumentException("Property '" + propertyName + "' is NOT defined in " + instance.getClass().getName() + "!", e);
        }
        return returnValue; 
    }

    public static boolean isValid(Collection<String> propertyValues, StringComparisonMode comparisonMode) {
        boolean ignoreCase = false;
        switch (comparisonMode) {
        case EQUAL_IGNORE_CASE:
        case NOT_EQUAL_IGNORE_CASE:
            ignoreCase = true;
        }

        List<String> values = new ArrayList<String> (propertyValues.size());
        for(String propertyValue : propertyValues) {
            if(ignoreCase) {
                values.add(propertyValue.toLowerCase());
            } else {
                values.add(propertyValue);
            }
        }

        switch (comparisonMode) {
        case EQUAL:
        case EQUAL_IGNORE_CASE:
            Set<String> uniqueValues = new HashSet<String> (values);
            return uniqueValues.size() == 1 ? true : false;
        case NOT_EQUAL:
        case NOT_EQUAL_IGNORE_CASE:
            Set<String> allValues = new HashSet<String> (values);
            return allValues.size() == values.size() ? true : false;
        }

        return true;
    }

    public static String resolveMessage(String[] propertyNames, StringComparisonMode comparisonMode) {
        StringBuffer buffer = concatPropertyNames(propertyNames);
        buffer.append(" must");
        switch(comparisonMode) {
        case EQUAL:
        case EQUAL_IGNORE_CASE:
            buffer.append(" be equal");
            break;
        case NOT_EQUAL:
        case NOT_EQUAL_IGNORE_CASE:
            buffer.append(" not be equal");
            break;
        }
        buffer.append('.');
        return buffer.toString();
    }

    private static StringBuffer concatPropertyNames(String[] propertyNames) {
        //TODO improve concating algorithm
        StringBuffer buffer = new StringBuffer();
        buffer.append('[');
        for(String propertyName : propertyNames) {
            char firstChar = Character.toUpperCase(propertyName.charAt(0));
            buffer.append(firstChar);
            buffer.append(propertyName.substring(1));
            buffer.append(", ");
        }
        buffer.delete(buffer.length()-2, buffer.length());
        buffer.append("]");
        return buffer;
    }
}

Pengguna

@CompareStrings(propertyNames={"password", "confirmPassword"})
public class User {
    private String password;
    private String confirmPassword;

    public String getPassword() { return password; }
    public void setPassword(String password) { this.password = password; }
    public String getConfirmPassword() { return confirmPassword; }
    public void setConfirmPassword(String confirmPassword) { this.confirmPassword =  confirmPassword; }
}

Uji

    public void test() {
        User user = new User();
        user.setPassword("password");
        user.setConfirmPassword("paSSword");
        Set<ConstraintViolation<User>> violations = beanValidator.validate(user);
        for(ConstraintViolation<User> violation : violations) {
            logger.debug("Message:- " + violation.getMessage());
        }
        Assert.assertEquals(violations.size(), 1);
    }

Keluaran Message:- [Password, ConfirmPassword] must be equal.

Dengan menggunakan batasan validasi CompareStrings, kami juga dapat membandingkan lebih dari dua properti dan kami dapat mencampur salah satu dari empat metode perbandingan string.

Pilihan warna

@CompareStrings(propertyNames={"color1", "color2", "color3"}, matchMode=StringComparisonMode.NOT_EQUAL, message="Please choose three different colors.")
public class ColorChoice {

    private String color1;
    private String color2;
    private String color3;
        ......
}

Uji

ColorChoice colorChoice = new ColorChoice();
        colorChoice.setColor1("black");
        colorChoice.setColor2("white");
        colorChoice.setColor3("white");
        Set<ConstraintViolation<ColorChoice>> colorChoiceviolations = beanValidator.validate(colorChoice);
        for(ConstraintViolation<ColorChoice> violation : colorChoiceviolations) {
            logger.debug("Message:- " + violation.getMessage());
        }

Keluaran Message:- Please choose three different colors.

Demikian pula, kita dapat memiliki CompareNumber, CompareDates, dll, validasi lintas-bidang lintas.

PS Saya belum menguji kode ini di bawah lingkungan produksi (meskipun saya mengujinya di bawah lingkungan dev), jadi pertimbangkan kode ini sebagai Milestone Release. Jika Anda menemukan bug, silakan tulis komentar yang bagus. :)


Saya suka pendekatan ini, karena lebih fleksibel daripada yang lain. Ini memungkinkan saya memvalidasi lebih dari 2 bidang untuk kesetaraan. Pekerjaan yang baik!
Tauren

9

Saya telah mencoba contoh Alberthoven (hibernate-validator 4.0.2.GA) dan saya mendapatkan ValidationException: "Metode beranotasi harus mengikuti konvensi penamaan JavaBeans. match () tidak. "juga. Setelah saya mengganti nama metode dari "cocokkan" menjadi "isValid" itu berhasil.

public class Password {

    private String password;

    private String retypedPassword;

    public Password(String password, String retypedPassword) {
        super();
        this.password = password;
        this.retypedPassword = retypedPassword;
    }

    @AssertTrue(message="password should match retyped password")
    private boolean isValid(){
        if (password == null) {
            return retypedPassword == null;
        } else {
            return password.equals(retypedPassword);
        }
    }

    public String getPassword() {
        return password;
    }

    public String getRetypedPassword() {
        return retypedPassword;
    }

}

Ini berfungsi dengan baik untuk saya tetapi tidak menampilkan pesan kesalahan. Apakah itu berhasil dan menampilkan pesan kesalahan untuk Anda. Bagaimana?
Tiny

1
@Tiny: Pesan harus berupa pelanggaran yang dikembalikan oleh validator. (Tulis uji Unit: stackoverflow.com/questions/5704743/… ). NAMUN pesan validasi milik Properti "isValid". Oleh karena itu pesan hanya akan ditampilkan di GUI jika GUI menunjukkan masalah untuk retypedPassword AND isValid (di sebelah retyped Password).
Ralph

8

Jika Anda menggunakan Spring Framework maka Anda bisa menggunakan Spring Expression Language (SpEL) untuk itu. Saya telah menulis perpustakaan kecil yang menyediakan validator JSR-303 berdasarkan SpEL - membuat validasi lintas-bidang menjadi mudah! Lihatlah https://github.com/jirutka/validator-spring .

Ini akan memvalidasi panjang dan kesetaraan bidang kata sandi.

@SpELAssert(value = "pass.equals(passVerify)",
            message = "{validator.passwords_not_same}")
public class MyBean {

    @Size(min = 6, max = 50)
    private String pass;

    private String passVerify;
}

Anda juga dapat dengan mudah memodifikasi ini untuk memvalidasi bidang kata sandi hanya ketika tidak keduanya kosong.

@SpELAssert(value = "pass.equals(passVerify)",
            applyIf = "pass || passVerify",
            message = "{validator.passwords_not_same}")
public class MyBean {

    @Size(min = 6, max = 50)
    private String pass;

    private String passVerify;
}

4

Saya suka ide dari Jakub Jirutka untuk menggunakan Spring Expression Language. Jika Anda tidak ingin menambahkan pustaka / dependensi lain (dengan asumsi bahwa Anda sudah menggunakan Spring), berikut ini adalah implementasi ide yang disederhanakan.

Kendala:

@Constraint(validatedBy=ExpressionAssertValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ExpressionAssert {
    String message() default "expression must evaluate to true";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
    String value();
}

Validator:

public class ExpressionAssertValidator implements ConstraintValidator<ExpressionAssert, Object> {
    private Expression exp;

    public void initialize(ExpressionAssert annotation) {
        ExpressionParser parser = new SpelExpressionParser();
        exp = parser.parseExpression(annotation.value());
    }

    public boolean isValid(Object value, ConstraintValidatorContext context) {
        return exp.getValue(value, Boolean.class);
    }
}

Terapkan seperti ini:

@ExpressionAssert(value="pass == passVerify", message="passwords must be same")
public class MyBean {
    @Size(min=6, max=50)
    private String pass;
    private String passVerify;
}

3

Saya tidak memiliki reputasi untuk mengomentari jawaban pertama tetapi ingin menambahkan bahwa saya telah menambahkan unit test untuk jawaban yang menang dan memiliki pengamatan berikut:

  • Jika Anda mendapatkan nama depan atau nama bidang yang salah, maka Anda mendapatkan kesalahan validasi seolah-olah nilainya tidak cocok. Jangan tersandung oleh kesalahan ejaan misalnya

@FieldMatch (first = " FieldName1" tidak valid , kedua = "validFieldName2")

  • Validator akan menerima tipe data yang setara yaitu semua ini akan lulus dengan FieldMatch:

private String stringField = "1";

private Integer integerField = Integer baru (1)

private int intield = 1;

  • Jika bidang adalah tipe objek yang tidak menerapkan sama dengan, validasi akan gagal.

2

Solusi bradhouse yang sangat bagus. Apakah ada cara untuk menerapkan anotasi @Matches ke lebih dari satu bidang?

EDIT: Inilah solusi yang saya buat untuk menjawab pertanyaan ini, saya memodifikasi Kendala untuk menerima array, bukan nilai tunggal:

@Matches(fields={"password", "email"}, verifyFields={"confirmPassword", "confirmEmail"})
public class UserRegistrationForm  {

    @NotNull
    @Size(min=8, max=25)
    private String password;

    @NotNull
    @Size(min=8, max=25)
    private String confirmPassword;


    @NotNull
    @Email
    private String email;

    @NotNull
    @Email
    private String confirmEmail;
}

Kode untuk anotasi:

package springapp.util.constraints;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

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 = MatchesValidator.class)
@Documented
public @interface Matches {

  String message() default "{springapp.util.constraints.matches}";

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

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

  String[] fields();

  String[] verifyFields();
}

Dan implementasinya:

package springapp.util.constraints;

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

import org.apache.commons.beanutils.BeanUtils;

public class MatchesValidator implements ConstraintValidator<Matches, Object> {

    private String[] fields;
    private String[] verifyFields;

    public void initialize(Matches constraintAnnotation) {
        fields = constraintAnnotation.fields();
        verifyFields = constraintAnnotation.verifyFields();
    }

    public boolean isValid(Object value, ConstraintValidatorContext context) {

        boolean matches = true;

        for (int i=0; i<fields.length; i++) {
            Object fieldObj, verifyFieldObj;
            try {
                fieldObj = BeanUtils.getProperty(value, fields[i]);
                verifyFieldObj = BeanUtils.getProperty(value, verifyFields[i]);
            } catch (Exception e) {
                //ignore
                continue;
            }
            boolean neitherSet = (fieldObj == null) && (verifyFieldObj == null);
            if (neitherSet) {
                continue;
            }

            boolean tempMatches = (fieldObj != null) && fieldObj.equals(verifyFieldObj);

            if (!tempMatches) {
                addConstraintViolation(context, fields[i]+ " fields do not match", verifyFields[i]);
            }

            matches = matches?tempMatches:matches;
        }
        return matches;
    }

    private void addConstraintViolation(ConstraintValidatorContext context, String message, String field) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(message).addNode(field).addConstraintViolation();
    }
}

Hmm. Tidak yakin. Anda dapat mencoba membuat validator spesifik untuk setiap bidang konfirmasi (sehingga mereka memiliki anotasi berbeda), atau memperbarui anotasi @Matches untuk menerima beberapa pasangan bidang.
bradhouse

Terima kasih bradhouse, datang dengan solusi dan telah diposting di atas. Perlu sedikit kerja untuk memenuhi ketika perbedaan jumlah argumen dilewatkan sehingga Anda tidak mendapatkan IndexOutOfBoundsExceptions, tetapi dasar-dasarnya ada di sana.
McGin

1

Anda perlu menyebutnya secara eksplisit. Pada contoh di atas, bradhouse telah memberi Anda semua langkah untuk menulis batasan kustom.

Tambahkan kode ini di kelas pemanggil Anda.

ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();

Set<ConstraintViolation<yourObjectClass>> constraintViolations = validator.validate(yourObject);

dalam kasus di atas akan menjadi

Set<ConstraintViolation<AccountCreateForm>> constraintViolations = validator.validate(objAccountCreateForm);


1

Kalian luar biasa. Ide yang sangat menakjubkan. Saya paling suka Alberthoven dan McGin , jadi saya memutuskan untuk menggabungkan kedua ide. Dan kembangkan beberapa solusi umum untuk memenuhi semua kasus. Inilah solusi yang saya usulkan.

@Documented
@Constraint(validatedBy = NotFalseValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotFalse {


    String message() default "NotFalse";
    String[] messages();
    String[] properties();
    String[] verifiers();

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

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

}

public class NotFalseValidator implements ConstraintValidator<NotFalse, Object> {
    private String[] properties;
    private String[] messages;
    private String[] verifiers;
    @Override
    public void initialize(NotFalse flag) {
        properties = flag.properties();
        messages = flag.messages();
        verifiers = flag.verifiers();
    }

    @Override
    public boolean isValid(Object bean, ConstraintValidatorContext cxt) {
        if(bean == null) {
            return true;
        }

        boolean valid = true;
        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);

        for(int i = 0; i< properties.length; i++) {
           Boolean verified = (Boolean) beanWrapper.getPropertyValue(verifiers[i]);
           valid &= isValidProperty(verified,messages[i],properties[i],cxt);
        }

        return valid;
    }

    boolean isValidProperty(Boolean flag,String message, String property, ConstraintValidatorContext cxt) {
        if(flag == null || flag) {
            return true;
        } else {
            cxt.disableDefaultConstraintViolation();
            cxt.buildConstraintViolationWithTemplate(message)
                    .addPropertyNode(property)
                    .addConstraintViolation();
            return false;
        }

    }



}

@NotFalse(
        messages = {"End Date Before Start Date" , "Start Date Before End Date" } ,
        properties={"endDateTime" , "startDateTime"},
        verifiers = {"validDateRange" , "validDateRange"})
public class SyncSessionDTO implements ControllableNode {
    @NotEmpty @NotPastDate
    private Date startDateTime;

    @NotEmpty
    private Date endDateTime;



    public Date getStartDateTime() {
        return startDateTime;
    }

    public void setStartDateTime(Date startDateTime) {
        this.startDateTime = startDateTime;
    }

    public Date getEndDateTime() {
        return endDateTime;
    }

    public void setEndDateTime(Date endDateTime) {
        this.endDateTime = endDateTime;
    }


    public Boolean getValidDateRange(){
        if(startDateTime != null && endDateTime != null) {
            return startDateTime.getTime() <= endDateTime.getTime();
        }

        return null;
    }

}

0

Saya membuat adaptasi kecil dalam solusi Nicko sehingga tidak perlu menggunakan perpustakaan Apache Commons BeanUtils dan menggantinya dengan solusi yang sudah tersedia di musim semi, bagi mereka yang menggunakannya karena saya bisa lebih sederhana:

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;

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

public class FieldMatchValidator implements ConstraintValidator<FieldMatch, Object> {

    private String firstFieldName;
    private String secondFieldName;

    @Override
    public void initialize(final FieldMatch constraintAnnotation) {
        firstFieldName = constraintAnnotation.first();
        secondFieldName = constraintAnnotation.second();
    }

    @Override
    public boolean isValid(final Object object, final ConstraintValidatorContext context) {

        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(object);
        final Object firstObj = beanWrapper.getPropertyValue(firstFieldName);
        final Object secondObj = beanWrapper.getPropertyValue(secondFieldName);

        boolean isValid = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);

        if (!isValid) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
                .addPropertyNode(firstFieldName)
                .addConstraintViolation();
        }

        return isValid;

    }
}

-1

Solusi dikerjakan dengan pertanyaan: Cara mengakses bidang yang dijelaskan dalam properti anotasi

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Match {

    String field();

    String message() default "";
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MatchValidator.class)
@Documented
public @interface EnableMatchConstraint {

    String message() default "Fields must match!";

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

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

public class MatchValidator implements  ConstraintValidator<EnableMatchConstraint, Object> {

    @Override
    public void initialize(final EnableMatchConstraint constraint) {}

    @Override
    public boolean isValid(final Object o, final ConstraintValidatorContext context) {
        boolean result = true;
        try {
            String mainField, secondField, message;
            Object firstObj, secondObj;

            final Class<?> clazz = o.getClass();
            final Field[] fields = clazz.getDeclaredFields();

            for (Field field : fields) {
                if (field.isAnnotationPresent(Match.class)) {
                    mainField = field.getName();
                    secondField = field.getAnnotation(Match.class).field();
                    message = field.getAnnotation(Match.class).message();

                    if (message == null || "".equals(message))
                        message = "Fields " + mainField + " and " + secondField + " must match!";

                    firstObj = BeanUtils.getProperty(o, mainField);
                    secondObj = BeanUtils.getProperty(o, secondField);

                    result = firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
                    if (!result) {
                        context.disableDefaultConstraintViolation();
                        context.buildConstraintViolationWithTemplate(message).addPropertyNode(mainField).addConstraintViolation();
                        break;
                    }
                }
            }
        } catch (final Exception e) {
            // ignore
            //e.printStackTrace();
        }
        return result;
    }
}

Dan bagaimana cara menggunakannya ...? Seperti ini:

@Entity
@EnableMatchConstraint
public class User {

    @NotBlank
    private String password;

    @Match(field = "password")
    private String passwordConfirmation;
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.