输入验证是Spring处理的最重要Web开发任务之一。在Spring MVC中,有两种方式可以验证输入,即利用Spring自带的验证框架,或者利用JSR 303实现。本篇博客将介绍这两种输入验证方法。

本篇博客用两个不同的示例分别介绍这两种方式:spring-validator和jsr303-validator。

一 验证概览

Converter和Formatter作用于字段级。在MVC Web应用中,它们将String类型转换或格式化成另一种Java类型,如java.time.LocalDate。验证器则作用于对象级。它决定某一个对象中的所有字段是否均是有效的,以及是否遵循某些规则。一个典型的Spring MVC应用会同时应用到Formatter(或Converter)和Validator。

如果一个应用程序既使用了Formatter,又有了Validator,那么,应用中的事件顺序是这样的:在调用Controller的请求处理方式时,将会有一个或者多个Formatter,试图将输入字符串转换成domain对象中的属性(或者说字段)值,一旦格式化成功,验证器就会介入。

例如:Order对象有一个shippingDate属性(其类型为LocalDate),它的值绝对不可能早于今天的日期。当调用OrderController时,LocalDateFormatter会将字符串转换成LocalDate,并将它赋予Order对象的shippingDate属性。如果转换失败,用户就会被转回到前一个表单;如果转换成功,则会调用验证器,查看shippingDate是否早于今天的日期。

现在,你或许会问,将验证逻辑转移到LocalDateFormatter中是否更加明智?

因为比较一下日期并非难事,但答案却是肯定的。首先,LocalDateFormatter还可以用于将其它字符串格式化成日期,如birthDate或者purchaseDate。这两个日期的规则都不同于shippingDate,事实上,比如,员工的出生日期绝对不可能晚于今日。

其次,校验器可以检查两个或更多字段之间的关系,各字段均受不同的Formatter支持。例如,假设Employee对象有birthDate属性和startDate属性,验证器就可以设定规则,使任何员工的入职日期均不可能早于他的出生日期。因此,有效的Employee对象必须让它的birthDate属性值早于其startDate值,这就是验证器的任务。

二 Spring验证器

从一开始,Spring就设计了输入验证,甚至早于JSR 303(Java验证规范)。因此,Spring的Validation框架至今都很普遍,对于新项目,一般建议使用JSR 303验证器。

为了创建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); }

这个接口需要实现两个方法: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); }

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

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

编写验证器时,不需要直接创建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);
}
} }

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对象。

1、目录结构

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

3、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参数将会记录到这个绑定错误,即类型转换错误。

4、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是否有名称和价格,并且价格是否不为负数,它还会确保生产日期不晚于今天。

5、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的相关信息,后面介绍。

6、视图

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>

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>

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

7、配置文件

下面给出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>

8、测试

部署项目,并在浏览器输入:

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应用。

1、目录结构

其中lib库文件如下:

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

3、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

4、测试

想要测试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验证框架,国际化配置

Spring MVC -- 验证器的更多相关文章

  1. SSM(spring mvc+spring+mybatis)学习路径——2-2、spring MVC拦截器

    目录 2-2 Spring MVC拦截器 第一章 概述 第二章 Spring mvc拦截器的实现 2-1 拦截器的工作原理 2-2 拦截器的实现 2-3 拦截器的方法介绍 2-4 多个拦截器应用 2- ...

  2. 写的太细了!Spring MVC拦截器的应用,建议收藏再看!

    Spring MVC拦截器 拦截器是Spring MVC中强大的控件,它可以在进入处理器之前做一些操作,或者在处理器完成后进行操作,甚至是在渲染视图后进行操作. 拦截器概述 对于任何优秀的MVC框架, ...

  3. spring mvc 拦截器的使用

    Spring MVC 拦截器的使用 拦截器简介 Spring MVC 中的拦截器(Interceptor)类似于 Servler 中的过滤器(Filter).用于对处理器进行预处理和后处理.常用于日志 ...

  4. 【Java Web开发学习】Spring MVC 拦截器HandlerInterceptor

    [Java Web开发学习]Spring MVC 拦截器HandlerInterceptor 转载:https://www.cnblogs.com/yangchongxing/p/9324119.ht ...

  5. Spring Boot 2.X(九):Spring MVC - 拦截器(Interceptor)

    拦截器 1.简介 Spring MVC 中的拦截器(Interceptor)类似于 Servlet 开发中的过滤器 Filter,它主要用于拦截用户请求并作相应的处理,它也是 AOP 编程思想的体现, ...

  6. Spring MVC拦截器配置

    Spring MVC拦截器配置 (1)自定义拦截器 package learnspringboot.xiao.other; import org.springframework.web.servlet ...

  7. 对于Spring MVC 拦截器的一些了解

    Spring MVC 拦截器的执行顺序 应用场景 假设请求 localhost:8080/ 则要求直接重定向到 localhost:8080/login ; 定义拦截器顺序 permission lo ...

  8. Spring MVC拦截器浅析

    Spring MVC拦截器 重点:Spring MVC的拦截器只会拦截控制器的请求,如果是jsp.js.image.html则会放行. 什么是拦截器 运行在服务器的程序,先于Servlet或JSP之前 ...

  9. Spring MVC拦截器+注解方式实现防止表单重复提交

    原理:在新建页面中Session保存token随机码,当保存时验证,通过后删除,当再次点击保存时由于服务器端的Session中已经不存在了,所有无法验证通过. 注,如果是集群的方式,则需要将token ...

随机推荐

  1. mysql where 1

    where后跟各种查询条件,当条件为真时即可查询出记录.在这里where 1,1为真,也就是where后的条件为真,查询表中所有内容. SELECT * FROM `sdb_pam_members` ...

  2. (安全之路)从头开始学python编程之文件操作

    0x00 python学习路径 b站(哔哩哔哩)视频,w3cschool(详情百度),官方文档,各大群内获取资料等等方式 0x01 python的学习要点 open()函数:有两个参数,文件名跟模式, ...

  3. 如何查看自己steam库里游戏是哪个区的

    1 开启Steam开发者模式,切换到控制台,以便调出游戏区域数据 1.1 首先找到Steam的快捷方式,在目标一行中最后输入 -dev (前面带空格),然后重新运行. 1.2 如下图上方标签切换到控制 ...

  4. expect工具实现脚本的自动交互

    1 安装expect工具 expect是建立在tcl基础上的一个自动化交互套件, 在一些需要交互输入指令的场景下, 可通过脚本设置自动进行交互通信. 其交互流程是: spawn启动指定进程 -> ...

  5. python开发笔记-变长字典Series的使用

    Series的基本特征: 1.类似一维数组的对象 2.由数据和索引组成 import pandas as pd >>> aSer=pd.Series([1,2.0,'a']) > ...

  6. test20190905 ChiTongZ

    100+22+90=212.前两道题不错,但T3 没什么意义. 围观刘老爷超强 T1 解法. ChiTongZ的水题赛 [题目简介] 我本可以容忍黑暗,如果我不曾见过太阳. 考试内容略有超纲,不超纲的 ...

  7. lis框架各种方法的使用

    //这个必须是lpedorapp表的主键才行LPEdorAppDB tLPEdorAppDB = new LPEdorAppDB();tLPEdorAppDB.setEdorAcceptNo(mEdo ...

  8. P2915 [USACO08NOV] Mixed Up Cows

    题目描述 Each of Farmer John's N (4 <= N <= 16) cows has a unique serial number S_i (1 <= S_i & ...

  9. python - 手机号正则匹配

    Python 手机号正则匹配 # -*- coding:utf-8 -*- import re def is_phone(phone): phone_pat = re.compile('^(13\d| ...

  10. LeetCode 1048. Longest String Chain

    原题链接在这里:https://leetcode.com/problems/longest-string-chain/ 题目: Given a list of words, each word con ...