Spring MVC 数据绑定(二)

上篇:Spring MVC 数据绑定(一)
Spring MVC通过反射机制对目标处理方法的签名进行分析,将请求信息绑定到处理方法的入参中。在参数解析之后,紧跟着进行参数的类型转换、格式化、校验等。这里主要讲解参数解析之后的过程。

数据绑定的核心部件是DataBinder,其运行机制如图

数据绑定.png

流程大概分为几个步骤:

  1. Spring MVC主框架将ServeltRequest对象及处理方法的入参对象实例传递给 DataBinder

  2. DataBinder首先调用装配在Spring Web上下文中的ConversionService组件进行数据类型转换、数据格式化工作,将ServletRequest对象填充到入参对象中,

  3. 然后调用Validator组件对已经绑定了请求消息数据的入参对象进行数据合法性校验

  4. 最终生成数据绑定结果BindingResult对象。BindingResult对象包含了已完成数据绑定的入参对象,还包含相应的校验错误对象。

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

传递参数到DataBinder

public class AnnotationMethodHandlerAdapter extends WebContentGenerator
 implements HandlerAdapter, Ordered, BeanFactoryAware {

 ...
 @Override
 protected void doBind(WebDataBinder binder, NativeWebRequest webRequest) throws Exception {
 ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
 servletBinder.bind(webRequest.getNativeRequest(ServletRequest.class));
 }
 ...
}

数据类型转换

Spring在核心模块中添加了一个通用的类型转换模块org.springframework.core.convert,希望通过整个模块体系替换Java标准的PropertyEditor。由于历史原因,Spring同时支持两者,在Bean配置、SpringMVC处理方法入参的过程中都会使用。

ConversionService

ConversionService 是 Spring类型转换体系的核心接口,在此接口中,定义了4个方法

public interface ConversionService {
 //判断是否可以将一个Java类转换为另外一个Java类,类似于PropertyEditor中的方法
 boolean canConvert(Class<?> sourceType, Class<?> targetType);
​
 /*
 需转换的类将以成员变量的方式出现在宿主类中。
 TypeDescriptor描述了需转换类的信息,还描述了宿主类的上下文信息,如成员变量上的注解,
 成员变量是否以数组、集合或Map的方式呈现。
 类型转换逻辑可以利用这些信息做出灵活的控制,这是PropertyEditor所做不到的。
 */
 boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
​
 //将原类型对象转换为目标类型对象,类似于PropertyEditor中的方法
 <T> T convert(Object source, Class<T> targetType);
​
 //将原类型对象转换为目标类型对象,此方法会用到宿主类上下文信息
 Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
​
}

在使用时,可以使用ConversionServiceFactoryBean在Spring上下文中定义一个ConversionService ,Spring会自动识别上下文中的ConversionService,并在Bean属性配置及SpringMVC处理方法入参绑定时使用它进行数据转换。

<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"/>

该FactoryBean 创建的 conversionService内置了很多转换器,可以完成大多数Java类型的转换工作,包括String、Number、Array、Collection、Map、Properties及Object。

也可以注册自定义的类型转换器,自定义的转换器需要实现特定的接口。具体见下面转换器。

<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <list>
            <bean>MyConverter</bean>
        </list>
    </property>
</bean>

转换器

Spring 在org.springframework.core.convert.converter中定义了多个Converter接口

Converter<S, T>
public interface Converter<S, T> {
    @Nullable
    T convert(S var1);
}

它是Spring中最简单的一个转换器接口,负责将S类型的对象转换为T类型的对象。如果需要将String转换为Number及Number的子类Integer、Long、Double等对象,就需要一系列的Converter,如StringToInteger、StringToLong、StringToDouble。

基于以上原因,Spring提供了一个将相同系列多个同质Convert封装在一起的ConvertFactory接口

ConverterFactory

S为转换的源类型,R为目标类型的基类,而T为扩展于R基类的类型。

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

如StringToNumberConverterFactory就实现了ConverterFactory接口,封装了String转换到各个数据类型的Converter。

final class StringToNumberConverterFactory implements ConverterFactory<String, Number> {
    StringToNumberConverterFactory() {
    }

    public <T extends Number> Converter<String, T> getConverter(Class<T> targetType) {
        return new StringToNumberConverterFactory.StringToNumber(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;
        }

        public T convert(String source) {
            return source.isEmpty() ? null : NumberUtils.parseNumber(source, this.targetType);
        }
    }
}

Converter只负责将对象与对象之间的转换,并没有考虑类型对象所在宿主类上下文的信息。GenericConverter接口会根据源类对象及目标类对象所在宿主类的上下文信息进行类型转换工作。

GenericConverter

GenericConverter.ConvertiblePair封装了源类型和目标类型,TypeDescriptor包含了需转换类型对象所在宿主类的信息,因此GenericConverter 的 convert接口方法可以利用上下文信息进行类型转换工作。

public interface GenericConverter {
    @Nullable
    Set<GenericConverter.ConvertiblePair> getConvertibleTypes();

    @Nullable
    Object convert(@Nullable Object var1, TypeDescriptor var2, TypeDescriptor var3);

    public static final class ConvertiblePair {
        private final Class<?> sourceType;
        private final Class<?> targetType;

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

        ...
    }
}
ConditionalConverter

ConditionalGenericConverter继承了GenericConverter 和 ConditionalConverter,自身并没有接口方法

public interface ConditionalGenericConverter 
    extends GenericConverter, ConditionalConverter {
}

在ConditionalConverter 中定义了一个接口方法,该方法只有只有返回true后,才能调用convert()方法进行类型转换。

public interface ConditionalConverter {
    boolean matches(TypeDescriptor var1, TypeDescriptor var2);
}

ConversionServiceFactoryBean 的converts属性可接受Converter、ConverterFactory、GenericConverter、

ConditionalConverter接口的实现类,并把这些转换器的转换逻辑统一封装到一个ConversionService实例对象中。Spring在属性配置及Spring MVC请求消息绑定时将使用这个ConversionService完成数据转换。

使用ConversionService

我们可以自定义一个Converter

@Component
public class StringtoUserConvert implements Converter<String, User> {
    @Override
    public User convert(String s) {
        User user = new User();
        user.setName(s);
        user.setAge(0);
        return user;
    }
}

配置到ConversionService到上下文。<mvc:annotation-driven/>在默认情况下,该标签会创建一个默认的RequestMappingHandlerMappingRequestMappingHandlerAdapter实例,还会注册一个默认的ConversionService(FormattingConversionServiceFactoryBean)以满足大部分类型转换的需求。

我们这里显示的定义一个conversionService代替默认实现,实际开发中不建议这么做。

spring 3.1 开始我们应该用RequestMappingHandlerMapping 来替换 DefaultAnnotationHandlerMapping,用 RequestMappingHandlerAdapter 来替换 AnnotationMethodHandlerAdapter,提供更多的扩展点。

<!-- mvc注解驱动 装备自定义的conversionService -->
<mvc:annotation-driven conversion-service="conversionService"/>
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
             <!-- 装配自定义的转换器converter -->
            <ref bean="stringtoUserConvert"/>
        </set>
    </property>
</bean>

数据格式化

Spring使用转换器完成对象目标类型的转换,转换过程不包含输入、输出信息的格式化。如日期,数字,时间,货币等数据都是具有一定格式的,在不同的本地化环境中,同一类型的数据显示不同的数据格式。

从格式化数据中获取真正数据并完成绑定,并将处理完成的数据输出为格式化的数据,Spring提供了格式化框架org.springframework.format。其中最重要的接口是Formatter<T>接口。

Formatter<T>

Printer<T>负责对象的格式化输出,Parser<T>负责对象的格式化输入

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

在两个接口中,各有一个接口方法

//将类型为T的成员根据本地化的不同输出为不同格式的字符串
@FunctionalInterface
public interface Printer<T> {
    String print(T var1, Locale var2);
}

//参考本地化信息将一个格式化的字符串转换为T类型的对象
@FunctionalInterface
public interface Parser<T> {
    T parse(String var1, Locale var2) throws ParseException;
}

格式化注解驱动

spring提供了注解驱动接口AnnotationFormatterFactory

public interface AnnotationFormatterFactory<A extends Annotation> {
    //注解A的应用范围,哪些属性类可以标注A注解
    Set<Class<?>> getFieldTypes();
    //获取属性A特定Printer
    Printer<?> getPrinter(A var1, Class<?> var2);
    //获取属性A特定Parser
    Parser<?> getParser(A var1, Class<?> var2);
}

spring提供了多个内建的实现类,通过名字很容易理解每个实现类的作用


AnnotationFormatterFactory实现类

启用格式化注解驱动

spring是基于对象转换框架植入格式化功能的,Spring在格式化模块定义了一个实现ConversionService接口的FormattingConversionService,它既继承了GenericConversionService,又实现了FormatterRegistry(实现FormatterRegistry是实现Formatter的一种方式)。

public class FormattingConversionService extends GenericConversionService
        implements FormatterRegistry, EmbeddedValueResolverAware {}

ConversionServiceFactoryBean一样,FormattingConversionService也有自己的工厂类FormattingConversionServiceFactoryBean,在上下文中构造这个工厂类,可以注册自定义转换器,还可以注册自定义注解驱动逻辑。

FormattingConversionService在内部会自动注册(这块源码没找到,应该读取配置文件是通过set方法注入)NumberFormatAnnotationFormatterFactory,JodaDateTimeFormatAnnotationFormatterFactory,因此,在装配了FormattingConversionService后,就可以在Spring MVC入参绑定及模型输出时使用注解驱动进行格式化。

public class User {
    private String name;
    private String age;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private Date birthday;

    @NumberFormat(pattern = "#,###.##")
    private long salary;
    ...
}

数据校验

JRS-303

JRS-303是Java为Bean数据合法性校验提供的标准框架,包含在Java EE6.0中,通过在Bean属性上标注类似@NotNull、@Max等注解指定校验规则,并通过标准的验证接口对Bean进行验证。

参考:JSR 303 - Bean Validation 介绍及最佳实践

Constraint 详细信息
@Null 被注释的元素必须为 null
@NotNull 被注释的元素必须不为 null
@AssertTrue 被注释的元素必须为 true
@AssertFalse 被注释的元素必须为 false
@Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Size(max, min) 被注释的元素的大小必须在指定的范围内
@Digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内
@Past 被注释的元素必须是一个过去的日期
@Future 被注释的元素必须是一个将来的日期
@Pattern(value) 被注释的元素必须符合指定的正则表达式

Hibernate Validator 是 Bean Validation 的参考实现 . Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint 的实现,除此之外还有一些附加的 constraint。

Constraint 详细信息
@Email 被注释的元素必须是电子邮箱地址
@Length 被注释的字符串的大小必须在指定的范围内
@NotEmpty 被注释的字符串的必须非空
@Range 被注释的元素必须在合适的范围内
<!-- 校验接口 JRS-303 -->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.1.0.Final</version>
</dependency>
<!-- JRS-303 实现 -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.4.1.Final</version>
</dependency>

Spring数据校验

Spring拥有自己独立的数据验证框架,同时支持JRS-303标准框架。Spring的DataBinder在进行数据绑定时,可同时调用校验框架完成数据校验,在Spring MVC中,可直接通过注解驱动的方式进行数据校验。

Validator

最基本的Spring校验接口

package org.springframework.validation;

public interface Validator {
    //对clazz类型的对象进行校验
    boolean supports(Class<?> clazz);
    //对目标类target进行校验,并将校验错误记录在errors中
    void validate(Object target, Errors errors);
}
LocalValidatorFactoryBean

LocalValidatorFactoryBean既实现了spring的Validator接口,又实现了JRS-303的Validator接口。

public class LocalValidatorFactoryBean extends SpringValidatorAdapter implements ValidatorFactory, ApplicationContextAware, InitializingBean, DisposableBean {}

public class SpringValidatorAdapter 
    implements SmartValidator, javax.validation.Validator {}

在Spring中定义一个bean,即可将其加入需要校验的bean中。另外一点,Spring并没有提供JRS-303的实现,必须引入相关实现者的jar包。

<!-- 校验器 -->
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>

Spring MVC数据校验

public class User {
    @Pattern(regexp = "w{4,30}")
    private String name;

    @Min(18)
    private Integer age;
    ...
}
@RequestMapping("/user")
/*
    Spring MVC通过处理方法签名的规约来保存校验结果,前一个校验结果保存在其后的入参中,必须要是
    BindingResult或Errors类型,之间不允许其他入参
*/
public String UserController(@Valid User user, BindingResult bindingResult){
    if(bindingResult.hasErrors()){
        System.out.println(bindingResult.getFieldErrors("age"));
    }else{
        System.out.println("no errors");
    }
    return null;
}
控制台信息:
[Field error in object 'user' on field 'age': rejected value [1]; codes [Min.user.age,Min.age,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age],18]; default message [最小不能小于18]]

自定义校验规则

可以使用注解@InitBinder设置自定义的Validator

@RestController
public class UserController {

    //这种方式会放弃Spring框架装载的validator
    @InitBinder
    public void initBinder(WebDataBinder binder){
        binder.setValidator(new UserValidator());
    }
    ...
}

也可以在方法中直接校验或者手动添加错误信息

@RequestMapping("/user2")
public String getUser2(User user, BindingResult bindingResult){
    //校验是否为空或有空格
    ValidationUtils.rejectIfEmptyOrWhitespace(bindingResult,"name","required");

    if("aaa".equalsIgnoreCase(user.getName())){
        bindingResult.rejectValue("name","reserved");
    }
    return null;
}
/*
产生的对应错误信息:
    required.user.name
    required.name
    required.java.lang.String
    required

    reserved.user.name
    reserved.name
    reserved.java.lang.String
    reserved
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容