文档章节

Spring笔记 - 验证、数据绑定和类型转换

Peter_Peng
 Peter_Peng
发布于 2015/07/06 11:07
字数 2782
阅读 103
收藏 1

1. 验证Validation

1.1 基本用法

// Bean
public class Person {
    private String name;
    private int age;
    //setter & getter, constructor略
}
// Validator
public class PersonValidator implements Validator {
    public boolean supports(Class<?> aClass) {
        return Person.class.isAssignableFrom(aClass);
    }

    public void validate(Object o, Errors errors) {
        ValidationUtils.rejectIfEmpty(errors, "name", "person.name.null", "Person name cannot be null.");
        Person person = (Person) o;
        if (person.getAge() < 0)
            errors.reject("person.age.negative", "Age cannot be negative.");
    }
}
// 验证
@Test
public void testPersonValidator() {
    Person person = new Person("张三", -10);
    PersonValidator validator = new PersonValidator();
    Assert.assertTrue(validator.supports(Person.class));
    Errors errors = new DirectFieldBindingResult(person, "person");
    //亦可使用ValidationUtils.invokeValidator(...)
    validator.validate(person, errors);
	
    for (ObjectError objectError : errors.getAllErrors()) {
        System.out.println("Error Code is: " + objectError.getCode() + " | " + "Error DefaultMessage is: " +  objectError.getDefaultMessage());
    }
}

// web容器环境的用法在SpringMVC部分介绍


1.2 在消息格式化中进行验证

[参考]

#properties消息文件
person.age.negative=\u5e74\u9f84\u4e0d\u80fd\u5c0f\u4e8e\u0030+
person.name.null=\u7528\u6237\u540d\u4e0d\u80fd\u4e3a\u7a7a
// 自定义MessageCodesResolver 
@Component
public class MyMessageCodesResolver implements MessageCodesResolver {
    @Autowired
    ApplicationContext context;

    public String[] resolveMessageCodes(String errorCode, String objectName) {
        return new String[]{context.getMessage(errorCode, null, null)};
    }

    public String[] resolveMessageCodes(String errorCode, String objectName, String field, Class<?> fieldType) {
        return resolveMessageCodes(errorCode, objectName);
    }
}
// 验证
BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(person, "person");
bindingResult.setMessageCodesResolver(myMessageCodesResolver);
ValidationUtils.invokeValidator(new PersonValidator(), person, bindingResult);

for (ObjectError objectError : bindingResult.getAllErrors()) {
    System.out.println("Error Code is: " + objectError.getCode() + " | " + "Error DefaultMessage is: " +  objectError.getDefaultMessage());
}


1.3 BeanValidation规范JSR303, JSR409的支持

- Spring完整支持JSR303 (BeanValidation 1.0),该规范定义了基于注解方式的JavaBean验证元数据模型和API,也可以通过XML进行元数据定义,但注解将覆盖XML的元数据定义。

- Spring也支持JSR409 (BeanValidation 1.1),该规范标准化了Java平台的约束定义、描述和验证,其实现例如Hibernate Validator

- JSR303主要是对JavaBean进行验证,如方法级别(方法参数/返回值)、依赖注入等的验证是没有指定的。因此又有了JSR-349规范的产生。

[参考]

1.3.1 JSR303

JSR-303原生支持的限制有如下几种

限制 说明
@Null
@NotNull
@AssertFalse
@AssertTrue
@DecimalMax(value) 不大于指定值的数字
@DecimalMin(value) 不小于指定值的数字
@Digits(integer,fraction) 小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction
@Max(value) 不大于指定值的数字
@Min(value) 不小于指定值的数字
@Pattern(value) 符合指定的正则表达式
@Size(max,min) 字符长度必须在min到max之间
@Future 将来的日期
@Past 过去的日期

1.3.2 自定义限制

// 定义限制
@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MoneyValidator.class)
public @interface Money {
    String message() default "不是金额形式";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// 定义限制验证器
public class MoneyValidator implements ConstraintValidator<Money, Double> {
    private String moneyReg = "^\\d+(\\.\\d{1,2})?$";//表示金额的正则表达式
    private Pattern moneyPattern = Pattern.compile(moneyReg);
 
    public boolean isValid(Double value, ConstraintValidatorContext arg1) {
       if (value == null)
           return true;
       return moneyPattern.matcher(value.toString()).matches();
    }
}

1.3.3 配置Bean Validation Provider

<!-- 配置LocalValidatorFactoryBean,Spring会自动查找并加载类路径下的provider,例如Hibernate Validator -->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

<!-- 配置后,就可以通过注入validator的方式使用validator -->

1.3.4 Spring驱动的方法验证

对方法的参数、返回值进行验证 [参考]

// 没有方法验证时的做法
public UserModel get(Integer uuid) {
    //前置条件
    Assert.notNull(uuid);
    Assert.isTrue(uuid > 0, "uuid must lt 0");
    //获取 User Model
    UserModel user = getUserModel(uuid); //从数据库获取
    //后置条件
    Assert.notNull(user);
    return user;
}

// 有方法验证时的做法
// a. 使用方法验证
@Validated // 告诉MethodValidationPostProcessor此Bean需要开启方法级别验证支持
public class UserService {
    public @NotNull UserModel getUserModel(@NotNull @Min(value = 1) Integer uuid) { //声明前置条件/后置条件
        if(uuid > 100) {//方便后置添加的判断(此处假设传入的uuid>100 则返回null)
            return null;
        }
        return getUserModel(uuid); //从数据库获取
    }
}
// b. 配置方法验证的后处理器
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>
// c. 测试用例
@RunWith(value = SpringJUnit4ClassRunner.class)
@ContextConfiguration(value = {"classpath:spring-config-method-validator.xml"})
public class MethodValidatorTest {
    @Autowired
    UserService userService;
    @Test
    public void testConditionSuccess() { // 正常流程 
        userService.getUserModel(1);
    }
    @Test(expected = org.hibernate.validator.method.MethodConstraintViolationException.class)
    public void testPreCondtionFail() { // 错误的uuid(即前置条件不满足)
        userService.getUserModel(0);
    }
    @Test(expected = org.hibernate.validator.method.MethodConstraintViolationException.class)
    public void testPostCondtionFail() { // 不满足后置条件的返回值
        userService.getUserModel(10000);
    }
}


1.4 在数据绑定中使用validator

Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator1());
binder.addValidators(new FooValidator2());
binder.replaceValidators(new FooValidator3());
// bind to the target object
binder.bind(propertyValues);
// validate the target object
binder.validate();
// get BindingResult that includes any validation errors
BindingResult results = binder.getBindingResult();


2. 操作Bean

2.1 BeanWrapper

- 封装一个bean的行为,诸如设置和获取属性值等

- 根据JavaDoc中的说明,BeanWrapper提供了设置和获取属性值(单个的或者是批量的),获取属性描述信息、查询只读或者可写属性等功能。不仅如此,BeanWrapper还支持嵌套属性,设置子属性的值。BeanWrapper无需任何辅助代码就可以支持标准JavaBean的PropertyChangeListeners和VetoableChangeListeners。此外,还支持设置索引属性。通常不直接使用BeanWrapper而是使用DataBinder 和BeanFactory

// 被操作的类
class Engine {
    private String name;
    //setter & getter ...
}
// 被操作的类
class Car {
    private String name;
    private Engine engine;
    //setter & getter ...
}
// 创建并设置Bean
BeanWrapper engine = BeanWrapperImpl(new Engine());
engine.setPropertyValue("name", "N73B68A"); // 也可以这样设置 engine.setPropertyValue(new PropertyValue("name", "N73B68A");
// 创建并设置Bean
BeanWrapper car = BeanWrapperImpl(new Car());
car.setPropertyValue("name", "劳斯莱斯幻影");
car.setPropertyValue("engine", engine.getWrappedInstance());

// 获取嵌套属性
String engineName = (String) car.getPropertyValue("engine.name");

2.2 PropertyEditor

- Spring大量使用了PropertyEditor以在Object和 String之间进行转化

- PE本来是Java为IDE可视化设置JavaBean属性而准备的,Spring对此进行了封装以简化使用[参考]

2.2.1 Spring内建属性编辑器

类型 内建PropertyEditor (是否已在BeanWrapperImpl注册)
基础数据

ByteArrayProperty (Y)、CustomBoolean (Y)、CustomDate (N)、CustomNumber (Y)

集合 CustomCollection (?)
资源 Class (Y)、File (Y)、InputStream (Y)、Locale (Y)、Pattern (?)、Properties (Y)、URL (Y)、StringTrimmer (N)

2.2.2 自定义属性编辑器

2.2.2.1 扩展PropertyEditorSupport 

// 定义Editor
class EngineEditor extends PropertyEditorSupport {
    public void setAsText(String text){
        if(text == null)
            throw new IllegalArgumentException("设置的字符串格式不正确");
        Engine engine = new Engine();
        engine.setName(text);
        setValue(engine);
    }
}
// 注册Editor
// 如果自定义Editor和被处理的类在同一包下面,则无需xml注册,会被自动识别
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="customEditors">
        <map>
            <entry key="x.y.z.Engine" value="x.y.z.EngineEditor"/>
        </map>
    </property>
</bean>

// 使用Editor
<bean id="car" class="x.y.z.Car" p:name="劳斯莱斯幻影">
    <property name="engine" value="N73B68A" />
</bean>

2.2.2.2 实现PropertyEditorRegistrar接口

在不同情况下(如编写一个相应的注册器然后在多种情况下重用它)需要使用相同的属性编辑器时该接口特别有用

// 现在Java代码里面注册
public final class CustomPropertyEditorRegistrar implements PropertyEditorRegistrar {
    public void registerCustomEditors(PropertyEditorRegistry registry) {
        registry.registerCustomEditor(Engine.class, new EngineEditor());
    }
}
<!-- 再到XML注册 -->
<bean id="customPropertyEditorRegistrar" class="com.foo.editors.spring.CustomPropertyEditorRegistrar"/>
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
    <property name="propertyEditorRegistrars">
        <list>
            <ref bean="customPropertyEditorRegistrar"/>
        </list>
    </property>
</bean>


3. 类型转换

3.1 概述

- Spring容器使用转换器进行属性注入;SpEL和DataBinder使用转换器绑定值

- 不同于Property的Object和String之间的转换,类型转换可以有更多的格式互转

- 转换器Converter可实现以下接口:

  • 非泛型的强类型转换用Converter接口

  • 一个类型转成指定基类的子类型用ConverterFactory接口

  • 多个源类型转成多个目标类型用GenericConverter接口,优先用前面两个

- ConversionService接口实现作为无状态的工具在运行时提供转换服务,可供多个线程共享,可在其中设置各种转换器.


3.2 Converter接口

将一个源类型转换成一个目标类型

public interface Converter<S, T> {
    T convert(S source);
}

final class StringToCarConverter implements Converter<String, Car> {
    public Car convert(String source) {
        return new Car(source);
    }
}


3.3 ConverterFactory接口

一个源类型转成指定基类的子类型

public interface ConverterFactory<S, R> {
    <T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

// given Car & Truck extends Auto
final class StringToAutoConverterFactory implements ConverterFactory<String, Auto> {
    public <T extends Auto> Converter<String, T> getConverter(Class<T> targetType) {
        if(targetType.getType() == Car.class)
            return new StringToCarConverter();
        else if(targetType.getType() == Truck.class)
            return new StringToTruckConverter();
        else
            throw new ConversionFailedException(String.getType(), targetType, null, new Throwable("不支持的转换类型"));
    }
    private final class StringToCarConverter implements Converter<String, Car> {
        public Car convert(String source) {
            return new Car(source);
        }
    }
    // StringToTruckConverter ...
}


3.4 GenericConverter接口

- 多个源类型转成多个目标类型

- ConditionalGenericConverter接口继承该接口,增加了boolean matches(...)方法

public interface GenericConverter {
    Set<ConvertiblePair> getConvertibleTypes();
    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
    
    public static final class ConvertiblePair {
        public ConvertiblePair(Class<?> sourceType, Class<?> targetType) {
            Assert.notNull(sourceType, "Source type must not be null");
            Assert.notNull(targetType, "Target type must not be null");
            this.sourceType = sourceType;
            this.targetType = targetType;
        }
        private final Class<?> sourceType;
        private final Class<?> targetType;
        // getter & setter
    }
 
}

final class GiftGenericConverter implements GenericConverter {
   public Set<ConvertiblePair> getConvertibleTypes() {
       Set<ConvertiblePair> pairs = new HashSet<ConvertiblePair>();
       paris.add(new ConvertiblePair(String.class, Flower.class));
       paris.add(new ConvertiblePair(Integer.class, Toy.class));
       return pairs;
   }
   Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
      if (source == null || sourceType == TypeDescriptor.NULL || targetType == TypeDescriptor.NULL) {  
          throw new ConversionFailedException(sourceType, targetType, null,
              new IllegalArgumentException("A null value cannot be assigned to a primitive type"));
      }
      if(targetType.getType() == String.class)
          return new Flower(source);
      else if(targetType.getType() == Integer.class)
          return new Toy(source);
   }
}


3.5 Conversion Service

- 无状态的工具在运行时提供转换服务,可供多个线程共享,可在其中设置各种转换器

- 内置GenericConversionService实现ConvensionService接口

- 也可以配置ConversionServiceFactoryBean提供转换服务

3.5.1 GenericConversionService

<bean id="conversionService" class="org.springframework.core.convert.support.GenericConversionService"/>
// 注入转换Bean就可以使用转换服务了
@Autowired
ConversionService conversionService;
public void doSth() {
    conversionService.convert(source, targetType);
    List<Integer> input = ....
    conversionService.convert(input,
        TypeDescriptor.forObject(input), // List<Integer> type descriptor
        TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));
}

// 也可以创建转换器实例,通常不需要

3.5.2 ConversionServiceFactoryBean

<bean id="conversionService"
        class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
            <bean class="x.y.z.StringToCarConverter"/>
            <bean class="x.y.z.StringToAutoConverterFactory"/>
            <bean class="x.y.z.GiftGenericConverter"/>
        </set>
    </property>
</bean>
// 使用方法同GenericConversionService


4. 格式化

4.1 概述

Spring格式化Formatter与Converter类似,但可以指定Locale,根据不同的Locale进行不同的双向格式化 (print/parse)


4.2 Formatter接口

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

public final class MyDateFormatter implements Formatter<Date> {
    public String print(Date object, Locale locale) {
        return new SimpleDateFormat("yyyy-MM-dd", locale).format(object);
    }
    public Date parse(String text, Locale locale) throws ParseException {
        return new SimpleDateFormat("yyyy-MM-dd", locale).parse(text);
    }
}


4.3 基于注解的格式化

[参考]

public interface AnnotationFormatterFactory<A extends Annotation> {
    Set<Class<?>> getFieldTypes();
    Printer<?> getPrinter(A annotation, Class<?> fieldType);
    Parser<?> getParser(A annotation, Class<?> fieldType);
}

// a. 创建注解
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface PhoneNumber {
}

// b. 要格式化的模型
public class PhoneNumberModel {
    private int areaCode, userNumber;
    // getter & setter, constructor ... 
}

// c. 实现工厂接口
public class PhoneNumberFormatAnnotationFormatterFactory implements AnnotationFormatterFactory<PhoneNumber> {
    public Set<Class<?>> getFieldTypes() {
        return fieldTypes;
    }
    public Parser<?> getParser(PhoneNumber annotation, Class<?> fieldType) {
        return formatter;
    }    
    public Printer<?> getPrinter(PhoneNumber annotation, Class<?> fieldType) {
        return formatter;
    }
    
    private final Set<Class<?>> fieldTypes;
    private final PhoneNumberFormatter formatter;
    public PhoneNumberFormatAnnotationFormatterFactory() {
        Set<Class<?>> set = new HashSet<Class<?>>();
        set.add(PhoneNumberModel.class);
        this.fieldTypes = set;
        this.formatter = new PhoneNumberFormatter(); // 之前定义的Formatter实现
    }
}

// d. 在需要格式化的字段前面加注解
public class contact {
    @PhoneNumber
    private PhoneNumberModel phoneNumber;
    // other fields ...
}

// e. 测试使用。这个用例有些牵强,更多是注册后,在属性注入、SpEL、数据绑定时,由容器调用自动完成格式化
@Test
public void test() throws SecurityException, NoSuchFieldException {
    DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(); // 创建格式化服务
    conversionService.addFormatterForFieldAnnotation(new PhoneNumberFormatAnnotationFormatterFactory()); // 添加自定义的注解格式化工厂
        
    TypeDescriptor phoneNumberDescriptor = new TypeDescriptor(FormatterModel.class.getDeclaredField("phoneNumber"));
    TypeDescriptor stringDescriptor = TypeDescriptor.valueOf(String.class);
    
    PhoneNumberModel value = (PhoneNumberModel) conversionService.convert("010-12345678", stringDescriptor, phoneNumberDescriptor); // 解析字符串"010-12345678"--> PhoneNumberModel
    ContactModel contact = new ContactModel();
    contact.setPhoneNumber(value);
        
    Assert.assertEquals("010-12345678", conversionService.convert(contact.getPhoneNumber(), phoneNumberDescriptor, stringDescriptor)); // 格式化PhoneNumberModel-->"010-12345678"
}


4.4 注册Formatter

- 实现FormatterRegistry接口,或使用内置实现FormattingConversionService,通常使用FormattingConversionServiceFactoryBean进行配置

- 实现FormatterRegistrar接口

<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="formatters">
        <list>
            <bean class="x.y.z.PhoneNumberFormatAnnotationFormatterFactory"/>
        </list>
    </property>
    <property name="converters">
        <set>
            <bean class="org.example.MyConverter"/>
        </set>
    </property>
    <property name="formatterRegistrars">
        <set>
            <bean class="org.example.MyFormatterRegistrar"/>
        </set>
    </property>
</bean>


4.5 注册全局日期和时间格式

Spring默认使用DateFormat.SHORT进行日期和时间格式化,在DefaultFormattingConversionService未被注册的情况下,可以自定义全局日期和时间格式

// a. 以JavaConfig的方式注册全局日期格式yyyyMMdd
@Bean
public FormattingConversionService conversionService() {
    // 使用但不注册DefaultFormattingConversionService
    DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService(false);
    // 确保@NumberFormat被支持
    conversionService.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());
    // 注册全局格式
    DateFormatterRegistrar registrar = new DateFormatterRegistrar();
    registrar.setFormatter(new DateFormatter("yyyyMMdd"));
    registrar.registerFormatters(conversionService);

    return conversionService;
}
<!-- b. 以xml的方式注册全局日期格式yyyyMMdd,且用到了Joda-Time第三方库
<bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
    <property name="registerDefaultFormatters" value="false" />
    <property name="formatters">
        <set>
            <bean class="org.springframework.format.number.NumberFormatAnnotationFormatterFactory" />
        </set>
    </property>
    <property name="formatterRegistrars">
        <set>
            <bean class="org.springframework.format.datetime.joda.JodaTimeFormatterRegistrar">
                <property name="dateFormatter">
                    <bean class="org.springframework.format.datetime.joda.DateTimeFormatterFactoryBean">
                        <property name="pattern" value="yyyyMMdd"/>
                    </bean>
                </property>
            </bean>
        </set>
    </property>
</bean>


© 著作权归作者所有

共有 人打赏支持
Peter_Peng
粉丝 4
博文 8
码字总数 10793
作品 0
普陀
私信 提问
MVC三层架构在各框架中的特征

1.基于web开发中最原始的jsp+Servlet 图形化理解jsp+servlet结构: 1.从结构上分析jsp+servlet图解原理: 在基于mvc设计模式下的最原始的jsp+Servlet框架,在某种程度上是不能够达到mvc最直观...

鲁雯雪
2014/03/14
0
159
spring mvc 的数据绑定,数据验证

正在学习spring(3.0.5) mvc,有个问题想请教一下:如何处理在数据绑定时候出的错呢?如何自定义出错的信息呢? 比如说:Object中要求是Integer,但是前台用户输入了字母A,这种情况,在Spring ...

一袭青衫
2013/04/18
1K
6
如何使用SpringMVC进行数据校验

前言: SpringMVC数据校验采用JSR-303校验。 • Spring 4.0 拥有自己独立的数据校验框架,同时支持 JSR 303 标准的校验框架。 • Spring 在进行数据绑定时,可同时调用校验框架完成数据校 验...

帅得拖网速
2016/09/01
574
0
【原创】遨游springmvc之WebDataBinder

1.前言 先上原理图 在我们学习servlet的时候我们知道有一个方法叫做:request.getParameter("paramName"),它返回的是一个String类型,但是如果一切都是这样子我们开发程序的时候就会显得特别...

开源中国首席脑科主任
2016/08/02
471
1
Spring MVC学习笔记(二)

在方法参数上使用@ModelAttribute表明参数的值需要从model中获取。如果model不存在,参数应当首先被实例化然后添加进model。一旦model存在,参数的属性需要从名称匹配的请求参数中获取。 示例...

第五郎
2014/03/21
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Linux iptables之mangle表使用案例

mangle表的用途 mangle表的主要功能是根据规则修改数据包的一些标志位,以便其他规则或程序可以利用这种标志对数据包进行过滤或策略路由。 mangel表使用示例 示例1-策略路由1 内网的客户机通...

月下狼
50分钟前
2
0
OSChina 周日乱弹 —— 兼职我想去学学布偶戏

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @clouddyy : 《火炎 - 女王蜂》 《火炎 - 女王蜂》 手机党少年们想听歌,请使劲儿戳(这里) @小鱼丁 :还在睡觉突然接到一个小哥哥电话“x...

小小编辑
今天
62
5
租房软件隐私保护如同虚设

近日,苏州市民赵先生向江苏新闻广播新闻热线025-84658888反映,他在“安居客”手机应用软件上浏览二手房信息,并且使用该软件自动生成的虚拟号码向当地一家中介公司进行咨询。可电话刚挂不久...

linux-tao
今天
3
0
分布式项目(五)iot-pgsql

书接上回,在Mapping server中,我们已经把数据都整理好了,现在利用postgresql存储历史数据。 iot-pgsql 构建iot-pgsql模块,这里我们写数据库为了性能考虑不在使用mybatis,换成spring jd...

lelinked
今天
6
0
一文分析java基础面试题中易出错考点

前言 这篇文章主要针对的是笔试题中出现的通过查看代码执行结果选择正确答案题材。 正式进入题目内容: 1、(单选题)下面代码的输出结果是什么? public class Base { private Strin...

一看就喷亏的小猿
今天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部