Spring MVC 数据绑定(数据转换 & 数据格式化 & 数据校验)

  • 数据绑定,即将ServletRequest对象与目标方法的入参,通过DataBinder进行绑定。实现数据类型转换、数据格式化工作。

数据绑定流程原理在这里插入图片描述

  • Spring MVC 主框架将 ServletRequest 对象及目标方法的入参实例传递给 WebDataBinderFactory 实例,以创建 DataBinder 实例对象
  • DataBinder 调用装配在 Spring MVC 上下文中的 ConversionService 组件进行数据类型转换数据格式化工作。将 Servlet 中的请求信息填充到入参对象中
  • 调用 Validator 组件对已经绑定了请求消息的入参对象进行数据合法性校验,并最终生成数据绑定结果 BindingData 对象
  • Spring MVC 抽取 BindingResult 中的入参对象和校验错误对象,将它们赋给处理方法的响应入参

转换器

  • Spring MVC 内建了很多转换器
    ConversionService converters =  
    java.lang.Boolean->java.lang.String: org.springframework.core.convert.support.ObjectToStringConverter@f874ca
    java.lang.Character -> java.lang.Number : CharacterToNumberFactory@f004c9
    java.lang.Character -> java.lang.String : ObjectToStringConverter@68a961
    java.lang.Enum -> java.lang.String : EnumToStringConverter@12f060a
    java.lang.Number -> java.lang.Character : NumberToCharacterConverter@1482ac5
    java.lang.Number -> java.lang.Number : NumberToNumberConverterFactory@126c6f
    java.lang.Number -> java.lang.String : ObjectToStringConverter@14888e8
    java.lang.String -> java.lang.Boolean : StringToBooleanConverter@1ca6626
    java.lang.String -> java.lang.Character : StringToCharacterConverter@1143800
    java.lang.String -> java.lang.Enum : StringToEnumConverterFactory@1bba86e
    java.lang.String -> java.lang.Number : StringToNumberConverterFactory@18d2c12
    java.lang.String -> java.util.Locale : StringToLocaleConverter@3598e1
    java.lang.String -> java.util.Properties : StringToPropertiesConverter@c90828
    java.lang.String -> java.util.UUID : StringToUUIDConverter@a42f23
    java.util.Locale -> java.lang.String : ObjectToStringConverter@c7e20a
    java.util.Properties -> java.lang.String : PropertiesToStringConverter@367a7f
    java.util.UUID -> java.lang.String : ObjectToStringConverter@112b07f  ……
    
  • 其中,有ConverterFactory的实现类,内置Converter的实现类,可通过泛型,实现多种类的转换。或者直接实现Converter的类。(当不确认返回的类型,只知道其父类时,一般使用ConverterFactory
    @SuppressWarnings({"rawtypes", "unchecked"})
    final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {
    
    	@Override
    	public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
    		return new StringToEnum(ConversionUtils.getEnumType(targetType));  // 获取枚举的class,并作为构造器的参数输入
    	}
    
    
    	private static class StringToEnum<T extends Enum> implements Converter<String, T> {
    
    		private final Class<T> enumType;
    
    		public StringToEnum(Class<T> enumType) {
    			this.enumType = enumType;
    		}
    		// 输入String,根据名字,返回枚举类型
    		@Override
    		public T convert(String source) {
    			if (source.isEmpty()) {
    				// It's an empty enum identifier: reset the enum value to null.
    				return null;
    			}
    			return (T) Enum.valueOf(this.enumType, source.trim());
    		}
    	}
    }
    
    final class StringToBooleanConverter implements Converter<String, Boolean> {
    
    	private static final Set<String> trueValues = new HashSet<>(4);
    
    	private static final Set<String> falseValues = new HashSet<>(4);
    
    	static {
    		trueValues.add("true");
    		trueValues.add("on");
    		trueValues.add("yes");
    		trueValues.add("1");
    
    		falseValues.add("false");
    		falseValues.add("off");
    		falseValues.add("no");
    		falseValues.add("0");
    	}
    
    	@Override
    	public Boolean convert(String source) {
    		String value = source.trim();
    		if (value.isEmpty()) {
    			return null;
    		}
    		value = value.toLowerCase();
    		if (trueValues.contains(value)) {
    			return Boolean.TRUE;
    		}
    		else if (falseValues.contains(value)) {
    			return Boolean.FALSE;
    		}
    		else {
    			throw new IllegalArgumentException("Invalid boolean value '" + source + "'");
    		}
    	}
    }
    

自定义转换器

  • ConversionService 是 Spring 类型转换体系的核心接口。
  • 可以利用 ConversionServiceFactoryBean 在 Spring 的 IOC 容器中定义一个ConversionService
  • Spring 将自动识别出 IOC 容器中的 ConversionService,并在 Bean 属性配置及 Spring MVC 处理方法入参绑定等场合使用它进行数据的转换
  • 可通过 ConversionServiceFactoryBean 的 converters 属性注册自定义的类型转换器,例如:
    <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <!-- 引用类型转换器 -->
                <ref bean="stringToEmployee"/>
            </set>
        </property>
    </bean>
    

Spring 支持的转换器类型

  • Spring 定义了 3 种类型的转换器接口,实现任意一个转换器接口都可以作为自定义转换器注册到ConversionServiceFactoryBean
    • Converter<S,T>:将 S 类型对象转为 T 类型对象
    • ConverterFactory:将相同系列多个 “同质” Converter 封装在一起。如果希望将一种类型的对象转换为另一种类型及其子类的对象(例如将 String 转换为 Number 及 Number 子类(Integer、Long、Double 等)对象)可使用该转换器工厂类
    • GenericConverter:会根据源类对象及目标类对象所在的宿主类中的上下文信息进行类型转换

示例一:Converter

  • Java Bean
    public class Employee {
      private Integer id;
      private String name;
      private Gender gender;
      /*省略...*/
    
    public enum Gender {
      male, female;
    }
    
  • 提交表单。提交格式为id-name-gender(0/1)
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
        <title>Submit Employee</title>
    </head>
    <%
        request.setAttribute("ct", request.getContextPath());
    %>
    <body>
    <form method="post" action="${requestScope.ct}/stringToEmployeeTest">
        <label> Employee: id-name-gender(0/1)
            <input type="text" name="employee" value="">
        </label><br/>
        <input type="submit" value="提交"><br/>
    </form>
    </body>
    </html>
    
  • 请求映射。请求参数注解为@RequestParam("employee"),即表单提交的name。
      @RequestMapping("/stringToEmployeeTest")
      public String stringToEmployeeTest(@RequestParam("employee") Employeeemployee) {
        System.out.println(employee);
        return "success";
      }
    
  • 转换器。用于将request 的 String,转换为 Employee。通过注解@Component自动注册
    @Component
    public class StringToEmployeeConverter implements Converter<String, Employee> {
      /**
       * 输入格式:id-name-genderCode
       *
       * @param source
       * @return
       */
      @Override
      public Employee convert(String source) {
        String[] sources = source.split("-");
        return new Employee(Integer.parseInt(sources[0]), sources[1], getGenderFromCode(sources[2]));
      }
    
      /**
       * 将String转换为Gender;0代表女;1代表男;
       * @param code
       * @return
       */
      private static Gender getGenderFromCode(String code) {
        if (code.equals("0")) return Gender.female;
        if (code.equals("1")) return Gender.male;
        return null;
      }
    }
    
  • spring-mvc配置文件中,注册一个ConversionServiceFactoryBean,并将上述转换器放入,作为参数
    <bean id="conversionService"
          class="org.springframework.context.support.ConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <!-- 引用类型转换器 -->
                <ref bean="StringToEmployeeConverter"/>
            </set>
        </property>
    </bean>
    
  • 将自定义的 ConversionService 注册到 Spring MVC 的上下文中
    <mvc:annotation-driven conversion-service="conversionService"/>
    

示例二:ConverterFactory

  • Employee的子类
    public class Manager extends Employee {
      public Manager() {
      }
      public Manager(int id, String name, Gender gender) {
        super(id, name, gender);
      }
    }
    
  • 转换器工厂
    @Component
    public class StringToEmployeeConverterFactory implements ConverterFactory<String, Employee> {
    
      @Override
      public <T extends Employee> Converter<String, T> getConverter(Class<T> targetType) {
        return new StringToEmployee<>(targetType);
      }
    
      private static class StringToEmployee<T extends Employee> implements Converter<String, T> {
        private final Class<T> targetType;
    
        public StringToEmployee(Class<T> targetType) {
          this.targetType = targetType;
        }
    
        /**
         * 输入格式:id-name-genderCode
         *
         * @param source
         * @return
         */
        @Override
        public T convert(String source) {
          T t = null;
          String[] sources = source.split("-");
          int id = Integer.parseInt(sources[0]);
          String name = sources[1];
          Gender gender = "0".equals(sources[2]) ? Gender.female : Gender.male;
          try {
            t = targetType.getDeclaredConstructor().newInstance();
            Class<? super T> superclass = targetType.getSuperclass();
    
            Method setId = superclass.getDeclaredMethod("setId", Integer.class);
            setId.setAccessible(true);
            setId.invoke(t, id);
    
            Method setName = superclass.getDeclaredMethod("setName", String.class);
            setName.setAccessible(true);
            setName.invoke(t, name);
    
            Method setGender = superclass.getDeclaredMethod("setGender", Gender.class);
            setGender.setAccessible(true);
            setGender.invoke(t, gender);
    
            System.out.println(t);
          } catch (Exception e) {
            e.printStackTrace();
          }
          return t;
        }
      }
    }
    
  • 注册
    <bean id="conversionService"
          class="org.springframework.context.support.ConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <!-- 引用类型转换器 -->
                <ref bean="stringToEmployeeConverterFactory"/>
            </set>
        </property>
    </bean>
    

数据格式化

  • 对属性对象的输入/输出进行格式化,从其本质上讲依然属于 “类型转换” 的范畴。
  • Spring 在格式化模块中定义了一个实现 ConversionService 接口的 FormattingConversionService 实现类,该实现类扩展了 GenericConversionService,因此它既具有类型转换的功能,又具有格式化的功能(Spring MVC默认使用这个)
  • FormattingConversionService 拥有一个 FormattingConversionServiceFactroyBean 工厂类,后者用于在 Spring 上下文中构造前者,FormattingConversionServiceFactroyBean 内部已经注册了
    • NumberFormatAnnotationFormatterFactroy:支持对数字类型的属性使用 @NumberFormat 注解
    • JodaDateTimeFormatAnnotationFormatterFactroy :支持对日期类型的属性使用 @DateTimeFormat 注解
  • 但是,上文使用的ConversionServiceFactoryBean仅包含转换器,没有包含数据格式化器。因此,需要将上文修改为
    <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <!-- 引用类型转换器 -->
                <ref bean="stringToEmployeeConverterFactory"/>
            </set>
        </property>
    </bean>
    
  • 而如果没有自定义的转换器,实际上并不需要声明,只需要声明mvc:annotation-driven即可,因为它默认创建DefaultFormattingConversionService

日期格式化

  • @DateTimeFormat 注解可对 java.util.Datejava.util.Calendarjava.long.Longjava.time.LocalDate 时间类型进行标注
    • pattern 属性:类型为字符串。指定解析/格式化字段数据的模式,如:”yyyy-MM-dd hh:mm:ss”
    • iso 属性:类型为 DateTimeFormat.ISO。指定解析/格式化字段数据的ISO模式,包括四种:ISO.NONE(不使用) – 默认、ISO.DATE(yyyy-MM-dd) 、ISO.TIME(hh:mm:ss.SSSZ)、 ISO.DATE_TIME(yyyy-MM-dd hh:mm:ss.SSSZ)
    • style 属性:字符串类型。通过样式指定日期时间的格式,由两位字符组成,第一位表示日期的格式,第二位表示时间的格式:S:短日期/时间格式、M:中日期/时间格式、L:长日期/时间格式、F:完整日期/时间格式、-:忽略日期或时间格式

数值格式化

  • @NumberFormat 可对类似数字类型的属性进行标注,它拥有两个互斥的属性:
    • style:类型为 NumberFormat.Style。用于指定样式类型,包括三种:Style.NUMBER(正常数字类型)、Style.CURRENCY(货币类型)、 Style.PERCENT(百分数类型)
    • pattern:类型为 String,自定义样式,如pattern="#,###"

示例

  • Java Bean
    public class Person {
      @DateTimeFormat(pattern = "yyyy-MM-dd")
      LocalDate birthday;
      @NumberFormat(pattern = "###,###.##")
      Double salary;
      /*省略*/
    }
    
  • 请求映射
      @RequestMapping("/formatTest")
      public String formatTest(Person person) {
        System.out.println(person);
        return "success";
      }
    
  • 请求表单
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
        <title>format test</title>
    </head>
    <body>
    <form action="${pageContext.request.contextPath}/formatTest" method="post">
        <label for="birthday">生日(yyyy-MM-dd):</label><input name="birthday" type="text" id="birthday"> <br/>
        <label for="salary">工资(###,###.##) </label><input name="salary" type="text" id="salary"><br/>
        <input type="submit" value="提交"><br/>
    </form>
    
    </body>
    </html>
    

JSR303数据校验

  • JSR 303 是 Java 为 Bean 数据合法性校验提供的标准框架,它已经包含在 JavaEE 6.0 中
  • JSR 303 通过在 Bean 属性上标注类似于 @NotNull@Max 等标准的注解指定校验规则,并通过标准的验证接口对 Bean 进行验证
    在这里插入图片描述
  • Hibernate Validator 是 JSR 303 的一个参考实现,除支持所有标准的校验注解外,它还支持以下的扩展注解
    在这里插入图片描述
  • Spring 在进行数据绑定时,可同时调用校验框架完成数据校验工作。在 Spring MVC 中,可直接通过注解驱动的方式进行数据校验
  • Spring 的 LocalValidatorFactroyBean 既实现了 Spring 的 Validator 接口,也实现了 JSR 303 的 Validator 接口。只要在 Spring 容器中定义了一个 LocalValidatorFactoryBean,即可将其注入到需要数据校验的 Bean 中。
  • <mvc:annotation-driven/> 会默认装配好一个 LocalValidatorFactoryBean,通过在处理方法的入参上标注 @Valid 注解即可让 Spring MVC 在完成数据绑定后执行数据校验的工作
  • Spring 本身并没有提供 JSR303 的实现,所以必须将 JSR303 的实现者的 jar 包放到类路径下

示例

  • 导包
    <!-- https://mvnrepository.com/artifact/org.hibernate.validator/hibernate-validator -->
    <dependency>
        <groupId>org.hibernate.validator</groupId>
        <artifactId>hibernate-validator</artifactId>
        <version>6.2.0.Final</version>
    </dependency>
    
  • Java Bean
    public class People {
      @Email  // 输入格式必须为email的格式
      private String email;
      @NotEmpty  // 必须非空
      private String lastName;
      @Past  // 必须是之前的日期
      @DateTimeFormat(pattern = "yyyy-MM-dd")
      private LocalDate birthday;
      @Future  // 必须是未来的时间
      @DateTimeFormat(pattern = "yyyy-MM-dd")
      private LocalDate plan;
    
      public People(@Email String email, @NotEmpty String lastName, @Past LocalDate birthday, @Future LocalDate plan) {
        this.email = email;
        this.lastName = lastName;
        this.birthday = birthday;
        this.plan = plan;
      }
    
      public People() {
      }
      /*省略*/
    }
    
  • 配置文件,必须开启
    <mvc:annotation-driven/>
    
  • 请求映射
      @RequestMapping("validationTest")
      // @Valid 表示这个参数需要被校验,bindingResult是校验的结果
      public String validationTest(@Valid People people, BindingResult bindingResult) {
        System.out.println(people);
        if (bindingResult.hasErrors()) {
          System.out.println("发生错误");
          return "forward:validation.jsp";
        }
        return "success";
      }
    
  • 请求表单。<form:input/>可以用于作为输入框,并且可以在转发之后,复现上一次输入框内容。<form:errors/>可以显示验证结果。modelAttribute="people"指定绑定的模型属性。
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
        <title>validation jsp</title>
    </head>
    <body>
        <form:form method="post" action="${pageContext.request.contextPath}/validationTest" modelAttribute="people">
            电子邮件:<form:input path="email"/> <form:errors path="email"/> <br/>
            姓氏:<form:input path="lastName"/><form:errors path="lastName"/><br/>
            生日:<form:input path="birthday"/><form:errors path="birthday"/><br/>
            计划:<form:input path="plan"/><form:errors path="plan"/><br/>
            <input type="submit" value="提交">
        </form:form>
    </body>
    </html>
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值