Spring MVC -- 驗證器 使用JSR-303進行校驗 @Valid 從源碼分析java.lang.String.isEmpty() 使用JSR-303進行校驗 @Valid spring M

輸入驗證是Spring處理的最重要Web開發任務之一。在Spring MVC中,有兩種方式能夠驗證輸入,即利用Spring自帶的驗證框架,或者利用JSR 303實現。本篇博客將介紹這兩種輸入驗證方法。css

本篇博客用兩個不一樣的示例分別介紹這兩種方式:spring-validator和jsr303-validator。html

一 驗證概覽

Converter和Formatter做用於字段級。在MVC Web應用中,它們將String類型轉換或格式化成另外一種Java類型,如java.time.LocalDate。驗證器則做用於對象級。它決定某一個對象中的全部字段是否均是有效的,以及是否遵循某些規則。一個典型的Spring MVC應用會同時應用到Formatter(或Converter)和Validator。java

若是一個應用程序既使用了Formatter,又有了Validator,那麼,應用中的事件順序是這樣的:在調用Controller的請求處理方式時,將會有一個或者多個Formatter,試圖將輸入字符串轉換成domain對象中的屬性(或者說字段)值,一旦格式化成功,驗證器就會介入。git

例如:Order對象有一個shippingDate屬性(其類型爲LocalDate),它的值絕對不可能早於今天的日期。當調用OrderController時,LocalDateFormatter會將字符串轉換成LocalDate,並將它賦予Order對象的shippingDate屬性。若是轉換失敗,用戶就會被轉回到前一個表單;若是轉換成功,則會調用驗證器,查看shippingDate是否早於今天的日期。web

如今,你或許會問,將驗證邏輯轉移到LocalDateFormatter中是否更加明智?正則表達式

由於比較一下日期並不是難事,但答案倒是確定的。首先,LocalDateFormatter還能夠用於將其它字符串格式化成日期,如birthDate或者purchaseDate。這兩個日期的規則都不一樣於shippingDate,事實上,好比,員工的出生日期絕對不可能晚於今日。spring

其次,校驗器能夠檢查兩個或更多字段之間的關係,各字段均受不一樣的Formatter支持。例如,假設Employee對象有birthDate屬性和startDate屬性,驗證器就能夠設定規則,使任何員工的入職日期均不可能早於他的出生日期。所以,有效的Employee對象必須讓它的birthDate屬性值早於其startDate值,這就是驗證器的任務。express

二 Spring驗證器

從一開始,Spring就設計了輸入驗證,甚至早於JSR 303(Java驗證規範)。所以,Spring的Validation框架至今都很廣泛,對於新項目,通常建議使用JSR 303驗證器。apache

爲了建立Spring驗證器,要實現org.springframework.validation.Validator接口,這個接口的源碼以下:編程

/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.validation;

/**
 * A validator for application-specific objects.
 *
 * <p>This interface is totally divorced from any infrastructure
 * or context; that is to say it is not coupled to validating
 * only objects in the web tier, the data-access tier, or the
 * whatever-tier. As such it is amenable to being used in any layer
 * of an application, and supports the encapsulation of validation
 * logic as a first-class citizen in its own right.
 *
 * <p>Find below a simple but complete {@code Validator}
 * implementation, which validates that the various {@link String}
 * properties of a {@code UserLogin} instance are not empty
 * (that is they are not {@code null} and do not consist
 * wholly of whitespace), and that any password that is present is
 * at least {@code 'MINIMUM_PASSWORD_LENGTH'} characters in length.
 *
 * <pre class="code"> public class UserLoginValidator implements Validator {
 *
 *    private static final int MINIMUM_PASSWORD_LENGTH = 6;
 *
 *    public boolean supports(Class clazz) {
 *       return UserLogin.class.isAssignableFrom(clazz);
 *    }
 *
 *    public void validate(Object target, Errors errors) {
 *       ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName", "field.required");
 *       ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "field.required");
 *       UserLogin login = (UserLogin) target;
 *       if (login.getPassword() != null
 *             && login.getPassword().trim().length() < MINIMUM_PASSWORD_LENGTH) {
 *          errors.rejectValue("password", "field.min.length",
 *                new Object[]{Integer.valueOf(MINIMUM_PASSWORD_LENGTH)},
 *                "The password must be at least [" + MINIMUM_PASSWORD_LENGTH + "] characters in length.");
 *       }
 *    }
 * }</pre>
 *
 * <p>See also the Spring reference manual for a fuller discussion of
 * the {@code Validator} interface and its role in an enterprise
 * application.
 *
 * @author Rod Johnson
 * @see SmartValidator
 * @see Errors
 * @see ValidationUtils
 */
public interface Validator {

    /**
     * Can this {@link Validator} {@link #validate(Object, Errors) validate}
     * instances of the supplied {@code clazz}?
     * <p>This method is <i>typically</i> implemented like so:
     * <pre class="code">return Foo.class.isAssignableFrom(clazz);</pre>
     * (Where {@code Foo} is the class (or superclass) of the actual
     * object instance that is to be {@link #validate(Object, Errors) validated}.)
     * @param clazz the {@link Class} that this {@link Validator} is
     * being asked if it can {@link #validate(Object, Errors) validate}
     * @return {@code true} if this {@link Validator} can indeed
     * {@link #validate(Object, Errors) validate} instances of the
     * supplied {@code clazz}
     */
    boolean supports(Class<?> clazz);

    /**
     * Validate the supplied {@code target} object, which must be
     * of a {@link Class} for which the {@link #supports(Class)} method
     * typically has (or would) return {@code true}.
     * <p>The supplied {@link Errors errors} instance can be used to report
     * any resulting validation errors.
     * @param target the object that is to be validated
     * @param errors contextual state about the validation process
     * @see ValidationUtils
     */
    void validate(Object target, Errors errors);

}
View Code

這個接口須要實現兩個方法:supports()和validate()

若是驗證器能夠處理指定的Class,supports()方法將返回true。只有當supports()方法的返回結果爲true的時候,validate()方法纔會被調用來驗證目標對象,並將驗證錯誤填入Errors對象。

Errors對象是org.springframework.validation.Errors接口的一個實現類。Errors接口的源代碼以下:

/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.validation;

import java.util.List;

import org.springframework.beans.PropertyAccessor;
import org.springframework.lang.Nullable;

/**
 * Stores and exposes information about data-binding and validation
 * errors for a specific object.
 *
 * <p>Field names can be properties of the target object (e.g. "name"
 * when binding to a customer object), or nested fields in case of
 * subobjects (e.g. "address.street"). Supports subtree navigation
 * via {@link #setNestedPath(String)}: for example, an
 * {@code AddressValidator} validates "address", not being aware
 * that this is a subobject of customer.
 *
 * <p>Note: {@code Errors} objects are single-threaded.
 *
 * @author Rod Johnson
 * @author Juergen Hoeller
 * @see #setNestedPath
 * @see BindException
 * @see DataBinder
 * @see ValidationUtils
 */
public interface Errors {

    /**
     * The separator between path elements in a nested path,
     * for example in "customer.name" or "customer.address.street".
     * <p>"." = same as the
     * {@link org.springframework.beans.PropertyAccessor#NESTED_PROPERTY_SEPARATOR nested property separator}
     * in the beans package.
     */
    String NESTED_PATH_SEPARATOR = PropertyAccessor.NESTED_PROPERTY_SEPARATOR;


    /**
     * Return the name of the bound root object.
     */
    String getObjectName();

    /**
     * Allow context to be changed so that standard validators can validate
     * subtrees. Reject calls prepend the given path to the field names.
     * <p>For example, an address validator could validate the subobject
     * "address" of a customer object.
     * @param nestedPath nested path within this object,
     * e.g. "address" (defaults to "", {@code null} is also acceptable).
     * Can end with a dot: both "address" and "address." are valid.
     */
    void setNestedPath(String nestedPath);

    /**
     * Return the current nested path of this {@link Errors} object.
     * <p>Returns a nested path with a dot, i.e. "address.", for easy
     * building of concatenated paths. Default is an empty String.
     */
    String getNestedPath();

    /**
     * Push the given sub path onto the nested path stack.
     * <p>A {@link #popNestedPath()} call will reset the original
     * nested path before the corresponding
     * {@code pushNestedPath(String)} call.
     * <p>Using the nested path stack allows to set temporary nested paths
     * for subobjects without having to worry about a temporary path holder.
     * <p>For example: current path "spouse.", pushNestedPath("child") ->
     * result path "spouse.child."; popNestedPath() -> "spouse." again.
     * @param subPath the sub path to push onto the nested path stack
     * @see #popNestedPath
     */
    void pushNestedPath(String subPath);

    /**
     * Pop the former nested path from the nested path stack.
     * @throws IllegalStateException if there is no former nested path on the stack
     * @see #pushNestedPath
     */
    void popNestedPath() throws IllegalStateException;

    /**
     * Register a global error for the entire target object,
     * using the given error description.
     * @param errorCode error code, interpretable as a message key
     */
    void reject(String errorCode);

    /**
     * Register a global error for the entire target object,
     * using the given error description.
     * @param errorCode error code, interpretable as a message key
     * @param defaultMessage fallback default message
     */
    void reject(String errorCode, String defaultMessage);

    /**
     * Register a global error for the entire target object,
     * using the given error description.
     * @param errorCode error code, interpretable as a message key
     * @param errorArgs error arguments, for argument binding via MessageFormat
     * (can be {@code null})
     * @param defaultMessage fallback default message
     */
    void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);

    /**
     * Register a field error for the specified field of the current object
     * (respecting the current nested path, if any), using the given error
     * description.
     * <p>The field name may be {@code null} or empty String to indicate
     * the current object itself rather than a field of it. This may result
     * in a corresponding field error within the nested object graph or a
     * global error if the current object is the top object.
     * @param field the field name (may be {@code null} or empty String)
     * @param errorCode error code, interpretable as a message key
     * @see #getNestedPath()
     */
    void rejectValue(@Nullable String field, String errorCode);

    /**
     * Register a field error for the specified field of the current object
     * (respecting the current nested path, if any), using the given error
     * description.
     * <p>The field name may be {@code null} or empty String to indicate
     * the current object itself rather than a field of it. This may result
     * in a corresponding field error within the nested object graph or a
     * global error if the current object is the top object.
     * @param field the field name (may be {@code null} or empty String)
     * @param errorCode error code, interpretable as a message key
     * @param defaultMessage fallback default message
     * @see #getNestedPath()
     */
    void rejectValue(@Nullable String field, String errorCode, String defaultMessage);

    /**
     * Register a field error for the specified field of the current object
     * (respecting the current nested path, if any), using the given error
     * description.
     * <p>The field name may be {@code null} or empty String to indicate
     * the current object itself rather than a field of it. This may result
     * in a corresponding field error within the nested object graph or a
     * global error if the current object is the top object.
     * @param field the field name (may be {@code null} or empty String)
     * @param errorCode error code, interpretable as a message key
     * @param errorArgs error arguments, for argument binding via MessageFormat
     * (can be {@code null})
     * @param defaultMessage fallback default message
     * @see #getNestedPath()
     */
    void rejectValue(@Nullable String field, String errorCode,
            @Nullable Object[] errorArgs, @Nullable String defaultMessage);

    /**
     * Add all errors from the given {@code Errors} instance to this
     * {@code Errors} instance.
     * <p>This is a convenience method to avoid repeated {@code reject(..)}
     * calls for merging an {@code Errors} instance into another
     * {@code Errors} instance.
     * <p>Note that the passed-in {@code Errors} instance is supposed
     * to refer to the same target object, or at least contain compatible errors
     * that apply to the target object of this {@code Errors} instance.
     * @param errors the {@code Errors} instance to merge in
     */
    void addAllErrors(Errors errors);

    /**
     * Return if there were any errors.
     */
    boolean hasErrors();

    /**
     * Return the total number of errors.
     */
    int getErrorCount();

    /**
     * Get all errors, both global and field ones.
     * @return a list of {@link ObjectError} instances
     */
    List<ObjectError> getAllErrors();

    /**
     * Are there any global errors?
     * @return {@code true} if there are any global errors
     * @see #hasFieldErrors()
     */
    boolean hasGlobalErrors();

    /**
     * Return the number of global errors.
     * @return the number of global errors
     * @see #getFieldErrorCount()
     */
    int getGlobalErrorCount();

    /**
     * Get all global errors.
     * @return a list of {@link ObjectError} instances
     */
    List<ObjectError> getGlobalErrors();

    /**
     * Get the <i>first</i> global error, if any.
     * @return the global error, or {@code null}
     */
    @Nullable
    ObjectError getGlobalError();

    /**
     * Are there any field errors?
     * @return {@code true} if there are any errors associated with a field
     * @see #hasGlobalErrors()
     */
    boolean hasFieldErrors();

    /**
     * Return the number of errors associated with a field.
     * @return the number of errors associated with a field
     * @see #getGlobalErrorCount()
     */
    int getFieldErrorCount();

    /**
     * Get all errors associated with a field.
     * @return a List of {@link FieldError} instances
     */
    List<FieldError> getFieldErrors();

    /**
     * Get the <i>first</i> error associated with a field, if any.
     * @return the field-specific error, or {@code null}
     */
    @Nullable
    FieldError getFieldError();

    /**
     * Are there any errors associated with the given field?
     * @param field the field name
     * @return {@code true} if there were any errors associated with the given field
     */
    boolean hasFieldErrors(String field);

    /**
     * Return the number of errors associated with the given field.
     * @param field the field name
     * @return the number of errors associated with the given field
     */
    int getFieldErrorCount(String field);

    /**
     * Get all errors associated with the given field.
     * <p>Implementations should support not only full field names like
     * "name" but also pattern matches like "na*" or "address.*".
     * @param field the field name
     * @return a List of {@link FieldError} instances
     */
    List<FieldError> getFieldErrors(String field);

    /**
     * Get the first error associated with the given field, if any.
     * @param field the field name
     * @return the field-specific error, or {@code null}
     */
    @Nullable
    FieldError getFieldError(String field);

    /**
     * Return the current value of the given field, either the current
     * bean property value or a rejected update from the last binding.
     * <p>Allows for convenient access to user-specified field values,
     * even if there were type mismatches.
     * @param field the field name
     * @return the current value of the given field
     */
    @Nullable
    Object getFieldValue(String field);

    /**
     * Return the type of a given field.
     * <p>Implementations should be able to determine the type even
     * when the field value is {@code null}, for example from some
     * associated descriptor.
     * @param field the field name
     * @return the type of the field, or {@code null} if not determinable
     */
    @Nullable
    Class<?> getFieldType(String field);

}
View Code

Errors對象中包含了一個FieldError類型的集合和一個ObjectError類型的集合:

  • FieldError對象表示與被驗證對象中的某個屬性相關的一個錯誤;
  • ObjectError對象表示與被驗證對象相關的一個錯誤;其中FieldError繼承自ObjectError類。

FieldError類的源碼以下:

/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.validation;

import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;

/**
 * Encapsulates a field error, that is, a reason for rejecting a specific
 * field value.
 *
 * <p>See the {@link DefaultMessageCodesResolver} javadoc for details on
 * how a message code list is built for a {@code FieldError}.
 *
 * @author Rod Johnson
 * @author Juergen Hoeller
 * @since 10.03.2003
 * @see DefaultMessageCodesResolver
 */
@SuppressWarnings("serial")
public class FieldError extends ObjectError {

    private final String field;

    @Nullable
    private final Object rejectedValue;

    private final boolean bindingFailure;


    /**
     * Create a new FieldError instance.
     * @param objectName the name of the affected object
     * @param field the affected field of the object
     * @param defaultMessage the default message to be used to resolve this message
     */
    public FieldError(String objectName, String field, String defaultMessage) {
        this(objectName, field, null, false, null, null, defaultMessage);
    }

    /**
     * Create a new FieldError instance.
     * @param objectName the name of the affected object
     * @param field the affected field of the object
     * @param rejectedValue the rejected field value
     * @param bindingFailure whether this error represents a binding failure
     * (like a type mismatch); else, it is a validation failure
     * @param codes the codes to be used to resolve this message
     * @param arguments the array of arguments to be used to resolve this message
     * @param defaultMessage the default message to be used to resolve this message
     */
    public FieldError(String objectName, String field, @Nullable Object rejectedValue, boolean bindingFailure,
            @Nullable String[] codes, @Nullable Object[] arguments, @Nullable String defaultMessage) {

        super(objectName, codes, arguments, defaultMessage);
        Assert.notNull(field, "Field must not be null");
        this.field = field;
        this.rejectedValue = rejectedValue;
        this.bindingFailure = bindingFailure;
    }


    /**
     * Return the affected field of the object.
     */
    public String getField() {
        return this.field;
    }

    /**
     * Return the rejected field value.
     */
    @Nullable
    public Object getRejectedValue() {
        return this.rejectedValue;
    }

    /**
     * Return whether this error represents a binding failure
     * (like a type mismatch); otherwise it is a validation failure.
     */
    public boolean isBindingFailure() {
        return this.bindingFailure;
    }


    @Override
    public boolean equals(@Nullable Object other) {
        if (this == other) {
            return true;
        }
        if (!super.equals(other)) {
            return false;
        }
        FieldError otherError = (FieldError) other;
        return (getField().equals(otherError.getField()) &&
                ObjectUtils.nullSafeEquals(getRejectedValue(), otherError.getRejectedValue()) &&
                isBindingFailure() == otherError.isBindingFailure());
    }

    @Override
    public int hashCode() {
        int hashCode = super.hashCode();
        hashCode = 29 * hashCode + getField().hashCode();
        hashCode = 29 * hashCode + ObjectUtils.nullSafeHashCode(getRejectedValue());
        hashCode = 29 * hashCode + (isBindingFailure() ? 1 : 0);
        return hashCode;
    }

    @Override
    public String toString() {
        return "Field error in object '" + getObjectName() + "' on field '" + this.field +
                "': rejected value [" + ObjectUtils.nullSafeToString(this.rejectedValue) + "]; " +
                resolvableToString();
    }

}
View Code

ObjectError類的源碼以下:

/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.validation;

import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;

/**
 * Encapsulates an object error, that is, a global reason for rejecting
 * an object.
 *
 * <p>See the {@link DefaultMessageCodesResolver} javadoc for details on
 * how a message code list is built for an {@code ObjectError}.
 *
 * @author Juergen Hoeller
 * @since 10.03.2003
 * @see FieldError
 * @see DefaultMessageCodesResolver
 */
@SuppressWarnings("serial")
public class ObjectError extends DefaultMessageSourceResolvable {

    private final String objectName;

    @Nullable
    private transient Object source;


    /**
     * Create a new instance of the ObjectError class.
     * @param objectName the name of the affected object
     * @param defaultMessage the default message to be used to resolve this message
     */
    public ObjectError(String objectName, String defaultMessage) {
        this(objectName, null, null, defaultMessage);
    }

    /**
     * Create a new instance of the ObjectError class.
     * @param objectName the name of the affected object
     * @param codes the codes to be used to resolve this message
     * @param arguments    the array of arguments to be used to resolve this message
     * @param defaultMessage the default message to be used to resolve this message
     */
    public ObjectError(
            String objectName, @Nullable String[] codes, @Nullable Object[] arguments, @Nullable String defaultMessage) {

        super(codes, arguments, defaultMessage);
        Assert.notNull(objectName, "Object name must not be null");
        this.objectName = objectName;
    }


    /**
     * Return the name of the affected object.
     */
    public String getObjectName() {
        return this.objectName;
    }

    /**
     * Preserve the source behind this error: possibly an {@link Exception}
     * (typically {@link org.springframework.beans.PropertyAccessException})
     * or a Bean Validation {@link javax.validation.ConstraintViolation}.
     * <p>Note that any such source object is being stored as transient:
     * that is, it won't be part of a serialized error representation.
     * @param source the source object
     * @since 5.0.4
     */
    public void wrap(Object source) {
        if (this.source != null) {
            throw new IllegalStateException("Already wrapping " + this.source);
        }
        this.source = source;
    }

    /**
     * Unwrap the source behind this error: possibly an {@link Exception}
     * (typically {@link org.springframework.beans.PropertyAccessException})
     * or a Bean Validation {@link javax.validation.ConstraintViolation}.
     * <p>The cause of the outermost exception will be introspected as well,
     * e.g. the underlying conversion exception or exception thrown from a setter
     * (instead of having to unwrap the {@code PropertyAccessException} in turn).
     * @return the source object of the given type
     * @throws IllegalArgumentException if no such source object is available
     * (i.e. none specified or not available anymore after deserialization)
     * @since 5.0.4
     */
    public <T> T unwrap(Class<T> sourceType) {
        if (sourceType.isInstance(this.source)) {
            return sourceType.cast(this.source);
        }
        else if (this.source instanceof Throwable) {
            Throwable cause = ((Throwable) this.source).getCause();
            if (sourceType.isInstance(cause)) {
                return sourceType.cast(cause);
            }
        }
        throw new IllegalArgumentException("No source object of the given type available: " + sourceType);
    }

    /**
     * Check the source behind this error: possibly an {@link Exception}
     * (typically {@link org.springframework.beans.PropertyAccessException})
     * or a Bean Validation {@link javax.validation.ConstraintViolation}.
     * <p>The cause of the outermost exception will be introspected as well,
     * e.g. the underlying conversion exception or exception thrown from a setter
     * (instead of having to unwrap the {@code PropertyAccessException} in turn).
     * @return whether this error has been caused by a source object of the given type
     * @since 5.0.4
     */
    public boolean contains(Class<?> sourceType) {
        return (sourceType.isInstance(this.source) ||
                (this.source instanceof Throwable && sourceType.isInstance(((Throwable) this.source).getCause())));
    }


    @Override
    public boolean equals(@Nullable Object other) {
        if (this == other) {
            return true;
        }
        if (other == null || other.getClass() != getClass() || !super.equals(other)) {
            return false;
        }
        ObjectError otherError = (ObjectError) other;
        return getObjectName().equals(otherError.getObjectName());
    }

    @Override
    public int hashCode() {
        return super.hashCode() * 29 + getObjectName().hashCode();
    }

    @Override
    public String toString() {
        return "Error in object '" + this.objectName + "': " + resolvableToString();
    }

}
View Code

編寫驗證器時,不須要直接建立Error對象,而且實例化FieldError和ObjectError花費了大量的編程精力。這是由於ObjectError類有兩個構造函數,其中一個須要2個參數,另外一個須要4個參數:FieldError類的構造器也有2個,其中一個須要3個參數,另外一個須要7個參數。

可是咱們能夠經過調用rejectValue()方法向Errors對象中添加被驗證對象的字段錯誤信息,該方法其實是建立一個FieldError對象,並添加Errors該對象的List<FieldError>集合中。

經過調用reject()方法向Errors對象中添加被驗證對象錯誤信息,該方法其實是建立一個ObjectError對象,並添加到Errors對象的List<ObjectError>中。

下面是reject()和rejectValue的部分方法重載:

    void reject(String errorCode);
    void reject(String errorCode, String defaultMessage);
    void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage);
    void rejectValue(@Nullable String field, String errorCode);
    void rejectValue(@Nullable String field, String errorCode, String defaultMessage);
    void rejectValue(@Nullable String field, String errorCode,
            @Nullable Object[] errorArgs, @Nullable String defaultMessage);

大多數時候,只給reject()或者rejectValue()方法傳入一個錯誤碼,Spring就會在屬性文件中查找錯誤碼,獲取相應的錯誤消息。還能夠傳入一個默認消息,當沒有找到指定的錯誤碼時,就會使用默認消息。

Errors對象中的錯誤消息,能夠利用表單標籤庫的Errors標籤顯示在HTML頁面中,錯誤消息能夠經過Spring支持的國際化特性本地化。

三 ValidationUtils類 

org.springframework.validation.ValidationUtils類是一個工具,源碼以下:

/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.validation;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * Utility class offering convenient methods for invoking a {@link Validator}
 * and for rejecting empty fields.
 *
 * <p>Checks for an empty field in {@code Validator} implementations can become
 * one-liners when using {@link #rejectIfEmpty} or {@link #rejectIfEmptyOrWhitespace}.
 *
 * @author Juergen Hoeller
 * @author Dmitriy Kopylenko
 * @since 06.05.2003
 * @see Validator
 * @see Errors
 */
public abstract class ValidationUtils {

    private static final Log logger = LogFactory.getLog(ValidationUtils.class);


    /**
     * Invoke the given {@link Validator} for the supplied object and
     * {@link Errors} instance.
     * @param validator the {@code Validator} to be invoked
     * @param target the object to bind the parameters to
     * @param errors the {@link Errors} instance that should store the errors
     * @throws IllegalArgumentException if either of the {@code Validator} or {@code Errors}
     * arguments is {@code null}, or if the supplied {@code Validator} does not
     * {@link Validator#supports(Class) support} the validation of the supplied object's type
     */
    public static void invokeValidator(Validator validator, Object target, Errors errors) {
        invokeValidator(validator, target, errors, (Object[]) null);
    }

    /**
     * Invoke the given {@link Validator}/{@link SmartValidator} for the supplied object and
     * {@link Errors} instance.
     * @param validator the {@code Validator} to be invoked
     * @param target the object to bind the parameters to
     * @param errors the {@link Errors} instance that should store the errors
     * @param validationHints one or more hint objects to be passed to the validation engine
     * @throws IllegalArgumentException if either of the {@code Validator} or {@code Errors}
     * arguments is {@code null}, or if the supplied {@code Validator} does not
     * {@link Validator#supports(Class) support} the validation of the supplied object's type
     */
    public static void invokeValidator(
            Validator validator, Object target, Errors errors, @Nullable Object... validationHints) {

        Assert.notNull(validator, "Validator must not be null");
        Assert.notNull(target, "Target object must not be null");
        Assert.notNull(errors, "Errors object must not be null");

        if (logger.isDebugEnabled()) {
            logger.debug("Invoking validator [" + validator + "]");
        }
        if (!validator.supports(target.getClass())) {
            throw new IllegalArgumentException(
                    "Validator [" + validator.getClass() + "] does not support [" + target.getClass() + "]");
        }

        if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
            ((SmartValidator) validator).validate(target, errors, validationHints);
        }
        else {
            validator.validate(target, errors);
        }

        if (logger.isDebugEnabled()) {
            if (errors.hasErrors()) {
                logger.debug("Validator found " + errors.getErrorCount() + " errors");
            }
            else {
                logger.debug("Validator found no errors");
            }
        }
    }


    /**
     * Reject the given field with the given error code if the value is empty.
     * <p>An 'empty' value in this context means either {@code null} or
     * the empty string "".
     * <p>The object whose field is being validated does not need to be passed
     * in because the {@link Errors} instance can resolve field values by itself
     * (it will usually hold an internal reference to the target object).
     * @param errors the {@code Errors} instance to register errors on
     * @param field the field name to check
     * @param errorCode the error code, interpretable as message key
     */
    public static void rejectIfEmpty(Errors errors, String field, String errorCode) {
        rejectIfEmpty(errors, field, errorCode, null, null);
    }

    /**
     * Reject the given field with the given error code and default message
     * if the value is empty.
     * <p>An 'empty' value in this context means either {@code null} or
     * the empty string "".
     * <p>The object whose field is being validated does not need to be passed
     * in because the {@link Errors} instance can resolve field values by itself
     * (it will usually hold an internal reference to the target object).
     * @param errors the {@code Errors} instance to register errors on
     * @param field the field name to check
     * @param errorCode error code, interpretable as message key
     * @param defaultMessage fallback default message
     */
    public static void rejectIfEmpty(Errors errors, String field, String errorCode, String defaultMessage) {
        rejectIfEmpty(errors, field, errorCode, null, defaultMessage);
    }

    /**
     * Reject the given field with the given error code and error arguments
     * if the value is empty.
     * <p>An 'empty' value in this context means either {@code null} or
     * the empty string "".
     * <p>The object whose field is being validated does not need to be passed
     * in because the {@link Errors} instance can resolve field values by itself
     * (it will usually hold an internal reference to the target object).
     * @param errors the {@code Errors} instance to register errors on
     * @param field the field name to check
     * @param errorCode the error code, interpretable as message key
     * @param errorArgs the error arguments, for argument binding via MessageFormat
     * (can be {@code null})
     */
    public static void rejectIfEmpty(Errors errors, String field, String errorCode, Object[] errorArgs) {
        rejectIfEmpty(errors, field, errorCode, errorArgs, null);
    }

    /**
     * Reject the given field with the given error code, error arguments
     * and default message if the value is empty.
     * <p>An 'empty' value in this context means either {@code null} or
     * the empty string "".
     * <p>The object whose field is being validated does not need to be passed
     * in because the {@link Errors} instance can resolve field values by itself
     * (it will usually hold an internal reference to the target object).
     * @param errors the {@code Errors} instance to register errors on
     * @param field the field name to check
     * @param errorCode the error code, interpretable as message key
     * @param errorArgs the error arguments, for argument binding via MessageFormat
     * (can be {@code null})
     * @param defaultMessage fallback default message
     */
    public static void rejectIfEmpty(Errors errors, String field, String errorCode,
            @Nullable Object[] errorArgs, @Nullable String defaultMessage) {

        Assert.notNull(errors, "Errors object must not be null");
        Object value = errors.getFieldValue(field);
        if (value == null || !StringUtils.hasLength(value.toString())) {
            errors.rejectValue(field, errorCode, errorArgs, defaultMessage);
        }
    }

    /**
     * Reject the given field with the given error code if the value is empty
     * or just contains whitespace.
     * <p>An 'empty' value in this context means either {@code null},
     * the empty string "", or consisting wholly of whitespace.
     * <p>The object whose field is being validated does not need to be passed
     * in because the {@link Errors} instance can resolve field values by itself
     * (it will usually hold an internal reference to the target object).
     * @param errors the {@code Errors} instance to register errors on
     * @param field the field name to check
     * @param errorCode the error code, interpretable as message key
     */
    public static void rejectIfEmptyOrWhitespace(Errors errors, String field, String errorCode) {
        rejectIfEmptyOrWhitespace(errors, field, errorCode, null, null);
    }

    /**
     * Reject the given field with the given error code and default message
     * if the value is empty or just contains whitespace.
     * <p>An 'empty' value in this context means either {@code null},
     * the empty string "", or consisting wholly of whitespace.
     * <p>The object whose field is being validated does not need to be passed
     * in because the {@link Errors} instance can resolve field values by itself
     * (it will usually hold an internal reference to the target object).
     * @param errors the {@code Errors} instance to register errors on
     * @param field the field name to check
     * @param errorCode the error code, interpretable as message key
     * @param defaultMessage fallback default message
     */
    public static void rejectIfEmptyOrWhitespace(
            Errors errors, String field, String errorCode, String defaultMessage) {

        rejectIfEmptyOrWhitespace(errors, field, errorCode, null, defaultMessage);
    }

    /**
     * Reject the given field with the given error code and error arguments
     * if the value is empty or just contains whitespace.
     * <p>An 'empty' value in this context means either {@code null},
     * the empty string "", or consisting wholly of whitespace.
     * <p>The object whose field is being validated does not need to be passed
     * in because the {@link Errors} instance can resolve field values by itself
     * (it will usually hold an internal reference to the target object).
     * @param errors the {@code Errors} instance to register errors on
     * @param field the field name to check
     * @param errorCode the error code, interpretable as message key
     * @param errorArgs the error arguments, for argument binding via MessageFormat
     * (can be {@code null})
     */
    public static void rejectIfEmptyOrWhitespace(
            Errors errors, String field, String errorCode, @Nullable Object[] errorArgs) {

        rejectIfEmptyOrWhitespace(errors, field, errorCode, errorArgs, null);
    }

    /**
     * Reject the given field with the given error code, error arguments
     * and default message if the value is empty or just contains whitespace.
     * <p>An 'empty' value in this context means either {@code null},
     * the empty string "", or consisting wholly of whitespace.
     * <p>The object whose field is being validated does not need to be passed
     * in because the {@link Errors} instance can resolve field values by itself
     * (it will usually hold an internal reference to the target object).
     * @param errors the {@code Errors} instance to register errors on
     * @param field the field name to check
     * @param errorCode the error code, interpretable as message key
     * @param errorArgs the error arguments, for argument binding via MessageFormat
     * (can be {@code null})
     * @param defaultMessage fallback default message
     */
    public static void rejectIfEmptyOrWhitespace(
            Errors errors, String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) {

        Assert.notNull(errors, "Errors object must not be null");
        Object value = errors.getFieldValue(field);
        if (value == null ||!StringUtils.hasText(value.toString())) {
            errors.rejectValue(field, errorCode, errorArgs, defaultMessage);
        }
    }

}
View Code

ValidationUtils類有助於編寫Spring驗證器,好比檢測一個name屬性是否爲null或者是「」字符串,不須要像下面這樣編寫:

if(name== null || name.isEmpty()){
   errors.rejectValue("name","name.required");
}

而是能夠利用類的rejectIfEmpty()方法,像下面這樣:

ValidationUtils.rejectIfEmpty(errors,"name","name.required");  

其中,"name"是屬性名,"name.required"是錯誤代碼。

或者下面這樣的代碼:

if(name== null || name.trim().isEmpty()){
   errors.rejectValue("name","name.required");  
}

能夠編寫成:

ValidationUtils.rejectIfEmptyOrWhitespace("name");

下面是ValidationUtils中rejectIfEmpty()和rejectIfEmptyOrWhitespace()方法的方法重載:

    public static void rejectIfEmpty(Errors errors, String field, String errorCode) {
        rejectIfEmpty(errors, field, errorCode, null, null);
    }

    public static void rejectIfEmpty(Errors errors, String field, String errorCode, String defaultMessage) {
        rejectIfEmpty(errors, field, errorCode, null, defaultMessage);
    }

    public static void rejectIfEmpty(Errors errors, String field, String errorCode, Object[] errorArgs) {
        rejectIfEmpty(errors, field, errorCode, errorArgs, null);
    }

    public static void rejectIfEmpty(Errors errors, String field, String errorCode,
            @Nullable Object[] errorArgs, @Nullable String defaultMessage) {

        Assert.notNull(errors, "Errors object must not be null");
        Object value = errors.getFieldValue(field);
        if (value == null || !StringUtils.hasLength(value.toString())) {
            errors.rejectValue(field, errorCode, errorArgs, defaultMessage);
        }
    }

    public static void rejectIfEmptyOrWhitespace(Errors errors, String field, String errorCode) {
        rejectIfEmptyOrWhitespace(errors, field, errorCode, null, null);
    }
    public static void rejectIfEmptyOrWhitespace(
            Errors errors, String field, String errorCode, String defaultMessage) {

        rejectIfEmptyOrWhitespace(errors, field, errorCode, null, defaultMessage);
    }
    public static void rejectIfEmptyOrWhitespace(
            Errors errors, String field, String errorCode, @Nullable Object[] errorArgs) {

        rejectIfEmptyOrWhitespace(errors, field, errorCode, errorArgs, null);
    }

    public static void rejectIfEmptyOrWhitespace(
            Errors errors, String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) {

        Assert.notNull(errors, "Errors object must not be null");
        Object value = errors.getFieldValue(field);
        if (value == null ||!StringUtils.hasText(value.toString())) {
            errors.rejectValue(field, errorCode, errorArgs, defaultMessage);
        }
    }

此外,ValidationUtils還有一個invokeValidator()方法,用來代用驗證器:

    public static void invokeValidator(
            Validator validator, Object target, Errors errors, @Nullable Object... validationHints) {

        Assert.notNull(validator, "Validator must not be null");
        Assert.notNull(target, "Target object must not be null");
        Assert.notNull(errors, "Errors object must not be null");

        if (logger.isDebugEnabled()) {
            logger.debug("Invoking validator [" + validator + "]");
        }
        if (!validator.supports(target.getClass())) {
            throw new IllegalArgumentException(
                    "Validator [" + validator.getClass() + "] does not support [" + target.getClass() + "]");
        }

        if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
            ((SmartValidator) validator).validate(target, errors, validationHints);
        }
        else {
            validator.validate(target, errors);
        }

        if (logger.isDebugEnabled()) {
            if (errors.hasErrors()) {
                logger.debug("Validator found " + errors.getErrorCount() + " errors");
            }
            else {
                logger.debug("Validator found no errors");
            }
        }
    }

接下來將經過範例來介紹如何使用這個工具。

四 Spring的Validator範例

本節將會建立一個spring-validator應用,該應用包含一個名爲ProductValidator的驗證器,用於驗證Product對象。

一、目錄結構

二、Product類

package domain;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;

public class Product implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
    private String description;
    private BigDecimal price; private LocalDate productionDate; public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public BigDecimal getPrice() {
        return price;
    }
    public void setPrice(BigDecimal price) {
        this.price = price;
    }
    public LocalDate getProductionDate() {
        return productionDate;
    }
    public void setProductionDate(LocalDate productionDate) {
        this.productionDate = productionDate;
    }
    
}

三、Formatter

爲了使ProductForm.jsp頁面中表單輸入的日期可使用不一樣於當前語言區域的日期樣式,,咱們建立了一個LocalDateFormatter 類:

package formatter;

import java.text.ParseException;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Locale;

import org.springframework.format.Formatter;

public class LocalDateFormatter implements Formatter<LocalDate> {

    private DateTimeFormatter formatter;
    private String datePattern;

    // 設定日期樣式
    public LocalDateFormatter(String datePattern) {
        this.datePattern = datePattern;
        formatter = DateTimeFormatter.ofPattern(datePattern);
    }

    //利用指定的Locale將一個LocalDate解析成String類型
    @Override
    public String print(LocalDate date, Locale locale) {
        return date.format(formatter);
    }

    //利用指定的Locale將一個String解析成LocalDate類型
    @Override
    public LocalDate parse(String s, Locale locale) throws ParseException {
        try {
            //使用指定的formatter從字符串中獲取一個LocalDate對象  若是字符串不符合formatter指定的樣式要求,轉換會失敗
            return LocalDate.parse(s, DateTimeFormatter.ofPattern(datePattern));
        } catch (DateTimeParseException e) {
            // the error message will be displayed in <form:errors>
            throw new IllegalArgumentException(
                    "invalid date format. Please use this pattern\""
                            + datePattern + "\"");
        }
    }
}

LocalDateFormatter 類的parse()方法,它利用傳給構造器的日期樣式,將一個String轉換成LocalDate。

若是輸入的日期格式有問題,將會拋出IllegalArgumentException異常,這代表如下代碼中input標籤綁定到表單支持對象的birthDate屬性出現錯誤:

        <p>
            <label for="productionDate">*Production Date (MM-dd-yyyy): </label>
            <form:input id="productionDate" path="productionDate" tabindex="4"/>
        </p>

在/save-product頁面對應的請求處理方法saveEmployee()中,bindingResult參數將會記錄到這個綁定錯誤,即類型轉換錯誤。

四、Validator

該應用包含一個名爲ProductValidator的驗證器:

package validator;

import java.math.BigDecimal;
import java.time.LocalDate;

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

import domain.Product;

public class ProductValidator implements Validator {

    @Override
    public boolean supports(Class<?> klass) {
        //支持Product類?
        return Product.class.isAssignableFrom(klass);
    }

    //將目標對象target的錯誤註冊到errors對象中
    @Override
    public void validate(Object target, Errors errors) {
        //強制類型轉換
        Product product = (Product) target;
        
        //若是目標對象的name屬性爲null,或者爲""字符串,則將錯誤註冊到errors對象
        ValidationUtils.rejectIfEmpty(errors, "name", "productName.required");
        //若是目標對象的price屬性爲null,或者爲""字符串,則將錯誤註冊到errors對象中
        ValidationUtils.rejectIfEmpty(errors, "price", "price.required");
        //若是目標對象的productionDate屬性爲null,或者爲""字符串,則將錯誤註冊到errors對象中
        ValidationUtils.rejectIfEmpty(errors, "productionDate", "productionDate.required");
        
        BigDecimal price = product.getPrice();
        
        //若是價格爲負數 則將錯誤註冊到errors對象中
        if (price != null && price.compareTo(BigDecimal.ZERO) < 0) {
            errors.rejectValue("price", "price.negative");
        }
        //若是產品日期在今天以後 則將錯誤註冊到errors對象中
        LocalDate productionDate = product.getProductionDate();
        if (productionDate != null) {
            if (productionDate.isAfter(LocalDate.now())) {
                errors.rejectValue("productionDate", "productionDate.invalid");
            }
        }
    }
}

ProductValidator驗證器是一個很是簡單的驗證器。它的validate()方法會檢驗Product是否有名稱和價格,而且價格是否不爲負數,它還會確保生產日期不晚於今天。

五、Controller類

在Controller類中經過實例化validator類,可使用Spring驗證器。

package controller;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;

import domain.Product;
import validator.ProductValidator;

@Controller
public class ProductController {

    private static final Log logger = LogFactory
            .getLog(ProductController.class);

    @RequestMapping(value = "/add-product")
    public String inputProduct(Model model) {
        model.addAttribute("product", new Product());
        return "ProductForm";
    }

    @RequestMapping(value = "/save-product")
    public String saveProduct(@ModelAttribute Product product,
            BindingResult bindingResult, Model model) {
        //建立一個ProductValidator,並調用其validate()方法校驗Product對象,並將驗證錯誤填入bindingResult中。
        ProductValidator productValidator = new ProductValidator();
        productValidator.validate(product, bindingResult);

        if (bindingResult.hasErrors()) {
            FieldError fieldError = bindingResult.getFieldError();
            logger.debug("Code:" + fieldError.getCode() + ", field:"
                    + fieldError.getField());
            return "ProductForm";
        }

        // save product here
        //model.addAttribute("product", product);
        return "ProductDetails";
    }
}    

ProductController類的saveProduct()方法,有三個參數:

  • 第一個參數product,使用了註解@ModelAttribute,該對象的各個屬性被用來接受表單的各個字段信息,而且將"product"屬性添加到Model對象中;
  • 第二個參數bindingResult中設置了Spring全部的綁定錯誤(主要是類型轉換問題,例如將表單String轉換爲LocalDate類型);
  • 第三個參數是Model。

注意:BindingResult接口是Errors接口的子類,在請求處理方法的簽名中使用了BindingResult參數,就是告訴Spring關於表單對象數據校驗的錯誤將由咱們本身來處理,不然Spring會直接拋出異常。

該方法建立一個ProductValidator,並調用其validate()方法校驗Product對象,並將驗證錯誤填入bindingResult中。

ProductValidator productValidator = new ProductValidator();
productValidator.validate(product, bindingResult);

爲了檢驗該驗證器是否生成錯誤消息,須要在BindingResult中調用hasErrors()方法:

 if (bindingResult.hasErrors()) {
            FieldError fieldError = bindingResult.getFieldError();
            logger.debug("Code:" + fieldError.getCode() + ", field:"
                    + fieldError.getField());
            return "ProductForm";
        }

若是存在表單綁定錯誤或者是輸入驗證錯誤,將會打印出錯誤相關的字段,並重定位到ProductForm.jsp頁面。

若是表單輸入的數據均合法,則會重定位到ProductDetails.jsp頁面。

使用Spring驗證器的另外一種方法是:在Controller中編寫initBinder()方法,並將驗證器傳到WebDataBinder,並調用其validate()方法:

@org.springframework.web.bind.annotation.InitBinder
public void initBinder(WebDataBinder binder){
    //this will apply the Validator to all request-handling methods
    binder.setValidator(new ProductValidator)();
    binder.validate();  
}

將驗證器傳到WebDataBinder,會使該驗證器應用於Controller類中的全部請求處理的方法。

或者利用@javax.validation.Valid對要驗證的對象參數進行註解,例如:

  public String saveProduct(@Valid @ModelAttribute Product product,
            BindingResult bindingResult, Model model) {

注意:這種寫法不須要編寫validator,可是須要使用JSR 303註解類型進行字段校驗,此外,Valid註解類型也是在JSR 303中定義的,關於JSR 303的相關信息,後面介紹。

六、視圖

spring-validator應用包含三個視圖文件:

ProductForm.jsp:

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE HTML>
<html>
<head>
<title>Add Product Form</title>
<style type="text/css">@import url("<c:url value="/css/main.css"/>");</style>
</head>
<body>

<div id="global">
<form:form modelAttribute="product" action="save-product" method="post">
    <fieldset>
        <legend>Add a product</legend>
   <p class="errorLine"> <form:errors path="name" cssClass="error"/> </p>
        <p>
            <label for="name">*Product Name: </label>
            <form:input id="name" path="name" tabindex="1"/>
        </p>
        <p>
            <label for="description">Description: </label>
            <form:input id="description" path="description" tabindex="2"/>
        </p>
 <p class="errorLine"> <form:errors path="price" cssClass="error"/> </p>
        <p>
            <label for="price">*Price: </label>
            <form:input id="price" path="price" tabindex="3"/>
        </p>
   <p class="errorLine"> <form:errors path="productionDate" cssClass="error"/> </p>
        <p>
            <label for="productionDate">*Production Date (MM-dd-yyyy): </label>
            <form:input id="productionDate" path="productionDate" tabindex="4"/>
        </p>
        <p id="buttons">
            <input id="reset" type="reset" tabindex="5">
            <input id="submit" type="submit" tabindex="6" 
                value="Add Product">
        </p>
    </fieldset>
</form:form>
</div>
</body>
</html>

ProductForm.jsp視圖中咱們使用到了表單標籤,而且使用了errors標籤。下面詳細介紹errors的用途:

  1. 當經過瀏覽器訪問http://localhost:8008/spring-validator/add-product,將會調用Controller類的請求處理方法inputProduct(),返回ProductForm.jsp視圖;
  2. 當表單中輸入有非法數據時,提交數據到save-product,將會發生表單綁定錯誤或者是輸入驗證錯誤,這些信息都會被填入請求處理方法saveProduct()方法的bindingResult參數中;
  3. saveProduct()方法將請求轉發到ProductForm.jsp頁面時,而後就能夠利用erros標籤(能夠把其看作bindingResult參數)將path指定的屬性的錯誤消息顯示出來。

若是想要從某個屬性文件中獲取錯誤消息,則須要經過聲明messageSource.bean。告訴Spring要去哪裏查找這個文件。下面是springmvc-config.xml中的messageSource.bean:

    <bean id="messageSource" 
            class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
        <property name="basename" value="/WEB-INF/resource/messages" />
    </bean>

配置中須要注意的地方:

  •  ReloadableResourceBundleMessageSource :spring中提供的信息源配置類,支持proerties和xml文件,更改配置無需重啓服務,basename指定文件位置和名稱(可以使用classpath前綴),spring中首先查找.properties後綴文件,找不到再查找.xml後綴文件。

這個bean其實是說,錯誤碼和錯誤信息能夠在/WEB-INF/resource/messages.properties文件中找到:

productname.required=Please enter a product name
price.required=Please enter a price
price.negative=Price cannot be less than 0
productionDate.required=Please enter a production date
productionDate.invalid=Please ensure the production date is not later than today
typeMismatch.productionDate=Invalid production date

每一行表明一個錯誤,格式爲:

errorCode=defaultMessage

若是是驗證錯誤(validator),錯誤碼通常就是errors.rejectValue()方法中errorCode參數;若是是類型轉換錯誤,錯誤碼通常就是:typeMismatch.屬性名。

ProductDetails.jsp:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE HTML>
<html>
<head>
<title>Save Product</title>
<style type="text/css">@import url("<c:url value="/css/main.css"/>");</style>
</head>
<body>
<div id="global">
    <h4>The product has been saved.</h4>
    <p>
        <h5>Details:</h5>
        Product Name: ${product.name}<br/>
        Description: ${product.description}<br/>
        Price: $${product.price}
    </p>
</div>
</body>
</html>
View Code

ProductView.jsp(沒用到):

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE HTML>
<html>
<head>
<title>View Product</title>
<style type="text/css">@import url("<c:url value="/css/main.css"/>");</style>
</head>
<body>
<div id="global">
    <h4>${message}</h4>
    <p>
        <h5>Details:</h5>
        Product Name: ${product.name}<br/>
        Description: ${product.description}<br/>
        Price: $${product.price}
    </p>
</div>
</body>
</html>
View Code

 main.css:

#global {
    text-align: left;
    border: 1px solid #dedede;
    background: #efefef;
    width: 560px;
    padding: 20px;
    margin: 100px auto;
}

form {
  font:100% verdana;
  min-width: 500px;
  max-width: 600px;
  width: 560px;
}

form fieldset {
  border-color: #bdbebf;
  border-width: 3px;
  margin: 0;
}

legend {
    font-size: 1.3em;
}

form label { 
    width: 250px;
    display: block;
    float: left;
    text-align: right;
    padding: 2px;
}

#buttons {
    text-align: right;
}
#errors, li {
    color: red;
}
.error {
    color: red;
    font-size: 9pt;    
}
.errorLine {
    text-align: center;
}
View Code

七、配置文件

下面給出springmvc-config.xml文件的全部內容:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd     
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="controller" />
    <context:component-scan base-package="formatter" />
    <mvc:annotation-driven conversion-service="conversionService" />
    <mvc:resources mapping="/css/**" location="/css/" />
    <mvc:resources mapping="/*.html" location="/" />

    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    </bean>
    
    <bean id="messageSource" 
            class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
        <property name="basename" value="/WEB-INF/resource/messages" />
    </bean>
    
    <bean id="conversionService"
        class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="formatters">
            <set>
                <bean class="formatter.LocalDateFormatter">
                    <constructor-arg type="java.lang.String" value="MM-dd-yyyy" />
                </bean>
            </set>
        </property>
    </bean>
</beans>

部署描述符(web.xml文件):

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1" 
        xmlns="http://xmlns.jcp.org/xml/ns/javaee" 
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee 
            http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"> 
    <servlet>
        <servlet-name>springmvc</servlet-name>
        <servlet-class>
            org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/config/springmvc-config.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>    
    </servlet>

    <servlet-mapping>
        <servlet-name>springmvc</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

八、測試

部署項目,並在瀏覽器輸入:

http://localhost:8008/spring-validator/add-product

試着輸入一個無效的日期,將會跳轉到/save--product,可是表單內容不會丟失,而且會在表單中看到錯誤的消息:

能夠看到因爲輸入的日期沒有按照MM-dd-yyyy的格式,因此將String轉換LocalDate類型時,將會發生typeMismatch.productionDate錯誤,Errors標籤將會將messages.properties中設置的errorCode對應的錯誤消息顯示出來。

可是若是將Spring MVC配置文件中聲明的messageSource bean刪除,將會提示錯誤碼typeMismatch.productionDate對應的默認系統錯誤消息。

可是針對驗證器中咱們本身設定的errorCode,咱們必須在messages.properties指定其對應的錯誤消息,並配置messageSource bean.

 

五 JSR303驗證

JSR 303 是 JAVA EE 6 中的一項子規範,叫作 Bean Validation。 JSR 303 用於對 Java Bean 中的字段的值進行驗證。

當前,JSR只是一個規範文檔,自己用處不大,除非編寫了它的實現。用於實現JSR Bean Validation,目前有兩個實現:

JSR 303不須要編寫驗證器,但要利用JSR 303註解類型嵌入約束。JSR 303約束見表:

屬性 描述 範例
@AssertFalse 驗證 Boolean 對象是否爲 false

@AssertFalse

boolean hasChildren;

@AssertTrue  驗證 Boolean 對象是否爲 true  

@AssertTrue

boolean isEmpty;

@DecimalMax 被標註的值必須不大於約束中指定的最大值. 這個約束的參數是一個經過BigDecimal定義的最大值的字符串表示.小數存在精度

@DecimalMax("1.1")

BigDecimal price;

@DecimalMin 被標註的值必須不小於約束中指定的最小值. 這個約束的參數是一個經過BigDecimal定義的最小值的字符串表示.小數存在精度

@DecimalMin("0.04")

BigDecimal price;

@Digits(integer=,fraction=) 驗證字符串是不是符合指定格式的數字,interger指定整數精度,fraction指定小數精度。

@Digits(integer=5,fraction=2)

BigDecimal price;

@Future 驗證 Date 和 Calendar 對象是否在當前時間以後 

@Future

Date shippingDate;

@Max 驗證 Number 和 String 對象是否小等於指定的值

@MAX(150)

int age;

@Min 驗證 Number 和 String 對象是否大等於指定的值  

@Min(30)

int age;

@NotNull 驗證對象是否不爲null, 沒法查檢長度爲0的字符串

@NotNull

String testName;

@Null 驗證對象是否爲null

@Null

String testString;

@Past 驗證 Date 和 Calendar 對象是否在當前時間以前  

@Past

Date birthDate;

@Pattern 驗證 String 對象是否符合正則表達式的規則

@Pattern(regext="\\d{3}")

String areaCode;

@Size 驗證對象(Array,Collection,Map,String)長度是否在給定的範圍以內  

@Size(min=2,max=140)

String description;

更多JSR303定義的校驗類型能夠參考:使用JSR-303進行校驗 @Valid

一旦瞭解了JSR 303 validation的使用方法,使用起來會比Spring驗證器還要容易。像使用Spring驗證器同樣,能夠在屬性文件中如下列格式來使用property鍵,覆蓋來自JSR 303驗證器的錯誤消息:

constraint.object.property

例如,爲了覆蓋以@Size註解約束的Product對象的name,能夠在屬性文件中使用下面這個鍵:

Size.Product.name

爲了覆蓋以@Past註解約束的Product對象的productionDate屬性,能夠在屬性文件中使用下面這個鍵:

Past.Product.productionDate

六 JSR 303 Validator範例

jsr303-validator應用展現了JSR 303輸入驗證的例子。這個例子是對spring-validator進行修改以後的版本,與以前版本有一些區別。首先,它沒有ProductValidator類。

其次,咱們使用JSR Bean Validation實現是Hiberanate Validator,須要引入如下4個jar包:

JSR規範定義的註解類型在validation-api下javax.validation.constraints包下,有興趣能夠本身查看。

下面咱們主要給出jsr303-validator應用與spring-validator應用的不一樣之處,相同部分代碼再也不重複,能夠參考spring-validator應用。

一、目錄結構

其中lib庫文件以下:

二、Product類

Product類的name和productionDate字段已經用JSR 303註解類型進行了註解:

package domain;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;

import javax.validation.constraints.Past;
import javax.validation.constraints.Size;

public class Product implements Serializable {
    private static final long serialVersionUID = 78L;

    @Size(min=1, max=10) private String name;
    
    private String description;
    private BigDecimal price;
    
   @Past private LocalDate productionDate;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public BigDecimal getPrice() {
        return price;
    }
    public void setPrice(BigDecimal price) {
        this.price = price;
    }
    public LocalDate getProductionDate() {
        return productionDate;
    }
    public void setProductionDate(LocalDate productionDate) {
        this.productionDate = productionDate;
    }

}

三、ProductController類

在ProductController類的saveProduct()方法中,必須用@Valid對Product參數進行註解:

package controller;

import javax.validation.Valid;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;

import domain.Product;

@Controller
public class ProductController {

    private static final Log logger = LogFactory
            .getLog(ProductController.class);

    @RequestMapping(value = "/add-product")
    public String inputProduct(Model model) {
        model.addAttribute("product", new Product());
        return "ProductForm";
    }

    @RequestMapping(value = "/save-product")
    public String saveProduct(@Valid @ModelAttribute Product product,
            BindingResult bindingResult, Model model) {

        if (bindingResult.hasErrors()) {
            FieldError fieldError = bindingResult.getFieldError();
            logger.info("Code:" + fieldError.getCode() + ", object:"
                    + fieldError.getObjectName() + ", field:"
                    + fieldError.getField());
            return "ProductForm";
        }

        // save product here

        model.addAttribute("product", product);
        return "ProductDetails";
    }

}

爲了定製來自驗證器的錯誤消息,要在messages.properties文件中使用兩個鍵:

typeMismatch.productionDate=Invalid production date
Past.product.productionDate=Production date must be a past date Size.product.name=Product name's size must be between 1 and 10  

四、測試

想要測試jsr303-validator中的驗證器,能夠在瀏覽器中打開如下網址:

http://localhost:8008/jsr303-validator/add-product

輸入如下內容,並提交,能夠看到頁面中提示了錯誤信息:

若是數據輸入合法:

參考文章

[1]從源碼分析java.lang.String.isEmpty()

[2]SpringMVC介紹之Validation

[3]Spring MVC學習指南

[4]使用JSR-303進行校驗 @Valid

[5]JSR 303 - Bean Validation 介紹及最佳實踐

[6]spring MVC 使用 hibernate validator驗證框架,國際化配置

相關文章
相關標籤/搜索