上篇:Spring MVC 數(shù)據(jù)綁定(一)
Spring MVC通過反射機制對目標處理方法的簽名進行分析逞姿,將請求信息綁定到處理方法的入?yún)⒅兄P恕T趨?shù)解析之后坑律,緊跟著進行參數(shù)的類型轉(zhuǎn)換匾南、格式化、校驗等馏锡。這里主要講解參數(shù)解析之后的過程雷蹂。
數(shù)據(jù)綁定的核心部件是DataBinder,其運行機制如圖
流程大概分為幾個步驟:
Spring MVC主框架將ServeltRequest對象及處理方法的入?yún)ο髮嵗齻鬟f給 DataBinder
DataBinder首先調(diào)用裝配在Spring Web上下文中的ConversionService組件進行數(shù)據(jù)類型轉(zhuǎn)換杯道、數(shù)據(jù)格式化工作匪煌,將ServletRequest對象填充到入?yún)ο笾校?/p>
然后調(diào)用Validator組件對已經(jīng)綁定了請求消息數(shù)據(jù)的入?yún)ο筮M行數(shù)據(jù)合法性校驗
最終生成數(shù)據(jù)綁定結果BindingResult對象。BindingResult對象包含了已完成數(shù)據(jù)綁定的入?yún)ο螅€包含相應的校驗錯誤對象萎庭。
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)建一個默認的RequestMappingHandlerMapping
和 RequestMappingHandlerAdapter
實例,還會注冊一個默認的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)類的作用
啟用格式化注解驅(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