Статьи

Сочетание строгой типизации и валидации (JSR 303)

Иногда лучше использовать строгую типизацию вместо повторения одинаковых проверок для всех слоев и уровней. Интересно то, что сделать класс устойчивым к неправильному использованию очень похоже на использование Java Bean Validation.

Классический подход может выглядеть так:

public class User {

    private static final Pattern PATTERN = Pattern.compile("[a-z][0-9a-z_\\-]*");

    private String name;

    public User(String name) {
        super();
        if (name == null) {
            throw new IllegalArgumentException("name == null");
        }
        String trimmed = name.trim().toLowerCase();
        if (trimmed.length() == 0) {
            throw new IllegalArgumentException("length name == 0");
        }
        if (trimmed.length() < 3) {
            throw new IllegalArgumentException("length name < 3");
        }
        if (trimmed.length() > 20) {
            throw new IllegalArgumentException("length name > 20");
        }
        if (!PATTERN.matcher(trimmed).matches()) {
            throw new IllegalArgumentException("name pattern violated");
        }
        this.name = trimmed;
    }

}

Используя Bean Validation, мы могли бы вместо этого создать собственное ограничение:

@Size(min = 3, max = 20)
@Pattern(regexp = "[a-z][0-9a-z_\\-]*")
@Target({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
@Documented
public @interface UserName {

    String message() default "{org.fuin.blog.UserName.message}";

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

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

}

Класс User теперь выглядит намного лучше:

public class User {

    @NotNull
    @UserName
    private String name;

    public User(String name) {
        super();
        this.name = name;
    }

}

Но теперь объект утратил способность защищать себя от неправильного использования. Это больше не надежный объект. Может быть, кто-то использует валидатор, чтобы проверить, является ли объект действительным; возможно, нет. В любом случае всегда можно создать недопустимые объекты такого рода.

Как насчет сочетания обеих техник?

Давайте переименуем аннотацию UserName в UserNameStr, потому что она фактически работает со строкой, и таким образом, мы также можем избежать столкновения имен с новым сильным типом, который мы скоро создадим:

@Size(min = 3, max = 20)
@Pattern(regexp = "[a-z][0-9a-z_\\-]*")
@Target({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
@Documented
public @interface UserNameStr {

    String message() default "{org.fuin.blog.UserNameStr.message}";

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

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

}

Далее мы создаем базовый тип для всех таких строк на основе строгой типизации:

public abstract class AbstractStringBasedType<T extends AbstractStringBasedType<T>> implements Comparable<T>, Serializable {

    private static final long serialVersionUID = 0L;

    private static final Validator VALIDATOR;

    static {
        VALIDATOR = Validation.buildDefaultValidatorFactory().getValidator();
    }

    public final int hashCode() {
        return nullSafeToString().hashCode();
    }

    public final boolean equals(final Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final T other = (T) obj;
        return nullSafeToString().equals(other.nullSafeToString());
    }

    public final int compareTo(final T other) {
        return this.nullSafeToString().compareTo(other.nullSafeToString());
    }

    public final int length() {
        return nullSafeToString().length();
    }

    protected final void requireValid(final T value) {
        final Set<ConstraintViolation<T>> constraintViolations = VALIDATOR.validate(value);
        if (constraintViolations.size() > 0) {
            final StringBuffer sb = new StringBuffer();
            for (final ConstraintViolation<T> constraintViolation : constraintViolations) {
                if (sb.length() > 0) {
                    sb.append(", ");
                }
                sb.append("[" + constraintViolation.getPropertyPath() + "] "
                        + constraintViolation.getMessage() + " {"
                        + constraintViolation.getInvalidValue() + "}");
            }
            throw new IllegalArgumentException(sb.toString());
        }
    }

    private String nullSafeToString() {
        final String str = toString();
        if (str == null) {
            return "null";
        }
        return str;
    }

    public abstract String toString();

}

Реорганизованный класс UserName теперь использует API Bean Validation для выполнения проверки ограничений в конце конструктора, что означает, что больше нельзя создавать недопустимые объекты:

public final class UserName extends AbstractStringBasedType<UserName> {

    private static final long serialVersionUID = 0L;

    @NotNull
    @UserNameStr
    private final String userName;

    public UserName(final String userName) {
        super();
        this.userName = userName;

        // Always the last line in the constructor!
        requireValid(this);
    }

    public String toString() {
        return userName;
    }

}

Измененный класс User теперь стал еще проще и содержит только аннотацию @NotNull для свойства name:

public class User {

    // Only null check here, because all other
    // checks are done by user name itself
    @NotNull
    private UserName name;

    public User(UserName name) {
        super();
        this.name = name;
    }

}

Вот простой пример использования типа UserName:

public class Example {

    public static void main(String[] args) {

        Locale.setDefault(Locale.ENGLISH);

        try {
            new UserName(null);
        } catch (IllegalArgumentException ex) {
            // [userName] may not be null {null}
        }

        try {
            new UserName("");
        } catch (IllegalArgumentException ex) {
            // [userName] must match "[a-z][0-9a-z_\-]*" {},
            // [userName] size must be between 3 and 20 {}
        }

        try {
            new UserName("_a1");
        } catch (IllegalArgumentException ex) {
            // [userName] must match "[a-z][0-9a-z_\-]*" {_a1}
        }

        // Valid name
        System.out.println(new UserName("john-2_a"));

    }

}

Если мы не хотим использовать строгую типизацию, легко использовать только аннотации. Эта возможность особенно полезна, когда вам приходится иметь дело с не-Java клиентами. В этих ситуациях DTO может содержать только простую аннотированную строку:

public class UserDTO implements Serializable {

    private static final long serialVersionUID = 1L;

    @NotNull
    @UserNameStr
    private String name;

    public UserDTO(String name) {
        super();
        this.name = name;
    }

}

Теперь сильные типы и проверка бобов могут жить в мирном сосуществовании в вашем приложении. Перед началом использования рекомендуется проверить, поддерживают ли элементы управления графического интерфейса такие расширенные типы JS303. В противном случае вы можете потерять возможность предварительной проверки на клиенте (например, проверка длины ввода на основе аннотации @Size).

 

От http://javadeveloperslife.wordpress.com/2011/12/05/combining-strong-typing-and-bean-validation-jsr-303/