Spring MVC 數(shù)據(jù)綁定(二)

上篇:Spring MVC 數(shù)據(jù)綁定(一)
Spring MVC通過反射機制對目標處理方法的簽名進行分析逞姿,將請求信息綁定到處理方法的入?yún)⒅兄P恕T趨?shù)解析之后坑律,緊跟著進行參數(shù)的類型轉(zhuǎn)換匾南、格式化、校驗等馏锡。這里主要講解參數(shù)解析之后的過程雷蹂。

數(shù)據(jù)綁定的核心部件是DataBinder,其運行機制如圖

數(shù)據(jù)綁定.png

流程大概分為幾個步驟:

  1. Spring MVC主框架將ServeltRequest對象及處理方法的入?yún)ο髮嵗齻鬟f給 DataBinder

  2. DataBinder首先調(diào)用裝配在Spring Web上下文中的ConversionService組件進行數(shù)據(jù)類型轉(zhuǎn)換杯道、數(shù)據(jù)格式化工作匪煌,將ServletRequest對象填充到入?yún)ο笾校?/p>

  3. 然后調(diào)用Validator組件對已經(jīng)綁定了請求消息數(shù)據(jù)的入?yún)ο筮M行數(shù)據(jù)合法性校驗

  4. 最終生成數(shù)據(jù)綁定結果BindingResult對象。BindingResult對象包含了已完成數(shù)據(jù)綁定的入?yún)ο螅€包含相應的校驗錯誤對象萎庭。

  5. SpringMVC抽取BindingResult中的入?yún)ο蠹靶r炲e誤對象霜医,將他們賦給處理方法的相應入?yún)ⅰ?/p>

傳遞參數(shù)到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));
 }
 ...
}

數(shù)據(jù)類型轉(zhuǎn)換

Spring在核心模塊中添加了一個通用的類型轉(zhuǎn)換模塊org.springframework.core.convert,希望通過整個模塊體系替換Java標準的PropertyEditor驳规。由于歷史原因肴敛,Spring同時支持兩者,在Bean配置吗购、SpringMVC處理方法入?yún)⒌倪^程中都會使用医男。

ConversionService

ConversionService 是 Spring類型轉(zhuǎn)換體系的核心接口,在此接口中捻勉,定義了4個方法

public interface ConversionService {
 //判斷是否可以將一個Java類轉(zhuǎn)換為另外一個Java類镀梭,類似于PropertyEditor中的方法
 boolean canConvert(Class<?> sourceType, Class<?> targetType);
?
 /*
 需轉(zhuǎn)換的類將以成員變量的方式出現(xiàn)在宿主類中。
 TypeDescriptor描述了需轉(zhuǎn)換類的信息贯底,還描述了宿主類的上下文信息丰辣,如成員變量上的注解,
 成員變量是否以數(shù)組禽捆、集合或Map的方式呈現(xiàn)笙什。
 類型轉(zhuǎn)換邏輯可以利用這些信息做出靈活的控制,這是PropertyEditor所做不到的胚想。
 */
 boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
?
 //將原類型對象轉(zhuǎn)換為目標類型對象琐凭,類似于PropertyEditor中的方法
 <T> T convert(Object source, Class<T> targetType);
?
 //將原類型對象轉(zhuǎn)換為目標類型對象,此方法會用到宿主類上下文信息
 Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
?
}

在使用時浊服,可以使用ConversionServiceFactoryBean在Spring上下文中定義一個ConversionService 统屈,Spring會自動識別上下文中的ConversionService,并在Bean屬性配置及SpringMVC處理方法入?yún)⒔壎〞r使用它進行數(shù)據(jù)轉(zhuǎn)換牙躺。

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

該FactoryBean 創(chuàng)建的 conversionService內(nèi)置了很多轉(zhuǎn)換器愁憔,可以完成大多數(shù)Java類型的轉(zhuǎn)換工作,包括String孽拷、Number吨掌、Array、Collection脓恕、Map膜宋、Properties及Object。

也可以注冊自定義的類型轉(zhuǎn)換器炼幔,自定義的轉(zhuǎn)換器需要實現(xiàn)特定的接口秋茫。具體見下面轉(zhuǎn)換器。

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

轉(zhuǎn)換器

Spring 在org.springframework.core.convert.converter中定義了多個Converter接口

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

它是Spring中最簡單的一個轉(zhuǎn)換器接口乃秀,負責將S類型的對象轉(zhuǎn)換為T類型的對象肛著。如果需要將String轉(zhuǎn)換為Number及Number的子類Integer圆兵、Long、Double等對象策泣,就需要一系列的Converter衙傀,如StringToInteger、StringToLong萨咕、StringToDouble统抬。

基于以上原因,Spring提供了一個將相同系列多個同質(zhì)Convert封裝在一起的ConvertFactory接口

ConverterFactory

S為轉(zhuǎn)換的源類型危队,R為目標類型的基類聪建,而T為擴展于R基類的類型。

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

如StringToNumberConverterFactory就實現(xiàn)了ConverterFactory接口茫陆,封裝了String轉(zhuǎn)換到各個數(shù)據(jù)類型的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只負責將對象與對象之間的轉(zhuǎn)換,并沒有考慮類型對象所在宿主類上下文的信息簿盅。GenericConverter接口會根據(jù)源類對象及目標類對象所在宿主類的上下文信息進行類型轉(zhuǎn)換工作挥下。

GenericConverter

GenericConverter.ConvertiblePair封裝了源類型和目標類型,TypeDescriptor包含了需轉(zhuǎn)換類型對象所在宿主類的信息桨醋,因此GenericConverter 的 convert接口方法可以利用上下文信息進行類型轉(zhuǎn)換工作棚瘟。

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后偎蘸,才能調(diào)用convert()方法進行類型轉(zhuǎn)換。

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

ConversionServiceFactoryBean 的converts屬性可接受Converter瞬内、ConverterFactory迷雪、GenericConverter、

ConditionalConverter接口的實現(xiàn)類虫蝶,并把這些轉(zhuǎn)換器的轉(zhuǎn)換邏輯統(tǒng)一封裝到一個ConversionService實例對象中章咧。Spring在屬性配置及Spring MVC請求消息綁定時將使用這個ConversionService完成數(shù)據(jù)轉(zhuǎn)換。

使用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/>在默認情況下慧邮,該標簽會創(chuàng)建一個默認的RequestMappingHandlerMappingRequestMappingHandlerAdapter實例,還會注冊一個默認的ConversionService(FormattingConversionServiceFactoryBean)以滿足大部分類型轉(zhuǎn)換的需求舟陆。

我們這里顯示的定義一個conversionService代替默認實現(xiàn),實際開發(fā)中不建議這么做耻矮。

spring 3.1 開始我們應該用RequestMappingHandlerMapping 來替換 DefaultAnnotationHandlerMapping秦躯,用 RequestMappingHandlerAdapter 來替換 AnnotationMethodHandlerAdapter,提供更多的擴展點裆装。

<!-- mvc注解驅(qū)動 裝備自定義的conversionService -->
<mvc:annotation-driven conversion-service="conversionService"/>
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean">
    <property name="converters">
        <set>
             <!-- 裝配自定義的轉(zhuǎn)換器converter -->
            <ref bean="stringtoUserConvert"/>
        </set>
    </property>
</bean>

數(shù)據(jù)格式化

Spring使用轉(zhuǎn)換器完成對象目標類型的轉(zhuǎn)換踱承,轉(zhuǎn)換過程不包含輸入倡缠、輸出信息的格式化。如日期茎活,數(shù)字昙沦,時間,貨幣等數(shù)據(jù)都是具有一定格式的载荔,在不同的本地化環(huán)境中盾饮,同一類型的數(shù)據(jù)顯示不同的數(shù)據(jù)格式。

從格式化數(shù)據(jù)中獲取真正數(shù)據(jù)并完成綁定懒熙,并將處理完成的數(shù)據(jù)輸出為格式化的數(shù)據(jù)丘损,Spring提供了格式化框架org.springframework.format。其中最重要的接口是Formatter<T>接口工扎。

Formatter<T>

Printer<T>負責對象的格式化輸出徘钥,Parser<T>負責對象的格式化輸入

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

在兩個接口中,各有一個接口方法

//將類型為T的成員根據(jù)本地化的不同輸出為不同格式的字符串
@FunctionalInterface
public interface Printer<T> {
    String print(T var1, Locale var2);
}

//參考本地化信息將一個格式化的字符串轉(zhuǎn)換為T類型的對象
@FunctionalInterface
public interface Parser<T> {
    T parse(String var1, Locale var2) throws ParseException;
}

格式化注解驅(qū)動

spring提供了注解驅(qū)動接口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提供了多個內(nèi)建的實現(xiàn)類呈础,通過名字很容易理解每個實現(xiàn)類的作用


AnnotationFormatterFactory實現(xiàn)類

啟用格式化注解驅(qū)動

spring是基于對象轉(zhuǎn)換框架植入格式化功能的,Spring在格式化模塊定義了一個實現(xiàn)ConversionService接口的FormattingConversionService橱健,它既繼承了GenericConversionService而钞,又實現(xiàn)了FormatterRegistry(實現(xiàn)FormatterRegistry是實現(xiàn)Formatter的一種方式)。

public class FormattingConversionService extends GenericConversionService
        implements FormatterRegistry, EmbeddedValueResolverAware {}

ConversionServiceFactoryBean一樣畴博,FormattingConversionService也有自己的工廠類FormattingConversionServiceFactoryBean笨忌,在上下文中構造這個工廠類,可以注冊自定義轉(zhuǎn)換器俱病,還可以注冊自定義注解驅(qū)動邏輯官疲。

FormattingConversionService在內(nèi)部會自動注冊(這塊源碼沒找到,應該讀取配置文件是通過set方法注入)NumberFormatAnnotationFormatterFactory,JodaDateTimeFormatAnnotationFormatterFactory亮隙,因此途凫,在裝配了FormattingConversionService后,就可以在Spring MVC入?yún)⒔壎澳P洼敵鰰r使用注解驅(qū)動進行格式化溢吻。

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

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

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

數(shù)據(jù)校驗

JRS-303

JRS-303是Java為Bean數(shù)據(jù)合法性校驗提供的標準框架维费,包含在Java EE6.0中,通過在Bean屬性上標注類似@NotNull促王、@Max等注解指定校驗規(guī)則犀盟,并通過標準的驗證接口對Bean進行驗證。

參考:JSR 303 - Bean Validation 介紹及最佳實踐

Constraint 詳細信息
@Null 被注釋的元素必須為 null
@NotNull 被注釋的元素必須不為 null
@AssertTrue 被注釋的元素必須為 true
@AssertFalse 被注釋的元素必須為 false
@Min(value) 被注釋的元素必須是一個數(shù)字蝇狼,其值必須大于等于指定的最小值
@Max(value) 被注釋的元素必須是一個數(shù)字阅畴,其值必須小于等于指定的最大值
@DecimalMin(value) 被注釋的元素必須是一個數(shù)字,其值必須大于等于指定的最小值
@DecimalMax(value) 被注釋的元素必須是一個數(shù)字迅耘,其值必須小于等于指定的最大值
@Size(max, min) 被注釋的元素的大小必須在指定的范圍內(nèi)
@Digits (integer, fraction) 被注釋的元素必須是一個數(shù)字贱枣,其值必須在可接受的范圍內(nèi)
@Past 被注釋的元素必須是一個過去的日期
@Future 被注釋的元素必須是一個將來的日期
@Pattern(value) 被注釋的元素必須符合指定的正則表達式

Hibernate Validator 是 Bean Validation 的參考實現(xiàn) . Hibernate Validator 提供了 JSR 303 規(guī)范中所有內(nèi)置 constraint 的實現(xiàn)监署,除此之外還有一些附加的 constraint。

Constraint 詳細信息
@Email 被注釋的元素必須是電子郵箱地址
@Length 被注釋的字符串的大小必須在指定的范圍內(nèi)
@NotEmpty 被注釋的字符串的必須非空
@Range 被注釋的元素必須在合適的范圍內(nèi)
<!-- 校驗接口 JRS-303 -->
<dependency>
    <groupId>javax.validation</groupId>
    <artifactId>validation-api</artifactId>
    <version>1.1.0.Final</version>
</dependency>
<!-- JRS-303 實現(xiàn) -->
<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>5.4.1.Final</version>
</dependency>

Spring數(shù)據(jù)校驗

Spring擁有自己獨立的數(shù)據(jù)驗證框架纽哥,同時支持JRS-303標準框架钠乏。Spring的DataBinder在進行數(shù)據(jù)綁定時,可同時調(diào)用校驗框架完成數(shù)據(jù)校驗春塌,在Spring MVC中晓避,可直接通過注解驅(qū)動的方式進行數(shù)據(jù)校驗。

Validator

最基本的Spring校驗接口

package org.springframework.validation;

public interface Validator {
    //對clazz類型的對象進行校驗
    boolean supports(Class<?> clazz);
    //對目標類target進行校驗摔笤,并將校驗錯誤記錄在errors中
    void validate(Object target, Errors errors);
}
LocalValidatorFactoryBean

LocalValidatorFactoryBean既實現(xiàn)了spring的Validator接口够滑,又實現(xiàn)了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的實現(xiàn)命辖,必須引入相關實現(xiàn)者的jar包况毅。

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

Spring MVC數(shù)據(jù)校驗

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

    @Min(18)
    private Integer age;
    ...
}
@RequestMapping("/user")
/*
    Spring MVC通過處理方法簽名的規(guī)約來保存校驗結果尔艇,前一個校驗結果保存在其后的入?yún)⒅卸恚仨氁?    BindingResult或Errors類型,之間不允許其他入?yún)?*/
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]]

自定義校驗規(guī)則

可以使用注解@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;
}
/*
產(chǎn)生的對應錯誤信息:
    required.user.name
    required.name
    required.java.lang.String
    required

    reserved.user.name
    reserved.name
    reserved.java.lang.String
    reserved
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末终娃,一起剝皮案震驚了整個濱河市味廊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌棠耕,老刑警劉巖余佛,帶你破解...
    沈念sama閱讀 212,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異窍荧,居然都是意外死亡辉巡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評論 3 385
  • 文/潘曉璐 我一進店門蕊退,熙熙樓的掌柜王于貴愁眉苦臉地迎上來郊楣,“玉大人,你說我怎么就攤上這事瓤荔【辉椋” “怎么了?”我有些...
    開封第一講書人閱讀 158,369評論 0 348
  • 文/不壞的土叔 我叫張陵输硝,是天一觀的道長塞栅。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么放椰? 我笑而不...
    開封第一講書人閱讀 56,799評論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮愉粤,結果婚禮上砾医,老公的妹妹穿的比我還像新娘。我一直安慰自己衣厘,他們只是感情好如蚜,可當我...
    茶點故事閱讀 65,910評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著影暴,像睡著了一般错邦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上型宙,一...
    開封第一講書人閱讀 50,096評論 1 291
  • 那天撬呢,我揣著相機與錄音,去河邊找鬼妆兑。 笑死魂拦,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的搁嗓。 我是一名探鬼主播芯勘,決...
    沈念sama閱讀 39,159評論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼腺逛!你這毒婦竟也來了荷愕?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,917評論 0 268
  • 序言:老撾萬榮一對情侶失蹤棍矛,失蹤者是張志新(化名)和其女友劉穎安疗,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體茄靠,經(jīng)...
    沈念sama閱讀 44,360評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡茂契,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,673評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了慨绳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掉冶。...
    茶點故事閱讀 38,814評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖脐雪,靈堂內(nèi)的尸體忽然破棺而出厌小,到底是詐尸還是另有隱情,我是刑警寧澤战秋,帶...
    沈念sama閱讀 34,509評論 4 334
  • 正文 年R本政府宣布璧亚,位于F島的核電站,受9級特大地震影響脂信,放射性物質(zhì)發(fā)生泄漏癣蟋。R本人自食惡果不足惜透硝,卻給世界環(huán)境...
    茶點故事閱讀 40,156評論 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望疯搅。 院中可真熱鬧濒生,春花似錦、人聲如沸幔欧。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽礁蔗。三九已至觉义,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間浴井,已是汗流浹背晒骇。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留滋饲,地道東北人厉碟。 一個月前我還...
    沈念sama閱讀 46,641評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像屠缭,于是被迫代替她去往敵國和親箍鼓。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,728評論 2 351

推薦閱讀更多精彩內(nèi)容