SpringMVC——类型转换和格式化、数据校验、客户端显示错误消息

在介绍类型转换和格式化之前,我首先来介绍 <mvc:annotation-driven />

需要导入的 schema:

xmlns:mvc="http://www.springframework.org/schema/mvc"

一、作用:

1.会自动注册 RequestMappingHandlerMapping、RequestMappingHandlerAdapter 以及 ExceptionHandlerExceptionResolver 三个 Bean。

若配置该注解后,对于一般的 springmvc 请求来说,不再使用未配置之前的过期的 AnnotationHandlerMapping 和 AnnotationMethodHandlerAdapter。

而是使用 RequestMappingHandlerMapping、RequestMappingHandlerAdatapter,作为对 AnnotationHandlerMapping 和 AnnotationHandlerAdapter 的一种替代。。

所以 DispatcherServlet 中的 HandlerAdapter 的 handler() 方法发生了改变,相当于一套新的逻辑。

清晰内容,请参见:

AnnotationMethodHandlerAdapter 下的 springmvc 运行流程分析。

RequestMappingHandlerAdapter 下的 springmvc 运行流程分析。

2. 支持使用 ConversionService 实例对表单参数进行类型转换。详细内容请参见:类型转换和格式化

3.支持使用 @Valid 对 java bean 进行 JSR-303 校验。详细内容请参见:数据校验

4.支持使用 @RequestBody 和 @ResponseBody 注解。详细内容请参见:springmvc 对 Ajax 的支持。

二、详细分析

添加 <mvc:annotation-driven /> 配置后:

默认情况下:

HandlerMapping 注册了 RequestMappingHandlerMapping 和 BeanNameUrlHandlerMapping。

HandlerAdapter 注册了 RequestMappingHandlerAdapter 和 HttpReqestHandlerAdapter 和 SimpleControllerHandlerAdapter。

 

org.springframework.beans.factory.xml.BeanDefinitionParser 用来解析 <beans/> 标签的。

<mvc:annotation-driven /> 属于 <beans/> 的子节点,由 org.springframework.web.servlet.config.AnnotationDrivenBeanDefinitionParser进行解析。

这个类注册了下面两个 HandlerMapping:RequestMappingHandlerMapping 和 BeanNameUrlHandlerMapping。

此外,也可以通过 <mvc:resources mapping="" location=""/>或 <mvc:view-controller path=""/> 来指定需要被注册的 HandlerMapping。

同时注册了三个 HandlerAdatper:RequestMappingHandlerAdapter,HttpRequestHandlerAdapter,SimpleControllerHandlerAdapter。

同时注册了三个 HandlerExceptionResolver:ExceptionHandlerExceptionResolver,ResponseStatusExceptionResolver和DefaultHandlerExceptionResolver。

 

RequestMappingHandlerAdapter 和 ExceptionHandlerExceptionResolver 作为一个默认的配置,由下列实例指定:

ContentNegotiationManager

DefaultFormattingConversionService

LocalValidatorFactoryBean——支持 JSR303

HttpMessageConverter

 

并在 org.springframework.web.servlet.config.AnnotationDrivenBeanDefinitionParser#parse 这里进行的注册。

并且发现 RequestMappingHandlerMapping 的 order 为 0,BeanNameUrlHandlerMapping 的 order 为2。所以请求先去映射 RequestMappingHandlerMapping。

 

上面介绍了 <mvc:annotation-driven /> 。下面介绍类型转换和格式化、数据校验。

一、在介绍每个模块之前,首先对整体流程有个清晰的认识。

SpringMVC 通过反射机制对目标方法的签名进行分析,将请求消息绑定到处理方法入参中。

SpringMVC 将 ServletReqeust 对象以及处理方法入参对象实例传递给 DataBinder,DataBinder 调用装配在 springmvc 上下文的 ConversionService 进行类型转换和格式化,

将请求信息填充到入参对象中,然后调用 Validator 组件对已入参的对象进行数据合法性校验,并生成数据绑定结果 BindingResult 对象,若数据转换或数据格式化失败,或验证失败,

都会将失败的信息填充到 BinddingResult 对象中。

二、SpringMVC 的类型转换和格式化:

ConversionService 是 spring 类型转换体系的核心接口。位于 org.springframework.core.convert 包下,可以利用 org.springframework.context.support.ConversionServiceFactoryBean 

在 springmvc 上下文中配置一个 ConversionService 的实例。SpringMVC 配置文件会自动识别上下文中的 ConversionService,并在类型转换的时候使用它。

使用<mvc:annotation-driven /> 注解后:

默认会注册一个 ConversionService ,即 FromattingConversionServiceFactoryBean, 生产的 ConversionService 可以进行类型(日期和数字)的格式化。

若添加自定义类型转换器后,需要对其装配自定义的 ConversionService 。如:

<bean id="customizeConversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="com.nucsoft.springmvc.converter.PersonConversionService"/>
            <bean class="com.nucsoft.springmvc.converter.String2MapConversionService"/>
        </set>
    </property>
</bean>
<mvc:annotation-driven conversion-service="customizeConversionService"/>

这里装配的自定义 ConversionService 为 customerConversionService, 是由 ConversionServiceFactoryBean 生产的。

这里会有一个问题,由 ConversionServiceFactoryBean 生产的 ConversionService 会覆盖默认的 FormattingConversionServiceFactoryBean,会导致类型(日期和数字)的给石化出错。

Spring 中定义了一个 ConversionService 实现类 FormattingConversionService ,该类扩展于 GenericConversionService ,它既有类型转换也有格式化的功能。

FormattingConversionService 也拥有一个 FormattingConversionServiceFactoryBean 。通过在 Spring 上下文中构造一个 FormattingConversionService,

既可以注册自定义类型转换器,也可以注册自定义注解格式化。NumberFormatAnnotationFormatterFactoryBean、JodaDateTimeFormatAnnotationFormatFactory 

会自定注册到 FormattingConversionServiceFactoryBean 中。因此配置 FormattingConversionServiceFactoryBean 后,能很好的解决上述问题。如:

<bean class="org.springframework.format.support.FormattingConversionServiceFactoryBean" id="FormattingConversionService">
    <property name="converters">
        <set>
            <bean class="com.nucsoft.springmvc.converter.PersonConverster"/>
            <bean class="com.nucsoft.springmvc.converter.String2MapConverter"/>
        </set>
    </property>
</bean>
<mvc:annotation-driven conversion-service="FormattingConversionService"/>

来看一个具体的转换器:

/**
 * @author solverpeng
 * @create 2016-08-15-14:50
 */
public class PersonConverter implements Converter<String, Person>{
    Person person = null;
    @Override
    public Person convert(String s) {
        try {
            if(s != null && s.length() > 0) {
                String[] strings = s.split("\\|");
                person = Person.class.newInstance();
                for(String str : strings) {
                    String[] properties = str.split(":");
                    Field field = Person.class.getDeclaredField(properties[0]);
                    field.setAccessible(true);
                    Class<?> type = field.getType();
                    if(type.equals(Integer.class)) {
                        field.set(person, Integer.parseInt(properties[1]));
                        continue;
                    }
                    field.set(person, properties[1]);
                }
            }
        } catch(InstantiationException | IllegalAccessException | NoSuchFieldException e) {
            e.printStackTrace();
        }
        return person;
    }
}

可以看出,具体解决转换问题的是:一个个的 Converter。

在 core.convert.support 包下提供了许多默认的类型转化器,为类型转换提供和极大的方便。

这些类型转换器虽然包含了大部分常用类型的转换,但是有时候我们有些特殊需求,就需要自定义类型转换器。

1.自定义类型转换器:

(1)实现 Converter 接口

package org.springframework.core.convert.converter;
public interface Converter<S, T> {
      T convert(S source);
}

创建自定义类型转换器,只需要实现该接口,参数 S 表示需要转换的类型,T 表示转换后的类型。对于每次调用 convert() 方法,必须保证参数 source 不能为 null。

如果转换失败,可能会抛出异常。特别的,一个 IllegalArgumentException 会被抛出来指明无效的 source 值。

请注意,需要保证转换器是线程安全的。

e1: 需要将 person=name:lily|age:23 转换为对应的 person 对象

自定义的类型转换器:

/**
 * @author solverpeng
 * @create 2016-08-15-14:50
 */
public class PersonConverter implements Converter<String, Person>{
    Person person = null;
    @Override
    public Person convert(String s) {
        try {
            if(s != null && s.length() > 0) {
                String[] strings = s.split("\\|");
                person = Person.class.newInstance();
                for(String str : strings) {
                    String[] properties = str.split(":");
                    Field field = Person.class.getDeclaredField(properties[0]);
                    field.setAccessible(true);
                    Class<?> type = field.getType();
                    if(type.equals(Integer.class)) {
                        field.set(person, Integer.parseInt(properties[1]));
                        continue;
                    }
                    field.set(person, properties[1]);
                }
            }
        } catch(InstantiationException | IllegalAccessException | NoSuchFieldException e) {
            e.printStackTrace();
        }
        return person;
    }
}

在 SpringMVC 配置文件中添加如下配置:

<bean class="org.springframework.format.support.FormattingConversionServiceFactoryBean" id="FormattingConversionService">
    <property name="converters">
        <set>
            <bean class="com.nucsoft.springmvc.converter.PersonConverter"/>
        </set>
    </property>
</bean>
<mvc:annotation-driven conversion-service="FormattingConversionService"/>

请求:

<a href="testConverter?person=name:lily|age:23">test converter</a>

目标 handler 方法:

@RequestMapping("/testConverter")
public String testSpring2Person(Person person) {
    System.out.println("persont:" + person);
    return "success";
}

控制台输出:

persont:Person{name='lily', age=23} 

e2:在介绍参数获取问题是,对 @RequestParam 的 “如果方法的入参类型是一个 Map,不包含泛型类型,并且请求参数名称是被指定” 这种情况没有进行详细说明,这里通过一个例子说明。

将 String 转换为 Map,将 params=a:1|b:2 转换为 Map 类型。

自定义类型转换器:

/**
 * @author solverpeng
 * @create 2016-08-15-15:40
 */
public class String2MapConverter implements Converter<String, Map<String, Object>>{
    @Override
    public Map<String, Object> convert(String s) {
        Map<String, Object> map = new HashMap<>();
        if(s != null & s.length() > 0) {
            String[] strings = s.split("\\|");
            for(String string : strings) {
                String[] split = string.split(":");
                map.put(split[0], split[1]);
            }
        }
        return map;
    }
}

在 SpringMVC 配置文件中添加如下配置:

<bean class="org.springframework.format.support.FormattingConversionServiceFactoryBean" id="FormattingConversionService">
    <property name="converters">
        <set>
            <bean class="com.nucsoft.springmvc.converter.String2MapConverter"/>
        </set>
    </property>
</bean>
<mvc:annotation-driven conversion-service="FormattingConversionService"/>

请求:

<a href="testConverter2?params=a:1|b:2">test converter2</a>

目标 handler 方法:

@RequestMapping("/testConverter2")
public String testString2Map(@RequestParam("params") Map map) {
    System.out.println(map);
    return "success";
}

控制台输出:

{b=2, a=1}

(2)实现 ConverterFactory 

package org.springframework.core.convert.converter;
public interface ConverterFactory<S, R> {
     <T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

如果希望将一种类型转换为另一种类型及其子类对象时,那么使用这个接口。

e: num=23&num2=33.33 将 num 转换为对应的 Integer 类型,将 num2 转换为对应的 Double 类型。

类型转换器:org.springframework.core.convert.support.StringToNumberConverterFactory

final class StringToNumberConverterFactory implements ConverterFactory<String, Number> {

    @Override
    public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) {
        return new StringToNumber<T>(targetType);
    }

    private static final class StringToNumber<T extends Number> implements Converter<String, T> {

        private final Class<T> targetType;

        public StringToNumber(Class<T> targetType) {
            this.targetType = targetType;
        }

        @Override
        public T convert(String source) {
            if (source.length() == 0) {
                return null;
            }
            return NumberUtils.parseNumber(source, this.targetType);
        }
    }

}

请求:

<a href="testString2Number?num=23&num2=33.33">test String to Number</a>

目标 handler 方法:

@RequestMapping("/testString2Number")
public String testString2Number(@RequestParam("num") Integer num, @RequestParam("num2") Double num2) {
    System.out.println("num:" + num);
    System.out.println("num2:" + num2);
    return "success";
}

控制台输出:

num:23
num2:33.33

(3)还有一种 GenericConverter ,这里不对其进行说明。有兴趣的童鞋,可自行研究,用到的情况比较少。

2.格式化

这里所说的格式化,主要指的是日期和数字的格式化。SpringMVC 支持使用 @DateTimeFormat 和 @NumberFormat 来完成数据类型的格式化。看一个例子。

/**
 * @author solverpeng
 * @create 2016-08-16-11:14
 */
public class Employee {
    private String empName;
    private String email;
    private Date birth;
    private Double salary;

    public String getEmpName() {
        return empName;
    }

    public void setEmpName(String empName) {
        this.empName = empName;
    }

    @Email
    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Past
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    public Date getBirth() {
        return birth;
    }

    public void setBirth(Date birth) {
        this.birth = birth;
    }

    @NumberFormat(pattern = "#,###,###.##")
    public Double getSalary() {
        return salary;
    }

    public void setSalary(Double salary) {
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "  empName='" + empName + '\'' +
                ", email='" + email + '\'' +
                ", birth=" + birth +
                ", salary=" + salary +
                '}';
    }
}

请求:

<a href="testFormat?empName=lily&email=lily@bb.com&birth=1992-12-23&salary=1,234,567.89">test Format</a>

控制台输出:

employee:Employee{  empName='null', email='lily@bb.com', birth=Wed Dec 23 00:00:00 CST 1992, salary=1234567.89}

请求:

<a href="testFormat?empName=lily&email=lily@bb.com&birth=2992-12-23&salary=1,234,567.89">test Format</a>

控制台输出:

allError:Field error in object 'employee' on field 'birth': rejected value [Sun Dec 23 00:00:00 CST 2992]; codes [Past.employee.birth,Past.birth,Past.java.util.Date,Past];

arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [employee.birth,birth]; arguments []; default message [birth]]; default message [需要是一个过去的事件]

employee:Employee{ empName='null', email='lily@bb.com', birth=Sun Dec 23 00:00:00 CST 2992, salary=1234567.89}

二、SpringMVC 使用 DataBinder 进行数据的绑定。在类型转换和格式化之后,会进行数据的绑定。

三、SpringMVC 的数据校验:

Spring 4.0 之后,支持  Bean Validation 1.0(JSR-303)和 Bean Validation 1.1(JSR-349) 校验。同时也支持 Spring Validator 接口校验。

Spring 提供一个验证接口,你可以用它来验证对象。这个 Validator 接口使用一个 Errors 对象来工作,验证器验证失败的时候向 Errors 对象填充验证失败信息。

1.使用 Spring Validator 接口校验

(1)一个简单对象的验证

实体类:

/**
 * @author solverpeng
 * @create 2016-08-12-10:50
 */
public class Person {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
Person.java

创建验证器(即创建 Validator 的实现类)

/**
 * @author solverpeng
 * @create 2016-08-12-10:51
 */
public class PersonValidator implements Validator{
    /**
     * This Validator validates *just* for Person
     */
    @Override
    public boolean supports(Class<?> aClass) {
        return Person.class.equals(aClass);
    }

    @Override
    public void validate(Object o, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "name", "name.empty", "人名不能为空.");
        Person person = (Person) o;
        if(person.getAge() < 0) {
            errors.rejectValue("age", "negativevalue", "年龄不能为负数.");
        } else if(person.getAge() > 110) {
            errors.rejectValue("age", "too.darn.old", "年龄不得超过110岁.");
        }
    }
}

使用:

/**
 * @author solverpeng
 * @create 2016-08-12-10:49
 */
@Controller
public class TargetHandler {

    @InitBinder
    public void initBinder(DataBinder binder) {
        binder.setValidator(new PersonValidator());
    }

    @RequestMapping("/testPersonValidator")
    public String testPersonValidator(@Valid Person person, BindingResult result) {
        if(result.hasErrors()) {
            List<ObjectError> allErrors = result.getAllErrors();
            if(!CollectionUtils.isEmpty(allErrors)) {
                for(ObjectError allError : allErrors) {
                    System.out.println("error: " + allError.getDefaultMessage());
                }
            }
        }
        System.out.println(person);
        return "success";
    }

}

(2)一个复杂对象的验证

实体类:

/**
 * @author solverpeng
 * @create 2016-08-12-11:14
 */
public class Customer {
    private String firstName;
    private String surname;
    private Address address;

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }


    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "firstName='" + firstName + '\'' +
                ", surname='" + surname + '\'' +
                ", address=" + address +
                '}';
    }
}
Customer.java
/**
 * @author solverpeng
 * @create 2016-08-12-11:15
 */
public class Address {
    private String addressName;

    public String getAddressName() {
        return addressName;
    }

    public void setAddressName(String addressName) {
        this.addressName = addressName;
    }

    @Override
    public String toString() {
        return "Address{" +
                "addressName='" + addressName + '\'' +
                '}';
    }
}
Address.java

验证器类:

/**
 * @author solverpeng
 * @create 2016-08-12-11:16
 */
public class AddressValidator implements Validator{
    @Override
    public boolean supports(Class<?> clazz) {
        return Address.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "addressName", "field.required", "地址名称必须输入.");
    }
}
/**
 * @author solverpeng
 * @create 2016-08-12-11:18
 */
public class CustomerValidator implements Validator{
    private final Validator addressValidator;

    public CustomerValidator(Validator addressValidator) {
        if(addressValidator == null) {
            throw new IllegalArgumentException("the validator is required and must be null.");
        }
        if(!addressValidator.supports(Address.class)) {
            throw new IllegalArgumentException("the validator must be [Address] instance.");
        }

        this.addressValidator = addressValidator;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return Customer.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required", "firstName 必须输入.");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required", "surname 必须输入.");
        Customer customer = (Customer) target;

        try {
            errors.pushNestedPath("address");
            ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
        } finally {
            errors.popNestedPath();
        }
    }
}

使用

/**
 * @author solverpeng
 * @create 2016-08-12-10:49
 */
@Controller
public class TargetHandler {

    @InitBinder
    public void initBinder(DataBinder binder) {
        binder.setValidator(new CustomerValidator(new AddressValidator()));
    }

    @RequestMapping("/testCustomerValidator")
    public String testCustomerValidator(@Valid Customer customer, BindingResult result) {
        if(result.hasErrors()) {
            List<ObjectError> allErrors = result.getAllErrors();
            if(!CollectionUtils.isEmpty(allErrors)) {
                for(ObjectError error : allErrors) {
                    System.out.println("error: " + error.getDefaultMessage());
                }
            }
        }
        return "success";
    }

}

说明:对于复杂对象来说,创建验证器的时候,可以使用嵌套的方式

通过实现 Validator 接口的方式创建的验证器,不是通过配置吗,也不是通过注解来调用,而是通过 @InitBinder 标注的方法,进而去调每个验证器进行验证。

 

Spring 没有对 @Valid 添加某个属性来指定使用哪个验证器进行验证。所以下面这种情况会报异常。

/**
 * @author solverpeng
 * @create 2016-08-12-10:49
 */
@Controller
public class TargetHandler {

    @InitBinder
    public void initBinder(DataBinder binder) {
        binder.setValidator(new PersonValidator());
        binder.setValidator(new CustomerValidator(new AddressValidator()));
    }

    @RequestMapping("/testCustomerValidator")
    public String testCustomerValidator(@Valid Customer customer, BindingResult result) {
        if(result.hasErrors()) {
            List<ObjectError> allErrors = result.getAllErrors();
            if(!CollectionUtils.isEmpty(allErrors)) {
                for(ObjectError error : allErrors) {
                    System.out.println("error: " + error.getDefaultMessage());
                }
            }
        }
        return "success";
    }

    @RequestMapping("/testPersonValidator")
    public String testPersonValidator(@Valid Person person, BindingResult result) {
        if(result.hasErrors()) {
            List<ObjectError> allErrors = result.getAllErrors();
            if(!CollectionUtils.isEmpty(allErrors)) {
                for(ObjectError allError : allErrors) {
                    System.out.println("error: " + allError.getDefaultMessage());
                }
            }
        }
        System.out.println(person);
        return "success";
    }

}

同时指定了两个验证器,不论是请求 testCustomerValidator,还是请求 testPersonValidator 都会报异常,因为在 initBinder() 中设置的两个 validator 是一个且的关系,只要是验证,必须同时满足这两个 validator 的验证规则。

 

默认情况下,如果不对 @InitBinder 注解指定 value 属性值,那么这个方法对每个入参存在 model 参数的目标 handler 方法起作用,在目标方法调用前,对 入参处的 model 执行校验。

如果对 @InitBinder 注解指定 value 属性值,那么它只会对对应的目标 handler 方法的入参 model 执行校验.

如:

/**
 * @author solverpeng
 * @create 2016-08-12-10:49
 */
@Controller
public class TargetHandler {

    @InitBinder("customer")
    public void initBinder(DataBinder binder) {
        binder.setValidator(new CustomerValidator(new AddressValidator()));
    }

    @RequestMapping("/testCustomerValidator")
    public String testCustomerValidator(@Valid Customer customer, BindingResult result) {
        if(result.hasErrors()) {
            List<ObjectError> allErrors = result.getAllErrors();
            if(!CollectionUtils.isEmpty(allErrors)) {
                for(ObjectError error : allErrors) {
                    System.out.println("error: " + error.getDefaultMessage());
                }
            }
        }
        return "success";
    }

    @RequestMapping("/test")
    public String test(@Valid Person person) {
        System.out.println(person);
        return "success";
    }

}

如此时,只会对 testCustomerValidator() 方法处的 customer 参数执行校验,而不会对 test() 方法处的 person 参数执行校验。

 

目标 handler 方法处的 BindingResult 类型的参数,它是 Errors 的子接口,验证失败之后的错误信息会存放到 Erros 中,在目标方法处,验证出错的信息可以通过 BindingResult 来获取。

需要注意的是,如果在目标方法处存在多个需要校验的对象,需要在每个对象后添加 BindingResult 来获取验证失败的错误信息,而不能只通过一个 BindingResult 来获取所有的错误信息,即 BindignResult 必须紧挨着要验证的参数后。

 

2.使用 JSR-303 进行校验

JSR-303 校验是一个数据校验规范,Spring 没有对其进行实现,所以使用的时候,需要添加一个实现,这里添加 Hibernate Validator 作为 JSR-303 的实现.

需要额外添加的 jar 包:

validation-api-1.1.0.CR1.jar
hibernate-validator-5.0.0.CR2.jar
hibernate-validator-annotation-processor-5.0.0.CR2.jar
jboss-logging-3.1.1.GA.jar
classmate-0.8.0.jar

在 Bean 的 getXxx() 或属性名上添加验证规则注解.

(1)一个简单对象的验证

实体类:

public class Person {
    private String name;
    private int age;

    @NotBlank(message = "人名不能为空")
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Min(value = 10, message = "年龄最小为10.")
    @Max(value = 110, message = "年龄最大不得超过110.")
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

使用:

@RequestMapping("/testPersonValidator")
public String testPersonValidator(@Valid Person person, BindingResult result) {
    if(result.hasErrors()) {
        List<ObjectError> allErrors = result.getAllErrors();
        if(!CollectionUtils.isEmpty(allErrors)) {
            for(ObjectError allError : allErrors) {
                System.out.println("error: " + allError.getDefaultMessage());
            }
        }
    }
    System.out.println(person);
    return "success";
}

不需要通过 @InitBinder 来指定验证器,只需要在目标 handler 方法处对需要验证的对象标注 @Valid 注解。可以对一个属性标有多个验证规则的注解。

(2)一个复杂对象的验证

实体:

public class Address {
    private String addressName;

    @NotBlank
    public String getAddressName() {
        return addressName;
    }

    public void setAddressName(String addressName) {
        this.addressName = addressName;
    }

    @Override
    public String toString() {
        return "Address{" +
                "addressName='" + addressName + '\'' +
                '}';
    }
}
public class Customer {
    private String firstName;
    private String surname;
    private Address address;

    @NotBlank(message = "firstName must be not null.")
    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    @Valid
    public Address getAddress() {
        return address;
    }

    public void setAddress(Address address) {
        this.address = address;
    }

    @NotBlank(message = "surname must be not null.")
    public String getSurname() {
        return surname;
    }

    public void setSurname(String surname) {
        this.surname = surname;
    }

    @Override
    public String toString() {
        return "Customer{" +
                "firstName='" + firstName + '\'' +
                ", surname='" + surname + '\'' +
                ", address=" + address +
                '}';
    }
}

说明:对复杂对象的验证和简单对象的验证使用方式相同,对内嵌对象的验证使用 @Valid 注解就行。

和实现 Validator 接口不同,在 Spring 中使用 JSR-303 可以在一个 handler 中验证多个不同的的 bean 。如:

@Controller
public class TargetHandler {

    @RequestMapping("/testCustomerValidator")
    public String testCustomerValidator(@Valid Customer customer, BindingResult result) {
        if(result.hasErrors()) {
            List<ObjectError> allErrors = result.getAllErrors();
            if(!CollectionUtils.isEmpty(allErrors)) {
                for(ObjectError error : allErrors) {
                    System.out.println("error: " + error.getDefaultMessage());
                }
            }
        }
        return "success";
    }

    @RequestMapping("/testPersonValidator")
    public String testPersonValidator(@Valid Person person, BindingResult result) {
        if(result.hasErrors()) {
            List<ObjectError> allErrors = result.getAllErrors();
            if(!CollectionUtils.isEmpty(allErrors)) {
                for(ObjectError allError : allErrors) {
                    System.out.println("error: " + allError.getDefaultMessage());
                }
            }
        }
        System.out.println(person);
        return "success";
    }

}

说明:不论是请求 testCustomerValidator 对 Customer 进行验证还是请求testPersonValidator 对 Person 进行验证,不会报异常

3.自定义验证规则约束注解

(1)参照已经实现的验证规则。自定义的验证规则注解需要使用 @Constraint 注解,同时指定其属性 validatedBy 的值,即使用哪个验证器类进行校验。

(2)必须对验证器注解类添加三个属性:message、groups和payload。

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = NumberValidator.class)
public @interface Number {
    String message() default "must be a number.";

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

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

}

定义验证器类,必须实现  public interface ConstraintValidator<A extends java.lang.annotation.Annotation, T> 这个接口,如:

public class NumberValidator implements ConstraintValidator<Number, String> {
    private Number number;

    @Override
    public void initialize(Number number) {
        this.number = number;
    }

    @Override
    public boolean isValid(String str, ConstraintValidatorContext constraintValidatorContext) {
        try {
            Integer.parseInt(str);
            return true;
        } catch(NumberFormatException e) {
            return false;
        }
    }
}

可以看出,需要实现两个方法,initialize() 方法进行初始化,isValid() 方法进行验证,其中 A 为验证器注解类名,也是initialize() 方法的入参,T 为要验证的对象,也是 isValid() 方法的的第一个参数。

(3)对验证规则注解进行分组

问题:通过验证接口的方式,可以为一个 bean 定义多个验证器,可以适用于不同的情况。那么使用 JSR-303 该如何达到这种效果?

<1>定义分组接口(普通接口就行),用于标识分组

<2>对要验证的 bean 标注的注解进行分组。

public class Person {
    private String name;
    private int age;

    @NotBlank(message = "人名不能为空")
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Min(value = 10, message = "年龄最小为10.", groups = First.class)
    @Max(value = 110, message = "年龄最大不得超过110.", groups = Second.class)
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

验证时指定分组

@RequestMapping("/testPersonValidator")
public String testPersonValidator(@Validated({First.class}) Person person, BindingResult result) {
    if(result.hasErrors()) {
        List<ObjectError> allErrors = result.getAllErrors();
        if(!CollectionUtils.isEmpty(allErrors)) {
            for(ObjectError allError : allErrors) {
                System.out.println("error: " + allError.getDefaultMessage());
            }
        }
    }
    System.out.println(person);
    return "success";
}

说明:

在使用时,可以指定同时使用多个分组,按照先后顺序进行验证,若第一个不通过,则不验证第二个。

为同一个 Bean 定义的多个验证器注解名称不能重复,若想添加相同的规则,需要自定义注解。

 

四、客户端错误消息的显示:

上面已经介绍过,不论是类型转换和格式化失败,还是数据校验失败,都会将错误信息填充到 BindingResult 中,那么在客户端如何获取呢?

使用 SpringMVC 标签显示错误消息。当使用 springmvc 标签显示错误消息时,SpringMVC 会查看 Web 上下文是否装配了对应的国际化资源文件,在国际化资源文件中查找对应的国际化消息。如果没有找到,则显示默认的错误消息。

每个属性在数据绑定或数据校验发生错误的时候,都会生成一个 fieldError 对象。当一个属性校验失败后,校验框架会为该属性生成4个消息代码。

如 User 的 password 属性标注了一个 @Pattern 注解,当不满足 @Pattern 定义的规则时,就会产生以下4个错误代码。

Pattern.user.password
Pattern.password
Pattern.java.lang.String
Pattern

若类型转换或数据格式化出错时,或该有的参数不存在时,或调用处理方法发生错误时,都会在隐含模型中创建错误消息。

错误代码前缀:

required:必要的参数不存在。如 @RequestParam("param1") 标注了一个入参,但是该参数不存在。

typeMismatch: 在数据绑定时,发生数据类型不匹配的问题。

methodInvocation: 调用处理方法时发生了错误。

 

注册国际化资源文件:

在SpringMVC配置文件中添加 ResourceBundleMessageSource 类型的 Bean ,同时指定其 id 为 messageSource。设置其国际化基名。如:

<bean class="org.springframework.context.support.ResourceBundleMessageSource" id="messageSource">
  <property name="basename" value="i18n"/>
</bean>

 

五、总结

本篇文章从 <mvc:annotation-driven /> 说起,主要说明了 SpringMVC 的类型转换、格式化以及数据校验。只是对用法进行了说明,没有更深层次的深入。以后深入的时候再写文章来介绍吧。

 

六、外传

(1)

在 idea 下,无法对国际化资源文件中的中文转为 Unicode,这里提供一个 中文汉字 | ASCII | Unicode互相转换工具 在线工具:

http://www.atool.org/chinese2unicode.php

(2)

@InitBinder 详解

标注有 @InitBinder 注解的方法。可以对 WebDataBinder 对象进行初始化,WebDataBinder 是 DataBinder 的子类,用于完成表单属性对 Bean 属性的绑定。

@InitBinder 标注的方法不能有返回值,参数一般为 WebDataBinder 。

如不自动绑定对象中的 name 值。

@InitBinder
public void initBinder(WebDataBinder dataBinder) {
    dataBinder.setDisAllowedField("name");
}

 

posted @ 2016-08-29 11:48  solverpeng  阅读(4482)  评论(7编辑  收藏  举报