Xác thực trường chéo với Trình xác thực Hibernate (JSR 303)


235

Có triển khai (hoặc triển khai của bên thứ ba cho) xác thực trường chéo trong Trình xác thực Hibernate 4.x không? Nếu không, cách sạch nhất để thực hiện trình xác nhận trường chéo là gì?

Ví dụ: làm thế nào bạn có thể sử dụng API để xác thực hai thuộc tính bean bằng nhau (chẳng hạn như xác thực trường mật khẩu khớp với trường xác minh mật khẩu).

Trong các chú thích, tôi mong đợi một cái gì đó như:

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

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

1
Xem stackoverflow.com/questions/2781771/ - để biết giải pháp API an toàn và không phản chiếu (imo thanh lịch hơn) ở cấp độ lớp.
Karl Richter

Câu trả lời:


282

Mỗi ràng buộc trường phải được xử lý bằng một chú thích trình xác nhận riêng biệt hoặc nói cách khác, không nên thực hiện kiểm tra chú thích xác thực của một trường đối với các trường khác; xác nhận trường chéo nên được thực hiện ở cấp lớp. Ngoài ra, JSR-303 Mục 2.2 cách ưa thích để thể hiện nhiều xác nhận hợp lệ cùng loại là thông qua một danh sách các chú thích. Điều này cho phép thông báo lỗi được chỉ định cho mỗi trận đấu.

Ví dụ: xác nhận một hình thức phổ biến:

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

Chú thích:

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

Trình xác nhận:

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: Có sự phụ thuộc bên ngoài vào BeanUtils của Apache Commons.
GaryF

7
@ScriptAssert không cho phép bạn tạo thông báo xác thực với đường dẫn tùy chỉnh. context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate()).addNode(secondFieldName).addConstraintViolation().disableDefaultConstraintViolation(); Cung cấp khả năng làm nổi bật đúng trường (nếu chỉ có JSF sẽ hỗ trợ nó).
Peter Davis

8
tôi đã sử dụng mẫu trên nhưng nó không hiển thị thông báo lỗi, ràng buộc nên có trong jsp là gì? tôi có ràng buộc cho mật khẩu và chỉ xác nhận, có cần thêm gì nữa không? <form: password path = "password" /> <form: error path = "password" cssClass = "errorz" /> <form: password path = "ConfirmPassword" /> <form: error path = "ConfirmPassword" cssClass = " errorz "/>
Mahmoud Saleh

7
BeanUtils.getPropertytrả về một chuỗi. Ví dụ có thể có nghĩa là sử dụng PropertyUtils.getPropertytrả về một đối tượng.
SingleShot

2
Câu trả lời hay, nhưng tôi đã hoàn thành nó với câu trả lời cho câu hỏi này: stackoverflow.com/questions/11890334/ Lời
maxivis

164

Tôi đề nghị bạn một giải pháp khả thi khác. Có lẽ ít thanh lịch, nhưng dễ dàng hơn!

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

Các isValidphương pháp được gọi bởi các validator tự động.


12
Tôi nghĩ rằng đây là một sự pha trộn của mối quan tâm một lần nữa. Toàn bộ quan điểm của Xác thực Bean là đưa ra ngoài xác thực vào ConstraintValidators. Trong trường hợp này, bạn có một phần logic xác thực trong chính bean và một phần trong khung Trình xác thực. Cách để đi là một ràng buộc cấp lớp. Trình xác thực Hibernate hiện cung cấp một @ScriptAssert, giúp cho việc thực hiện các phụ thuộc nội bộ của bean dễ dàng hơn.
Hardy

10
Tôi sẽ nói điều này là thanh lịch hơn , không phải ít hơn!
NickJ

8
Ý kiến ​​của tôi cho đến nay là Bean xác thực Bean là một sự pha trộn của các mối quan tâm.
Dmitry Minkovsky

3
@GaneshKrishnan Nếu chúng ta muốn có một vài @AssertTruephương thức như vậy thì sao? Một số quy ước đặt tên giữ?
Stephane

3
Tại sao đây không phải là câu trả lời hay nhất
funky-nd

32

Tôi ngạc nhiên điều này không có sẵn trong hộp. Dù sao, đây là một giải pháp có thể.

Tôi đã tạo trình xác nhận cấp lớp, không phải cấp trường như được mô tả trong câu hỏi ban đầu.

Đây là mã chú thích:

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

Và chính trình xác nhận:

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

Lưu ý rằng tôi đã sử dụng MVEL để kiểm tra các thuộc tính của đối tượng được xác thực. Điều này có thể được thay thế bằng các API phản chiếu tiêu chuẩn hoặc nếu đó là một lớp cụ thể mà bạn đang xác thực, chính các phương thức truy cập.

Chú thích @Matches sau đó có thể được sử dụng trên một bean như sau:

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

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

  ...
}

Từ chối trách nhiệm, tôi đã viết bài này trong 5 phút cuối, vì vậy tôi có lẽ vẫn chưa giải quyết được tất cả các lỗi. Tôi sẽ cập nhật câu trả lời nếu có gì sai.


1
Điều này thật tuyệt và nó hoạt động với tôi, ngoại trừ addNote không được dùng nữa và tôi nhận được AbstractMethodError nếu tôi sử dụng addPropertyNode thay thế. Google không giúp tôi ở đây. Giải pháp gì? Có một sự phụ thuộc bị thiếu ở đâu đó?
Paul Grenyer

29

Với Trình xác thực Hibernate 4.1.0.Final tôi khuyên bạn nên sử dụng @ScriptAssert . Exceprt từ JavaDoc của nó:

Các biểu thức tập lệnh có thể được viết bằng bất kỳ ngôn ngữ kịch bản hoặc biểu thức nào, có thể tìm thấy công cụ tương thích với JSR 223 ("Scripting cho nền tảng JavaTM" trên đường dẫn lớp.

Lưu ý: việc đánh giá đang được thực hiện bởi một " công cụ " tập lệnh đang chạy trong máy ảo Java, do đó ở phía "máy chủ" của Java, chứ không phải ở "phía máy khách" như đã nêu trong một số nhận xét.

Thí dụ:

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

  private String passVerify;
}

hoặc với bí danh ngắn hơn và null-safe:

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

  private String passVerify;
}

hoặc với 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;
}

Tuy nhiên, không có gì sai với giải pháp trình xác thực cấp độ tùy chỉnh @Matches .


1
Giải pháp thú vị, chúng tôi có thực sự sử dụng javascript ở đây để thực hiện xác nhận này không? Điều đó có vẻ như quá mức cần thiết cho những gì một chú thích dựa trên java có thể thực hiện được. Trước mắt tôi, giải pháp của Nicko đã đề xuất ở trên dường như vẫn rõ ràng hơn từ quan điểm về khả năng sử dụng (chú thích của anh ấy rất dễ đọc và khá chức năng so với các tham chiếu javascript-> java không liên quan) và từ quan điểm khả năng mở rộng (tôi cho rằng có chi phí hợp lý để xử lý javascript, nhưng có lẽ Hibernate đang lưu trữ mã được biên dịch ít nhất?). Tôi tò mò muốn hiểu tại sao điều này sẽ được ưa thích.
David park

2
Tôi đồng ý rằng việc triển khai của Nicko là tốt, nhưng tôi không thấy bất cứ điều gì phản đối về việc sử dụng JS làm ngôn ngữ biểu thức. Java 6 bao gồm Rhino cho các ứng dụng chính xác như vậy. Tôi thích @ScriptAssert vì nó chỉ hoạt động mà không cần tôi phải tạo chú thích và trình xác nhận mỗi khi tôi có một loại thử nghiệm mới lạ để thực hiện.

4
Như đã nói, không có gì sai với trình xác nhận cấp lớp. ScriptAssert chỉ là một giải pháp thay thế không yêu cầu bạn phải viết mã tùy chỉnh. Tôi không nói rằng đó là giải pháp ưa thích ;-)
Hardy

Câu trả lời tuyệt vời vì xác nhận mật khẩu không phải là xác thực quan trọng do đó có thể được thực hiện ở phía máy khách
peterchaula

19

Xác nhận trường chéo có thể được thực hiện bằng cách tạo các ràng buộc tùy chỉnh.

Ví dụ: - So sánh các trường mật khẩu và xác nhận mật khẩu của trường hợp Người dùng.

So sánhStrings

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

So sánhStringsValidator

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

Người sử dụng

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

Kiểm tra

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

Đầu ra Message:- [Password, ConfirmPassword] must be equal.

Bằng cách sử dụng ràng buộc xác thực So sánhStrings, chúng ta cũng có thể so sánh nhiều hơn hai thuộc tính và chúng ta có thể trộn bất kỳ phương thức so sánh bốn chuỗi nào.

ColorChoice

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

Kiểm tra

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

Đầu ra Message:- Please choose three different colors.

Tương tự như vậy, chúng ta có thể có các ràng buộc xác thực trường chéo so sánh, so sánh, vv.

PS Tôi chưa kiểm tra mã này trong môi trường sản xuất (mặc dù tôi đã thử nghiệm nó trong môi trường dev), vì vậy hãy xem mã này là Phát hành Mốc. Nếu bạn tìm thấy một lỗi, xin vui lòng viết bình luận tốt đẹp. :)


Tôi thích cách tiếp cận này, vì nó linh hoạt hơn các phương pháp khác. Nó cho phép tôi xác nhận hơn 2 trường cho sự bình đẳng. Công việc tốt!
Tauren

9

Tôi đã thử ví dụ của Alberthoven (hibernate-validator 4.0.2.GA) và tôi nhận được một ExceptionException: Các phương thức được chú thích phải tuân theo quy ước đặt tên JavaBeans. trận đấu () không. Sau khi tôi đổi tên phương thức từ „match, thành" isValid ", nó hoạt động.

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

}

Nó hoạt động chính xác với tôi nhưng không hiển thị thông báo lỗi. Nó đã hoạt động và hiển thị thông báo lỗi cho bạn. Làm sao?
Tiny

1
@Tiny: Tin nhắn nên có trong các vi phạm được người xác nhận trả lại. (Viết bài kiểm tra Đơn vị: stackoverflow.com/questions/5704743/ - ). NHƯNG thông báo xác thực thuộc về Thuộc tính "isValid". Do đó, thông báo sẽ chỉ được hiển thị trong GUI nếu GUI hiển thị các sự cố cho retypedPassword AND isValid (bên cạnh Mật khẩu được gõ lại).
Ralph

8

Nếu bạn đang sử dụng Spring Framework thì bạn có thể sử dụng Ngôn ngữ biểu thức mùa xuân (SpEL) cho điều đó. Tôi đã viết một thư viện nhỏ cung cấp trình xác nhận hợp lệ JSR-303 dựa trên SpEL - nó làm cho việc xác thực giữa các trường trở nên dễ dàng! Hãy xem https://github.com/jiruska/validator-spring .

Điều này sẽ xác nhận độ dài và sự bình đẳng của các trường mật khẩu.

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

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

    private String passVerify;
}

Bạn cũng có thể dễ dàng sửa đổi điều này để xác thực các trường mật khẩu chỉ khi không cả hai trống.

@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

Tôi thích ý tưởng từ Jakub Jiruska để sử dụng Ngôn ngữ biểu hiện mùa xuân. Nếu bạn không muốn thêm một thư viện / phụ thuộc khác (giả sử rằng bạn đã sử dụng Spring), thì đây là một triển khai đơn giản hóa ý tưởng của anh ấy.

Các ràng buộc:

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

Trình xác nhận:

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

Áp dụng như thế này:

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

3

Tôi không có tiếng để bình luận về câu trả lời đầu tiên nhưng muốn thêm rằng tôi đã thêm các bài kiểm tra đơn vị cho câu trả lời chiến thắng và có các quan sát sau:

  • Nếu bạn nhận được tên đầu tiên hoặc tên trường thì bạn sẽ gặp lỗi xác thực như thể các giá trị không khớp. Đừng vấp phải lỗi chính tả, vd

@FieldMatch (đầu tiên = " không hợp lệ FieldName1", second = "hợp lệFieldName2")

  • Trình xác nhận sẽ chấp nhận các loại dữ liệu tương đương, tức là tất cả các loại này sẽ vượt qua với FieldMatch:

chuỗi riêng StringField = "1";

private Integer integField = new Integer (1)

int int intField = 1;

  • Nếu các trường thuộc loại đối tượng không thực hiện bằng, xác thực sẽ thất bại.

2

Giải pháp bradhouse rất đẹp. Có cách nào để áp dụng chú thích @Matches cho nhiều trường không?

EDIT: Đây là giải pháp tôi đưa ra để trả lời câu hỏi này, tôi đã sửa đổi Ràng buộc để chấp nhận một mảng thay vì một giá trị duy nhất:

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

Mã cho chú thích:

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

Và việc thực hiện:

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

Hừm. Không chắc. Bạn có thể thử tạo trình xác nhận cụ thể cho từng trường xác nhận (để chúng có các chú thích khác nhau) hoặc cập nhật chú thích @Matches để chấp nhận nhiều cặp trường.
bradhouse

Cảm ơn bradhouse, đã đưa ra một giải pháp và đã đăng nó ở trên. Nó cần một công việc nhỏ để phục vụ khi số lượng tranh luận khác nhau được thông qua để bạn không nhận được IndexOutOfBoundExceptions, nhưng những điều cơ bản là có.
McGin

1

Bạn cần gọi nó một cách rõ ràng. Trong ví dụ trên, bradhouse đã cung cấp cho bạn tất cả các bước để viết một ràng buộc tùy chỉnh.

Thêm mã này trong lớp người gọi của bạn.

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

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

trong trường hợp trên nó sẽ là

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

1

Tại sao không thử Oval: http://oval.sourceforge.net/

Tôi có vẻ như nó hỗ trợ OGNL vì vậy có lẽ bạn có thể làm điều đó một cách tự nhiên hơn

@Assert(expr = "_value ==_this.pass").

1

Các bạn thật tuyệt vời. Những ý tưởng thực sự tuyệt vời. Tôi thích nhất của AlberthovenMcGin , vì vậy tôi quyết định kết hợp cả hai ý tưởng. Và phát triển một số giải pháp chung để phục vụ tất cả các trường hợp. Đây là giải pháp đề xuất của tôi.

@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

Tôi đã thực hiện một điều chỉnh nhỏ trong giải pháp của Nicko để không cần sử dụng thư viện Apache Commons BeanUtils và thay thế bằng giải pháp đã có sẵn vào mùa xuân, đối với những người sử dụng nó vì tôi có thể đơn giản hơn:

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

Giải pháp được thực hiện với câu hỏi: Làm thế nào để truy cập vào một trường được mô tả trong thuộc tính chú thích

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

Và làm thế nào để sử dụng nó ...? Như thế này:

@Entity
@EnableMatchConstraint
public class User {

    @NotBlank
    private String password;

    @Match(field = "password")
    private String passwordConfirmation;
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.