zoukankan      html  css  js  c++  java
  • Spring MVC 框架学习六:Spring MVC 请求消息中的数据处理流程

    一定要统一各个 jar 文件的版本,不然启动服务器时会出现异常

    org.springframework.beans.factory.BeanCreationException:

    Error creating bean with name 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping',

    NoSuchMethodError: org.springframework.web.bind.annotation.RequestMapping.path()[Ljava/lang/String; 错误

     本文程序需用到的包有

        

    Spring 会根据请求方法签名的不同,将请求消息中的信息以一定的方式转换并绑定到请求方法的入参中。在请求消息到达真正调用处理方法的这一段时间内,SpringMVC 还完成了很多工作,包括数据转换、数据格式化及数据校验等。

    数据绑定流程

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

    数据绑定的核心部件是 DataBinder,它的进行机制描述如图

    1,2. SpringMVC 主框架将 ServletRequest 对象及目标方法的入参实例传递给 WebDataBinderFactory 实例,以创建 DataBinder 实例对象

    3. DataBinder 调用装配在 SpringMVC 上下文的 ConversionService 组件进行数据类型转换、数据格式化工作,并将 Servlet 中的请求信息填充到入参对象中

    4. 调用 Validator 组件对已经绑定了请求消息的入参对象进行数据合法性校验,并最终生成数据绑定结果 BindingResult 对象

    5. SpringMVC 抽取 BindingResult 中的入参对象和校验错误对象,将它们赋给处理方法的相应入参

    数据转换

    Java 标准的 PropertyEditor 的核心功能是将一个字符串装换成一个 Java 对象,以便根据界面的输入或配置文件中的配置字符串构造出一个 JVM 内部的 Java 对象。

    但 Java 原生的 PropertEditor 存在以下不足:

    1. 只能用于字符串和Java 对象的转换,不适用于任意两个 Java类型之间的转换

    2. 对源数据及目标对象所在的上下文信息(如注解、所在宿主类的结构类)不敏感,在类型转换时不能利用这些上下文信息实施高级转换逻辑

    鉴于此种情况,Spring 在核心模型中添加了一个通用的类型转换模块

    ConversionService 是 Spring 类型转换体系的核心接口,它位于 org.springframework.core.convert 包中

    可以利用 org.springframework.context.support.ConversionServiceFactoryBean 在 Spring 的上下文中定义一个 ConversionService。Spring 将自动识别出上下文中的 ConversionService,并在 Bean 属性配置及 SpringMVC 处理方法入参绑定等场合使用它进行数据格式的转换。该 FacyoryBean 创建 ConversionService 内建了很多的转换器,可完成大多数 Java 类型的转换工作。除了包括将 String 对象转换为各种基础类型的对象外,还包括 String、Number、Array、Collection、Map、Properties 及 Object 之间的转换器。

    可以通过 ConversionServiceFactoryBean 的 converters 属性注册自定义的类型转换器。自定义的转换器必须实现 org.springframework.core.convert.converter 包中的转换器接口。该包中一共定义了3中类型的转换器接口,实现任意一个转换器接口都可以作为自定义转换器注册到 ConversionServiceFactoryBean 中。这3个类型的转换器接口分别为:

    1. Converter<S, T>:将 S 类型的对象转为 T 类型的对象

    2. ConverterFactory:将相同系列多个“同质”Converter封装在一起。如果希望将一种类型的对象转换为另一种类型及其子类的对象(例如将 String 转换为 Number 及 Number子类(Integer、Long、Double等))可以使用该转换器工厂类

    3. GenericConverter:会根据源类对象及目标类对象所在的宿主类中的上下文信息进行类型转换

    现在我们通过一个例子来看如何自定义转换器

    假设处理方法有一个 Employee 类型的入参,我们希望将一个请求参数字符串直接转换为 Employee 对象,该字符串的格式为:<lastName>-<Email>

    新建 javaweb 项目,导入 Spring 相关的包

    新建自定义的转换器 EmployeeConerter 继承 Converter<S, T> 接口类

    package com.bupt.springmvc.converter.converter;
    
    import org.springframework.core.convert.converter.Converter;
    import org.springframework.stereotype.Component;
    import com.bupt.springmvc.converter.entity.Employee;
    
    @Component
    public class EmployeeConverter implements Converter<String, Employee>
    {
        @Override
        public Employee convert(String arg0)
        {
            if(arg0 != null)
            {
                //按"-"来分割输入的字符串
                String[] vals = arg0.split("-");
                
                if(vals != null && vals.length == 2)
                {
                    String lastName = vals[0];
                    String email = vals[1];
                    
                    Employee employee = new Employee(null, lastName, email);
                    System.out.println(arg0 + " : " + employee);
                    return employee;
                }
            }
            return null;
        }
    }

    新建实体类 Employee 和模拟数据库操作的 EmployeeDao 类

    package com.bupt.springmvc.converter.entity;
    
    public class Employee
    {
        private Integer id;
        private String lastName;
        private String email;
        
      //生成 getter 和 setter 方法,生成带参和不带参的构造器,重写 toString()
    }
    package com.bupt.springmvc.converter.Dao;
    
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Map;
    import org.springframework.stereotype.Repository;
    import com.bupt.springmvc.converter.entity.Employee;
    
    @Repository
    public class EmployeeDao
    {
        private static Map<Integer, Employee> employees = null;
    
        static
        {
            employees = new HashMap<Integer, Employee>();
    
            employees.put(1001, new Employee(1001, "E-AA", "aa@163.com"));
            employees.put(1002, new Employee(1002, "E-BB", "bb@163.com"));
            employees.put(1003, new Employee(1003, "E-CC", "cc@163.com"));
            employees.put(1004, new Employee(1004, "E-DD", "dd@163.com"));
            employees.put(1005, new Employee(1005, "E-EE", "ee@163.com"));
        }
    
        private static Integer initId = 1006;
    
        public void save(Employee employee)
        {
            if (employee.getId() == null)
            {
                employee.setId(initId++);
            }
    
            employees.put(employee.getId(), employee);
        }
    
        public Collection<Employee> getAll()
        {
            return employees.values();
        }
    
        public Employee get(Integer id)
        {
            return employees.get(id);
        }
    
        public void delete(Integer id)
        {
            employees.remove(id);
        }
    }

    新建方法处理器类 ConverterHandler

    package com.bupt.springmvc.converter.handler;
    
    import java.util.Map;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RequestParam;
    import com.bupt.springmvc.converter.Dao.EmployeeDao;
    import com.bupt.springmvc.converter.entity.Employee;
    
    @Controller
    public class ConverterHandler
    {
        @Autowired
        private EmployeeDao employeeDao;
        
        @RequestMapping("/emps")
        public String list(Map<String, Object> map)
        {
            map.put("employees", employeeDao.getAll());
            return "list";
        }
        
        @RequestMapping(value="/emp", method=RequestMethod.GET)
        public String input(Map<String, Object> map)
        {
        
    return "input"; } @RequestMapping(value="/testConversionServiceConverter", method=RequestMethod.POST) public String testConverter(@RequestParam("employee") Employee employee) { employeeDao.save(employee); return "redirect:/emps"; } }

    配置 web.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://xmlns.jcp.org/xml/ns/javaee"
        xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
        id="WebApp_ID" version="3.1">
    
        <!-- 自定义的spring 配置文件可以放置在类路径 src下,名字在 param-value 属性中指定 -->
        <servlet>
            <servlet-name>springDispatcherServlet</servlet-name>
            <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
            <init-param>
                <param-name>contextConfigLocation</param-name>
                <param-value>classpath:springmvc.xml</param-value>
            </init-param>
            <load-on-startup>1</load-on-startup>
        </servlet>
    
        <servlet-mapping>
            <servlet-name>springDispatcherServlet</servlet-name>
            <url-pattern>/</url-pattern>
        </servlet-mapping>
        
    </web-app>

    类路径 src 下新建 springmvc.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:context="http://www.springframework.org/schema/context"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
            http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    
        <!-- 配置自动扫描的包 -->
        <context:component-scan base-package="com.bupt.springmvc.converter"/>
        <!-- 配置视图解析器 -->
        <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <property name="prefix" value="/WEB-INF/views/"></property>
            <property name="suffix" value=".jsp"></property>
        </bean>
        
        <mvc:annotation-driven conversion-service="ConversionService"/>
        
        <!-- 配置ConversionService -->
        <bean id="ConversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
            <property name="converters">
                <set>
                    <ref bean="employeeConverter"/>
                </set>
            </property>
        </bean>
        
    </beans>

    WEB-INF 下新建 viws 文件夹,内新建 list.jsp 和 input.jsp

    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <title>Insert title here</title>
    </head>
    <body>
    
            <table border="1" cellpadding="10" cellspacing="0">
                <tr>
                    <th>ID</th>
                    <th>LastName</th>
                    <th>Email</th>
                </tr>
                
                <c:forEach items="${requestScope.employees }" var="emp">
                    <tr>
                        <td>${emp.id }</td>
                        <td>${emp.lastName }</td>
                        <td>${emp.email }</td>
                    </tr>
                </c:forEach>
            </table>
        <br><br>
        
        <a href="emp">Add New Employee</a>
        
    </body>
    </html>
    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <title>Insert title here</title>
    </head>
    <body>
        
        <form action="testConversionServiceConverter" method="post">
            Employee: <input type="text" name="employee">
            <input type="submit" value="submit">
        </form>
        
    </body>
    </html>

    WebContent 下新建 index.jsp。

    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <title>Insert title here</title>
    </head>
    <body>
        <a href="emps">List All Employees</a>
    </body>
    </html>

    部署项目后启动 tomcat 访问,访问 index.jsp 页面,点击超链接,页面跳转到如下图页面

    点击 Add New Employee 超链接,跳转到 input.jsp 页面,我们在输入框输入如图所示的数据格式

    提交后效果如图

    由此可以看出,我们自定义的转换器已经把我们输入的特定规则的字符串转换成了 Employee 对象。

     

    <mvc:annotation-drivern> 配置作用

    如果我们在 spring 配置文件中加上 <mvc:annotation-drivern>,它会自动注册 RequestMappingHandlerMapping、RequestMappingHandlerAdapter 与 ExceptionHandlerExceptionResolver 三个 bean。

    还将提供以下支持:

    1. 支持使用 ConversionService 实例对表单参数进行类型转换

    2. 支持使用 @NumberFormat 注解、@DataTimeFormat 注解完成数据类型的格式化

    3. 支持使用 @Valid 注解对 JavaBean 实例进行 JSR 303 验证

    4. 支持使用 @RequestBody 和 @ResponseBody 注解

    我们通过 Debug 来看一下为什么要添加 <mvc:annotation-drivern>

    我们 Debug 的代码是上一篇博文写的 CRUD 程序,在 Employee 实体类的 setLastName() 方法内设置断点的,点击 Add New Employee 超链接,填写表单提交数据

    当配置了<mvc:annotation-drivern conversion-service=" "/> 属性,即存在自定义的转换器情况下。conversionService 的值是 DefaultConversionService,它其实就是 conversion-service 属性值所指的转换器

    当把 <mvc:annotation-drivern conversion-service=" "/> 属性去掉后,再进行 Debug,此时 conversionService  变成 spring内置的转换器 DefaultFormattingConversionService

    当把 <mvc:annotation-drivern> 注释掉后,此时 conversionService 变为 null

    @InitBinder 注解用法

    SpringMVC 在支持新的转换器框架的同时,也支持 JavaBeans 的 PropertyEditor。可以在控制器中使用 @InitBinder 添加自定义的编辑器。

    由 @InitBinder 标识的方法,可以对 WebDataBinder 对象进行初始化。WebDataBinder 是 DataBinder 的子类,用于完成由表单字段到 JavaBean 属性的绑定。

    @InitBinder 方法不能有返回值,它必须声明为 void

    @InitBinder 方法的参数通常是 WebDataBinder

    现在通过代码来说明它的一些用法

    以前面添加员工信息作为例子,如果我们希望某个属性比如 lastName 不进行赋值时,就可以使用如下代码,这就使得结果页面不会出现新增员工的 lastName 值。

    @Controller
    public class ConverterHandler
    {
        @InitBinder
        public void initBinder(WebDataBinder binder)
        {
            //提交表单时不自动绑定对象中的 lastName 属性,另行处理
            binder.setDisallowedFields("lastName");
        }
    }

    @InitBinder 最主要的作用还是用来为控制器注册属性编辑器,Spring MVC 自己提供了大量的实现类,包括 CustomDateEditor、 CustomBooleanEditor、 CustomNumberEditor 等。当然,我们也可以自定义编辑器类而不使用 Spring MVC 为我们提供的编辑器类。

    //自定义编辑器需继承 PropertiesEditorSupport
    public class UserEditor extends PropertiesEditorSupport
    {
        //自定义逻辑
    }

    这种使用 @InitBinder 注释注册的属性编辑器,只对当前 Controller 有效

    @Controller
    public class ConverterHandler
    {    
        //在控制器初始化时调用
        @InitBinder
        public void initBinder(WebDataBinder binder)
        {
            //注册指定自定义的编辑器
            binder.registerCustomEditor(User.class, new UserEditor());
         //注册 SpringMVC 自带编辑器,日期实现字符串和Date类型的转换
         binder.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("yyy-MM-dd"), false)); } }

    如果希望在全局范围内使用 UserEditor 编辑器,则可实现 WebBindingInitializer 接口并在该实现类中注册 UseEditor

    public class MyBindingInitializer implements WebBindingInitializer
    {
        @Override
        public void initBinder(WebDataBinder binder, WebRequest request)
        {
            binder.registerCustomEditor(Employee.class, new UserEditor());
        }
    }

    在 initBinder() 接口方法中注册 UserEditor 编辑器后。接下来,还需要在 Web 上下文中通过 AnnotationMethodHandlerAdapter 装配 MyBindingInitializer

    配置 springmvc.xml

        <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
            <property name="webBindingInitializer">
                <bean class="com.bupt.springmvc.converter.converter.MyBindingInitializer"/>
            </property>
        </bean>

    对于同一个类型对象来说,如果既在 ConversionService 装配自定义的转换器,又通过 WebBindingInitializer 装配了自定义编辑器,同时还在控制中通过 @InitBinder 装配了自定义编辑器,那么 SpringMVC 将按照以下的优先顺序查找对应类型的编辑器:

    1. 查询通过 @InitBinder 装配的自定义编辑器

    2. 查询通过 ConversionService 装配的自定义转换器

    3. 查询通过 WebBindingInitializer 装配的自定义编辑器

     

    数据格式化

    Spring 使用转换器进行源类型对象到目标类型对象的转换,Spring 的转换器并不提供输入输出信息格式化工作。如果需要转换的源类型数据(一般为字符串)是从客户端界面传过来的,为了方便使用者,这些数据一往往是拥有一定的格式,如日期、时间和数字等。如何从格式化的数据中获取真正的数据以完成数据绑定,并将处理完成的数据输出为格式化的数据是Spring 格式化框架要解决的问题。

    Spring 在 org.springframework.format 包下定义了一个格式化框架中最重要的接口 Formatter<T> 接口。它的实现类如:DateFormatter 提供了一个用于时间对象格式化,NumberFormatter 提供了用于数字类型对象的格式化等。可以手工调用这些 Formatter 接口实现类进行对象数据输入/输出的格式化工作,这种硬编码的格式化显然不符合我们所追求的低耦合原则。所以 Spring 为我们提供了注解驱动的属性对象格式化功能:在 Bean 属性设置、Spring MVC 处理方法入参数据绑定、模型数据输出时自动通过注解应用格式化功能。

    其实对属性对象的输入/输出进行格式化,从其本质上来讲依然属于“类型转换”的范畴。

    Spring 在格式化模块中定义了一个实现 ConversionService 接口的 FormattingConversionService 实现类,因此他既具有类型转换的功能,又具有格式化功能

    相对于 ConversionService 的 ConversionServiceFactoryBean 工厂类,FormattingConversionService 也拥有一个对应的 FormattingConversionServiceFactoryBean 工厂类,后者用于在 Spring 上下文构造一个 FormattingConversionService。通过这个工厂类,既可注册自定义转换器,还可注册自定义的注解驱动逻辑。

    FormattingConversionServiceFactoryBean内部已经注册了:

    1. NumberFormatAnnotationFormatterFactory:支持对数字类型的属性使用 @NumberFormat 注解

    2. JodaDateTimeFormatAnnotationFormatterFactory:支持对日期类型的属性使用 @DateTimeFormat 注解

    装配了 FormattingConversionServiceFactoryBean 后,就可以在 SpringMVC 入参及模型数据输出时使用注解驱动了

    需要注意的是 <mvc:annotation-drivern conversion-service=" "/> 默认创建的 ConversionService 实例即为 FormattingConversionServiceFactoryBean。

    我们以日期格式化 @DateTimeFormat 注解和数值格式化 @NumberFormat 注解为例来看看如何在程序中使用格式化注解

    1. @DateTimeFormat 注解可对 java.util.Date、java.util.Calendar 和 java.long.Long 等时间类型进行标注:

    1). pattern 属性:类型为字符串。指定解析/格式化字段数据的模式,如:“yyyy-MM-dd hh:mm:ss”

    2). iso 属性:类型为 DateTimeFormat.ISO,指定解析/格式化字段数据的ISO模式,包括四种: DateTimeFormat.ISO.NONE(表示不应使用ISO格式的日期)、 DateTimeFormat.ISO.DATE(yyyy-MM-dd)、 DateTimeFormat.ISO.TIME(hh:mm:ss.SSSZ)、 DateTimeFormat.ISO.DATE_TIME(yyyy-MM-dd hh:mm:ss.SSSZ)

    3.) style 属性:字符串类型。通过样式指定日期和时间的格式,由两位字符组成,第一位表示日期的格式,第二位表示时间的格式,以下为几个常用的可选值

     S:短日期/时间的样式  M:中日期/时间的样式  L:长日期/时间的样式  F:完整日期/时间的样式  -:忽略日期或时间的样式

    2. @NumberFormat 可对类似数字类型的属性进行标注,它拥有两个互斥的属性:

    1). pattern:类型为 String,自定义样式,如 pattern=“###,###,#”

    2). style:类型为 NumberFormat.Style。用于指定样式类型,包括三种:NumberFormat.Style.NUMBER(正常数字类型)、NumberFormat.Style.CURRENCY(货币类型)、NumberFormat.Style.PERCENT(百分数类型)

    通过例子来看具体用法

    在之前的 Employee 实体类中添加如下属性,并生成 getter 和 setter 方法,增加构造方法,重写 toString 方法,可以在属性上增加格式化的注解

    @DateTimeFormat(pattern="yyyy-MM-dd")
    private Date birth;
    @NumberFormat(pattern="###,###.#")
    private Float salary; 

    修改 springmvc.xml 

        <!-- 配置自动扫描的包 -->
        <context:component-scan base-package="com.bupt.springmvc.converter"/>
        <!-- 配置视图解析器 -->
        <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
            <property name="prefix" value="/WEB-INF/views/"></property>
            <property name="suffix" value=".jsp"></property>
        </bean>
        
        <!-- 默认 ConversionService 属性值即为 FormattingConversionServiceFactoryBean 的实例 -->
        <mvc:annotation-driven></mvc:annotation-driven>    
    </beans>

    也可以在 ConversionService 属性中直接指明其值为 FormattingConversionServiceFactoryBean

        <mvc:annotation-driven conversion-service="ConversionService"></mvc:annotation-driven>
        
        <!-- 配置ConversionService,这样写既可以添加自定义的类型转换器,又可以 spring 为我们提供的格式化功能 -->
        <bean id="ConversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
            <property name="converters">
                <set>
                    <ref bean="employeeConverter"/>
                </set>
            </property>
        </bean>

    重写 input.jsp 页面

    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <title>Insert title here</title>
    </head>
    <body>
        
        <form:form action="emp" Method="POST" modelAttribute="employee">
            LastName: <form:input path="lastName"/><br>
            Email: <form:input path="email"/><br>
            Birth: <form:input path="birth"/><br>
            Salary: <form:input path="salary"/><br>
            <input type="submit" value="submit">
        </form:form>
        
    </body>
    </html>

    ConverterHandler 方法类重写为

    @Controller
    public class ConverterHandler
    {
        @Autowired
        private EmployeeDao employeeDao;
        
        @RequestMapping("/emps")
        public String list(Map<String, Object> map)
        {
            map.put("employees", employeeDao.getAll());
            return "list";
        }
        
        @RequestMapping(value="/emp", method=RequestMethod.GET)
        public String input(Map<String, Object> map)
        {
            map.put("employee", new Employee());
            return "input";
        }
        
        @RequestMapping(value="/emp", method=RequestMethod.POST)
        public String input(Employee employee)
        {
            employeeDao.save(employee);
            System.out.println("employee: " + employee);
            return "redirect:/emps";
        }
    }

    启动服务器,访问 index.jsp 页面,点击超链接,跳转页面后点击 Add New Employee 超链接,填写表单如下图

    点击提交,我们可以看到控制台输出,能够正常的格式化数据

    数据校验

    应用程序在执行业务逻辑前,必须通过数据校验保证收到的输入数据是合法的,如代表生日的日期应该是一个过去的时间,工资的数值必须是一个正数。很多时候,同样的数据验证会出现在不同的层中,这样会导致代码冗余,为了避免这样的情况。最好的方法时将验证逻辑和相应的域模型进行绑定,将代码验证的逻辑集中管理

    JSR 303 是 Java 为 Bean 数据合法性校验提供的标准框架,它已经包含在 JavaEE 6.0 中。

    JSR 303 通过在 Bean 属性上标注类似于 @NotNull、@Max 等标准的注解指定校验规则,并通过标准的验证接口对 Bean 进行验证。它定义了一套可标注在成员变量、属性方法上的校验注解,如表所示

    注解 功能说明
    @Null 被注释的元素必须为 null
    @NotNull 被注释的元素必须不为 null
    @AssertTrue 被注释的元素必须为true
    @AssertFalse 被注释的元素必须为false
    @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
    @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
    @DecimalMax(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
    @DecimalMin(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
    @Size(max, min) 被注释的元素必须的大小必须在指定的范围内
    @Digits(integer, fraction) 被注释的元素的大小必须是一个数字,其值必须在可接受的范围内
    @Past 被注释的元素必须是一个过去的日期
    @Future 被注释的元素必须是一个将来的日期

    Hibernate Validator 是 JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持如表所示的扩展注解

    注解 功能说明
    @Email 被注释的元素必须是电子邮箱地址
    @Length 被注释的字符串的大小必须在指定范围内
    @NotEmpty 被注释的字符串必须非空
    @Range 被注释的元素必须在合适的范围内

    那么 SpringMVC 如何实现数据校验呢,它可以分为几步

    1. 使用 JSR 303 验证标准。SpringMVC 4.x 拥有自己独立的数据校验框架,同时还支持 JSR 303 标准的校验框架。Spring 在进行数据绑定时,可以同时调用校验框架完成数据校验工作。在 SpringMVC 中,可以直接通过注解驱动的方式进行数据校验。

    2. 加入 Hibernate Validator 验证框架的 jar 包

    Spring 本身并没有提供 JSR 303 的实现,所有必须将 JSR 303 的实现的 jar 包放到类路径下,包括:

    hibernate-validator-5.0.0.CR2.jar、hibernate-validator-annotation-processor-5.0.0.CR2.jar、classmate-0.8.0.jar、validation-api-1.1.0.CR1.jar 和

    jboss-logging-3.1.1.GA.jar

    3. 在Spring配置文件中添加 <mvc:annotation-drivern/> 注解。

    Spring 的 LocalValidatorFactoryBean 既实现了 Spring 的 Validator 接口,也实现了 JSR 303 的 Validator 接口。只要在 Spring 容器中定义一个 LocalValidatorFactoryBean,即可将其注入到需要数据校验的 Bean 中。<mvc:annotation-drivern> 会默认装配好一个 LocalValidatorFactoryBean。

     4. 在 Bean 属性上添加相应的注解

        @NotEmpty
        private String lastName;
        
        @Email
        private String email;
        
        @Past//表示必须是一个过去的时间
        @DateTimeFormat(pattern="yyyy-MM-dd")
        private Date birth;
        @NumberFormat(pattern="###,###.#")
        private Float salary; 

    5. 在处理方法的 Bean 类型的参数前添加 @Valid 注解,同时添加校验结果的入参 BiningResult 或 Errors

    通过在处理方法的入参上标注 @Valid 注解即可让 SpringMVC 在完成数据绑定后执行数据校验工作。SpringMVC 框架在将请求参数绑定到该入参对象后,就会调用验证框架根据注解声明的校验规则实施校验。

    SpringMVC 是通过对处理方法签名的规约来保存校验结果的:前一个表单/命令对象的校验结果保存到随后的入参中,这个保存校验结果的入参必须是 BindingResult 或 Errors 类型,这两个类都位于 prg.springframework.validation 包中

    需校验的 Bean 对象和其绑定结果对象或错误对象是成对出现的,它们之间不允许声明其它的入参

    Errors 接口提供了获取错误信息的方法,如 getErrorCount() 或 getFieldErrors(String field)。BindingResult 继承了 Errors 接口

     在 ConerterHandler 类中改写 input 处理方法

        @RequestMapping(value="/emp", method=RequestMethod.POST)
        public String input(@Valid Employee employee, BindingResult result)
        {
            if(result.getErrorCount() > 0)
            {
                System.out.println("error");
                
                for(FieldError error : result.getFieldErrors())
                {
                    System.out.println(error.getField() + ": " + error.getDefaultMessage());
                }
           return "input"; } employeeDao.save(employee); System.out.println(
    "employee: " + employee); return "redirect:/emps"; }

    改写 input.jsp 页面

    <%@ page language="java" contentType="text/html; charset=UTF-8"
        pageEncoding="UTF-8"%>
    <%@taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <title>Insert title here</title>
    </head>
    <body>
        
        <form:form action="emp" method="POST" modelAttribute="employee">
            LastName: <form:input path="lastName"/><br>
            Email: <form:input path="email"/><br>
            Birth: <form:input path="birth"/><br>
            Salary: <form:input path="salary"/><br>
            <input type="submit" value="submit">
        </form:form>
        
    </body>
    </html>

    启动服务器转到提交页面,提交如图表单

    控制台输出为

    除了使用 Annotation JSR-303 标准进行数据校验之外,SpringMVC 还提供基于 Validator 接口的方式进行数据校验

    这种情况下我们需要提供一个 Validator 实现类,并实现 Validator 接口的 supports() 和validate() 方法。

    supports() 方法用于判断当前的 Validator 实现类是否支持校验当前需要的实体类,只有当此方法的返回值为true时,该 Validator 接口实现类中的 validate() 方法才会被调用来对当前需要验证的实体类进行校验。

    public class UserValidator implements Validator
    {
         public boolean supports(Class<?> clazz)  
         {
              //只支持对 User 类进行验证        
              return User.class.equals(clazz);
         }
        
          public void validate(Object obj, Errors errors)
          {
              //校验 username 和 password 不为空的情况
              ValidationUtils.rejectIfEmpty(errors, "username", null, "Username is empty.");
              User user = (User) obj;
              if(null == user.getPassword() || "".equals(user.getPassword()))
              errors.rejectValue("password", null, "Password is empty.");
          }
    }

    虽然对 User 类进行校验的 UserValidator 定义好了,但是这个校验类还不能对 User 对象进行校验。因为我们还没有告诉 SpringMVC 应该使用 UserValidator 来对 User 进行校验。我们还需要使用 DataBinder 来设定 当前 Controller 需要使用的 Validator。

    @Controller
    public class UserController
    {
         @InitBinder
         public void initBinder(DataBinder binder)
         {
             //设置当前 Controller 需要使用 UserValidator  
             binder.setValidator(new UserValidator());
         } 
    
         @RequestMapping("login")
         //需要添加 @Valid 注解告诉 Spring 需要校验的参数
         public String login(@Valid User user, BindingResult result)  
         {
             if(result.hasErrors())
                  return "redirect:user/login";
             return "redirect:/";
         }
    }

    上面定义的 Validator 只对当前的 Controller 有效,如果希望定义一个全局的 Validator 多所有 Controller 都起作用的话,我们可以通过 WebBindingInitializer 的 initBinder 方法来设定。另外,还可以在 SpringMVC 的配置文件中通过 <mvc:anntation> 的 validator 属性指定全局的 Validator。

    public class UserBindingInitializer implements WebBindingInitializer
    {
        @Override
        public void initBinder(WebDataBinder binder, WebRequest request)
        {
    //设置全局的 Validator binder.setValidator(new UserValidator())
    ; } }
    <!-- 配置时指定全局的 Validator --!>
    <mvc:annotation-drivern validator="userValidator"/> <bean id="userValidator" class="com.xxx.UserValidator"/>

     

    使用 @Validated 进行分组校验

    在前面的 UserController 中,我们使用了 @Valid 注解来告诉 SpringMVC 我们需要校验的参数是 user。这个注解是定义在 JSR-303 标准中的,这里使用的是 hibernate validation 对它的实现。当然,Spring 中也定义了一个注解 @Validated 它与 @Valid 实现的功能一样。但是 @Validated 为我们带来一种叫做分组验证的校验机制,而 @Valid 则不具备这种功能。

    假设我们想在新增的情况下验证 id ,而修改的情况验证 name 和 password,这种情况下就需要分组进行校验了。

    首先需要定义分组接口,分组接口就是两个普通的接口,用于标识,类似于 java.io.Serializable

    public interface First{
    }
    
    public interface Second{
    }

     接下来使用定义的接口标识实体属性

    public class User implements Serializable
    {
        @NotNull(message="{user.id.null}", groups={First.class})  
        private Long id;
        
        @Length(min=5, max=20, message="{user.name.length.illegal}", group={Second.class})
        private String name;
    
        @NotNull(message="{user.password.null}", groups={First.class, Second.class})
        private String password;
    }

    编写 Controller

    @Controller
    public class UserController
    {
    @RequestMapping("/save")
    public String save(@Validated({Second.class}) User user, BindingResult result) { if(result.hasErrors()) { return "error"; } return "success"; } }

    通过 @Validated 注解标识要验证的分组,如果要验证两个的话,可以这样 @Validated({First.class, Second.class})

    如果我们想先验证一个信息,如果不通过在验证另一个时,可以使用 @GroupSequence 指定分组验证顺序

    @GroupSequence({First.class, Second.class, User.class})
    public class User implements Serializable
    {
        @NotNull(message="{user.id.null}", groups={First.class})  
        private Long id;
        
        @Length(min=5, max=20, message="{user.name.length.illegal}", group={Second.class})
        private String name;
    
        @NotNull(message="{user.password.null}", groups={First.class, Second.class})
        private String password;
    }

    通过 @GroupSequence 指定验证顺序:先校验 First 分组,如果有误立即返回而不会校验 Second 分组,接着如果 First 分组验证通过了,那么才去验证 Second 分组,最后指定 User.class 表示没有分组的在最后校验。

    定制错误信息

    由上面的例子我们可以看到错误信息是会显示出来的,但它是在控制台中显示,我们希望的是它在页面上显示,如何做呢?

    SpringMVC 除了会将表单/命令对象的校验结果保存到对应的 BindingResult 或 Errors 对象中外,还会将所有校验结果保存到“隐含模型”。

    即使处理方法的签名中没有对应于表单/命令对象的结果入参,校验结果也保存在“隐含对象”中。

    隐含模型中的所有数据最终将通过 HttpServletRequest 的属性列表暴露给 JSP 视图对象,因此在 JSP 中可以获取错误信息。

    在 JSP 页面上可通过 <form:errors/> 显示错误信息,可以通过 path 属性值来指定显示哪部分的错误信息。

    1. path="*":显示全部的错误信息

    2. path="username":只显示名为 username 的表单项错误

    改写 input.jsp 页面

        <form:form action="emp" method="POST" modelAttribute="employee">
            <form:errors path="*"/>
            <br><br>
            LastName: <form:input path="lastName"/><br>
            Email: <form:input path="email"/><br>
            Birth: <form:input path="birth"/><br>
            Salary: <form:input path="salary"/><br>
            <input type="submit" value="submit">
        </form:form>

    提交如图表单时,发现有错误将重定向到登录页面

    得到的错误信息将显示在页面上

    也可以通过 path 属性指明要显示的表单项,从而使信息显示在相应的出错位置

    改写 index.jsp 页面

        <form:form action="emp" method="POST" modelAttribute="employee">
            <form:errors path="lastName"/><br>
            LastName: <form:input path="lastName"/><br>
            <form:errors path="email"/><br>
            Email: <form:input path="email"/><br>
            <form:errors path="birth"/><br>
            Birth: <form:input path="birth"/><br><br>
            Salary: <form:input path="salary"/><br>
            <input type="submit" value="submit">
        </form:form>

    提交如图表单

    提交结果为

    需要注意的是,我们要统一整个 IDE 的编码格式,例如统一设置为 UTF-8,不然在页面显示错误信息时会出现乱码现象。

    自定义错误信息

    虽然我们已经实现了在页面上显示错误信息,但这些信息是框架根据规则自动生成的,缺乏人性化和可读性。

    我们希望的是可以显示本地化的错误信息,这就要用到 SpringMVC 为我们提供的支持了。

    通过国际化资源定制我们的错误信息

    每个属性在数据绑定和数据发生错误时,都会生成一个对应的 FieldError 对象,当一个属性校验失败后,校验框架就会为该属性生成4个消息代码,这些代码以校验注解类名为前缀,结合类名、属性名及属性类型名产生多个对应的消息代码。

    如在 Employee 类的 lastName 属性标注的 @NotEmpty 注解,当该注解值不满足 @NotEmpty 所定义的限制规则时,就会产生如下4个错误代码(@Email、@Past 类似)

    NotEmpty.employee.lastName:根据类名、属性名产生的错误码

    NotEmpty.lastName:根据属性名产生的错误码

    NotEmpty.java.lang.String:根据类型产生的错误码

    NotEmpty:根据验证注解名产生的错误码

    当使用 SpringMVC 标签显示错误消息时,SpringMVC 会查看 WEB 上下文是否装配了对应的国际化消息,如果没有,则显示默认的错误消息,否则使用国际化消息。

    具体做法

    1. src 下新建国际化文件 i18n.properties 

    NotEmpty.employee.lastName=######
    Email.employee.email=^^^^^^
    Past.employee.birth=*******

    2. 在spring 配置文件 springmvc.xml 中配置这个资源文件

        <!-- 配置国际化资源文件 -->
        <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
            <property name="basename" value="i18n"></property>
        </bean>

    当我们再次提交如下表单时

    呈现的结果为

    如此,页面的结果就按照我们在资源文件中所配置的显示规则显示

    值得注意的是,如果在数据类型转换或数据格式转换时发生错误,或者该有的参数不存在,或调用处理方法时发生错误,也都会在隐含模型中创建错误信息。

    其错误代码前缀说明如下。

    1. required:必要的参数,如 @RequestParam("param1")标注的一个人入参,但是请求参数不存在 param1 的参数

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

    3. methodInvocation:SpringMVC 在调用处理方法时发生了错误

    以  typeMismatch 为例,在国际化文件中添加代码

    typeMismatch.employee.birth=illegal date

    提交如下表单

    得到的结果页面为

  • 相关阅读:
    FileWatcher
    virtual table(有180个评论)
    this 指针
    docker -ce(社区免费版)
    vue-cli
    CAP理论、BASE理论
    B+树和LSM存储引擎代表树和B-树
    CPU高速缓存
    Python&基础环境搭建
    二叉树
  • 原文地址:https://www.cnblogs.com/2015110615L/p/5655351.html
Copyright © 2011-2022 走看看