SpringMVC 之類型轉換 Converter
來自: http://blog.csdn.net//chenleixing/article/details/44801803
1.1 目錄
1.1 目錄
1.2 前言
1.3 Converter 接口
1.4 ConversionService 接口
1.5 ConverterFactory 接口
1.6 GenericConverter 接口
1.6.1 概述
1.6.2 ConditionalGenericConverter 接口
1.2 前言
在以往我們需要 SpringMVC 為我們自動進行類型轉換的時候都是用的 PropertyEditor 。通過 PropertyEditor 的 setAsText() 方法我們可以實現字符串向特定類型的轉換。但是這里有一個限制是它只支持從 String 類型轉為其他類型。在 Spring3 中引入了一個 Converter 接口,它支持從一個 Object 轉為另一個 Object 。除了 Converter 接口之外,實現 ConverterFactory 接口和 GenericConverter 接口也可以實現我們自己的類型轉換邏輯。
1.3 Converter 接口
我們先來看一下 Converter 接口的定義:
public interface Converter<S, T> { T convert(S source); }
我們可以看到這個接口是使用了泛型的,第一個類型表示原類型,第二個類型表示目標類型,然后里面定義了一個 convert 方法,將原類型對象作為參數傳入進行轉換之后返回目標類型對象。當我們需要建立自己的 converter 的時候就可以實現該接口。下面假設有這樣一個需求,有一個文章實體,在文章中是可以有附件的,而附件我們需要記錄它的請求地址、大小和文件名,所以這個時候文章應該是包含一個附件列表的。在實現的時候我們的附件是實時上傳的,上傳后由服務端返回對應的附件請求地址、大小和文件名,附件信息不直接存放在數據庫中,而是作為文章的屬性一起存放在 Mongodb 中。客戶端獲取到這些信息以后做一個簡單的展示,然后把它們封裝成特定格式的字符串作為隱藏域跟隨文章一起提交到服務端。在服務端我們就需要把這些字符串附件信息轉換為對應的 List<Attachment> 。所以這個時候我們就建立一個 String[] 到 List<Attachment> 的 Converter 。代碼如下:
import java.util.ArrayList; import java.util.List; import org.springframework.core.convert.converter.Converter; import com.tiantian.blog.model.Attachment; public class StringArrayToAttachmentList implements Converter<String[], List<Attachment>> { @Override public List<Attachment> convert(String[] source) { if (source == null) return null; List<Attachment> attachs = new ArrayList<Attachment>(source.length); Attachment attach = null; for (String attachStr : source) { //這里假設我們的Attachment是以“name,requestUrl,size”的形式拼接的。 String[] attachInfos = attachStr.split(","); if (attachInfos.length != 3)//當按逗號分隔的數組長度不為3時就拋一個異常,說明非法操作了。 throw new RuntimeException(); String name = attachInfos[0]; String requestUrl = attachInfos[1]; int size; try { size = Integer.parseInt(attachInfos[2]); } catch (NumberFormatException e) { throw new RuntimeException();//這里也要拋一個異常。 } attach = new Attachment(name, requestUrl, size); attachs.add(attach); } return attachs; } }
1.4 ConversionService 接口
在定義好 Converter 之后,就是使用 Converter 了。為了統一調用 Converter 進行類型轉換, Spring 為我們提供了一個 ConversionService 接口。通過實現這個接口我們可以實現自己的 Converter 調用邏輯。我們先來看一下 ConversionService 接口的定義:
public interface ConversionService { boolean canConvert(Class<?> sourceType, Class<?> targetType); <T> T convert(Object source, Class<T> targetType); boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType); Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); }
我們可以看到 ConversionService 接口里面定義了兩個 canConvert 方法和兩個 convert 方法, canConvert 方法用于判斷當前的 ConversionService 是否能夠對原類型和目標類型進行轉換, convert 方法則是用于進行類型轉換的。上面出現的參數類型 TypeDescriptor 是對于一種類型的封裝,里面包含該種類型的值、實際類型等等信息。
在定義了 ConversionService 之后我們就可以把它定義為一個 bean 對象,然后指定<mvn:annotation-driven/> 的 conversion-service 屬性為我們自己定義的 ConversionService bean 對象。如:
<mvc:annotation-driven conversion-service="myConversionService"/> <bean id="myConversionService" class="com.tiantian.blog.web.converter.support.MyConversionService"/>
這樣當 SpringMVC 需要進行類型轉換的時候就會調用 ConversionService 的 canConvert 和 convert 方法進行類型轉換。
一般而言我們在實現 ConversionService 接口的時候也會實現 ConverterRegistry接口。使用 ConverterRegistry 可以使我們對類型轉換器做一個統一的注冊。 ConverterRegistry 接口的定義如下:
public interface ConverterRegistry { void addConverter(Converter<?, ?> converter); void addConverter(GenericConverter converter); void addConverterFactory(ConverterFactory<?, ?> converterFactory); void removeConvertible(Class<?> sourceType, Class<?> targetType); }
正如前言所說的,要實現自己的類型轉換邏輯我們可以實現 Converter 接口、 ConverterFactory 接口和 GenericConverter 接口, ConverterRegistry 接口就分別為這三種類型提供了對應的注冊方法,至于里面的邏輯就可以發揮自己的設計能力進行設計實現了。
對于 ConversionService , Spring 已經為我們提供了一個實現,它就是 GenericConversionService ,位于 org.springframework.core.convert.support 包下面,它實現了 ConversionService 接口和 ConverterRegistry 接口。但是不能直接把它作為 SpringMVC 的 ConversionService ,因為直接使用時不能往里面注冊類型轉換器。也就是說不能像下面這樣使用:
<mvc:annotation-driven conversion-service="conversionService"/> <bean id="conversionService" class="org.springframework.core.convert.support.GenericConversionService"/>
為此我們必須對 GenericConversionService 做一些封裝,比如說我們可以在自己的 ConversionService 里面注入一個 GenericConversionService ,然后通過自己的ConversionService 的屬性接收 Converter 并把它們注入到 GenericConversionService中,之后所有關于 ConversionService 的方法邏輯都可以調用 GenericConversionService 對應的邏輯。按照這種思想我們的 ConversionService 大概是這樣的:
package com.tiantian.blog.web.converter.support; import java.util.Set; import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.convert.support.GenericConversionService; public class MyConversionService implements ConversionService { @Autowired private GenericConversionService conversionService; private Set<?> converters; @PostConstruct public void afterPropertiesSet() { if (converters != null) { for (Object converter : converters) { if (converter instanceof Converter<?, ?>) { conversionService.addConverter((Converter<?, ?>)converter); } else if (converter instanceof ConverterFactory<?, ?>) { conversionService.addConverterFactory((ConverterFactory<?, ?>)converter); } else if (converter instanceof GenericConverter) { conversionService.addConverter((GenericConverter)converter); } } } } @Override public boolean canConvert(Class<?> sourceType, Class<?> targetType) { return conversionService.canConvert(sourceType, targetType); } @Override public boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType) { return conversionService.canConvert(sourceType, targetType); } @Override public <T> T convert(Object source, Class<T> targetType) { return conversionService.convert(source, targetType); } @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { return conversionService.convert(source, sourceType, targetType); } public Set<?> getConverters() { return converters; } public void setConverters(Set<?> converters) { this.converters = converters; } }
在上面代碼中,通過 converters 屬性我們可以接收需要注冊的 Converter 、 ConverterFactory 和 GenericConverter ,在 converters 屬性設置完成之后 afterPropertiesSet 方法會被調用,在這個方法里面我們把接收到的 converters 都注冊到注入的 GenericConversionService 中了,之后關于 ConversionService 的其他操作都是通過這個 GenericConversionService 來完成的。這個時候我們的 SpringMVC 文件可以這樣配置:
<mvc:annotation-driven conversion-service="conversionService"/> <bean id="genericConversionService" class="org.springframework.core.convert.support.GenericConversionService"/> <bean id="conversionService" class="com.tiantian.blog.web.converter.support.MyConversionService"> <property name="converters"> <set> <bean class="com.tiantian.blog.web.converter.StringArrayToAttachmentList"/> </set> </property> </bean>
除了以上這種使用 GenericConversionService 的思想之外, Spring 已經為我們提供了一個既可以使用 GenericConversionService ,又可以注入 Converter 的類,那就是 ConversionServiceFactoryBean 。該類為我們提供了一個可以接收 Converter 的 converters 屬性,在它的內部有一個 GenericConversionService 對象的引用,在對象初始化完成之后它會 new 一個 GenericConversionService 對象,并往 GenericConversionService 中注冊 converters 屬性指定的 Converter 和 Spring 自身已經實現了的默認 Converter ,之后每次返回的都是這個 GenericConversionService 對象。當使用 ConversionServiceFactoryBean 的時候我們的 SpringMVC 文件可以這樣配置:
<mvc:annotation-driven conversion-service="conversionService"/> <bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"> <property name="converters"> <list> <bean class="com.tiantian.blog.web.converter.StringArrayToAttachmentList"/> </list> </property> </bean>
除了 ConversionServiceFactoryBean 之外, Spring 還為我們提供了一個 FormattingConversionServiceFactoryBean 。當使用 FormattingConversionServiceFactoryBean 的時候我們的 SpringMVC 配置文件的定義應該是這樣:
<mvc:annotation-driven conversion-service="conversionService"/> <bean id="conversionService" class="org.springframework.format.support.FormattingConversionServiceFactoryBean"> <property name="converters"> <set> <bean class="com.tiantian.blog.web.converter.StringArrayToAttachmentList"/> </set> </property> </bean>
以上介紹的是 SpringMVC 自動進行類型轉換時需要我們做的操作。如果我們需要在程序里面手動的來進行類型轉換的話,我們也可以往我們的程序里面注入一個 ConversionService ,然后通過 ConversionService 來進行相應的類型轉換操作,也可以把Converter直接注入到我們的程序中。
1.5 ConverterFactory 接口
ConverterFactory 的出現可以讓我們統一管理一些相關聯的 Converter 。顧名思義, ConverterFactory 就是產生 Converter 的一個工廠,確實 ConverterFactory 就是用來產生 Converter 的。我們先來看一下 ConverterFactory 接口的定義:
public interface ConverterFactory<S, R> { <T extends R> Converter<S, T> getConverter(Class<T> targetType); }
我們可以看到 ConverterFactory 接口里面就定義了一個產生 Converter 的 getConverter 方法,參數是目標類型的 class 。我們可以看到 ConverterFactory 中一共用到了三個泛型, S 、 R 、 T ,其中 S 表示原類型, R 表示目標類型, T 是類型 R 的一個子類。
考慮這樣一種情況,我們有一個表示用戶狀態的枚舉類型 UserStatus ,如果要定義一個從 String 轉為 UserStatus 的 Converter ,根據之前 Converter 接口的說明,我們的StringToUserStatus 大概是這個樣子:
public class StringToUserStatus implements Converter<String, UserStatus> { @Override public UserStatus convert(String source) { if (source == null) { return null; } return UserStatus.valueOf(source); } }
如果這個時候有另外一個枚舉類型 UserType ,那么我們就需要定義另外一個從String 轉為 UserType 的 Converter —— StringToUserType ,那么我們的 StringToUserType 大概是這個樣子:
public class StringToUserType implements Converter<String, UserType> { @Override public UserType convert(String source) { if (source == null) { return null; } return UserType.valueOf(source); } }
如果還有其他枚舉類型需要定義原類型為 String 的 Converter 的時候,我們還得像上面那樣定義對應的 Converter 。有了 ConverterFactory 之后,這一切都變得非常簡單,因為 UserStatus 、 UserType 等其他枚舉類型同屬于枚舉,所以這個時候我們就可以統一定義一個從 String 到 Enum 的 ConverterFactory ,然后從中獲取對應的 Converter 進行 convert 操作。 Spring 官方已經為我們實現了這么一個 StringToEnumConverterFactory :
@SuppressWarnings("unchecked") final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> { public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) { return new StringToEnum(targetType); } private class StringToEnum<T extends Enum> implements Converter<String, T> { private final Class<T> enumType; public StringToEnum(Class<T> enumType) { this.enumType = enumType; } public T convert(String source) { if (source.length() == 0) { // It's an empty enum identifier: reset the enum value to null. return null; } return (T) Enum.valueOf(this.enumType, source.trim()); } } }
這樣,如果是要進行 String 到 UserStatus 的轉換,我們就可以通過 StringToEnumConverterFactory 實例的 getConverter(UserStatus.class).convert(string) 獲取到對應的 UserStatus ,如果是要轉換為 UserType 的話就是 getConverter(UserType.class).convert(string) 。這樣就非常方便,可以很好的支持擴展。
對于 ConverterFactory 我們也可以把它當做 ConvertionServiceFactoryBean 的 converters 屬性進行注冊,在 ConvertionServiceFactoryBean 內部進行 Converter 注入的時候會根據 converters 屬性具體元素的具體類型進行不同的注冊,對于 FormattingConversionServiceFactoryBean 也是同樣的方式進行注冊。所以如果我們自己定義了一個 StringToEnumConverterFactory ,我們可以這樣來進行注冊:
<bean id="conversionService" class="org.springframework.context.support.ConversionServiceFactoryBean"> <property name="converters"> <list> <bean class="com.tiantian.blog.web.converter.StringArrayToAttachmentList"/> <bean class="com.tiantian.blog.web.converter.StringToEnumConverterFactory"/> </list> </property> </bean>
1.6 GenericConverter 接口
1.6.1 概述
GenericConverter 接口是所有的 Converter 接口中最靈活也是最復雜的一個類型轉換接口。像我們之前介紹的 Converter 接口只支持從一個原類型轉換為一個目標類型; ConverterFactory 接口只支持從一個原類型轉換為一個目標類型對應的子類型;而 GenericConverter 接口支持在多個不同的原類型和目標類型之間進行轉換,這也就是 GenericConverter 接口靈活和復雜的地方。
我們先來看一下 GenericConverter 接口的定義:
public interface GenericConverter { Set<ConvertiblePair> getConvertibleTypes(); Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType); 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; } public Class<?> getSourceType() { return this.sourceType; } public Class<?> getTargetType() { return this.targetType; } } }
我們可以看到 GenericConverter 接口中一共定義了兩個方法, getConvertibleTypes() 和 convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType)。 getConvertibleTypes 方法用于返回這個 GenericConverter 能夠轉換的原類型和目標類型的這么一個組合; convert 方法則是用于進行類型轉換的,我們可以在這個方法里面實現我們自己的轉換邏輯。之所以說 GenericConverter 是最復雜的是因為它的轉換方法 convert 的參數類型 TypeDescriptor 是比較復雜的。 TypeDescriptor 對類型 Type進行了一些封裝,包括 value 、 Field 及其對應的真實類型等等,具體的可以查看 API。
關于 GenericConverter 的使用,這里也舉一個例子。假設我們有一項需求是希望能通過 user 的 id 或者 username 直接轉換為對應的 user 對象,那么我們就可以針對于 id 和 username 來建立一個 GenericConverter 。這里假設 id 是 int 型,而 username是 String 型的,所以我們的 GenericConverter 可以這樣來寫:
public class UserGenericConverter implements GenericConverter { @Autowired private UserService userService; @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null || sourceType == TypeDescriptor.NULL || targetType == TypeDescriptor.NULL) { return null; } User user = null; if (sourceType.getType() == Integer.class) { user = userService.findById((Integer) source);//根據id來查找user } else if (sourceType.getType() == String.class) { user = userService.find((String)source);//根據用戶名來查找user } return user; } @Override public Set<ConvertiblePair> getConvertibleTypes() { Set<ConvertiblePair> pairs = new HashSet<ConvertiblePair>(); pairs.add(new ConvertiblePair(Integer.class, User.class)); pairs.add(new ConvertiblePair(String.class, User.class)); return pairs; } }
我們可以看到在上面定義的 UserGenericConverter 中,我們在 getConvertibleTypes 方法中添加了兩組轉換的組合, Integer 到 User 和 String 到 User 。然后我們給 UserGenericConverter 注入了一個 UserService ,在 convert 方法
中我們簡單的根據原類型是 Integer 還是 String 來判斷傳遞的原數據是 id 還是 username ,并利用 UserService 對應的方法返回相應的 User 對象。
GenericConverter 接口實現類的注冊方法跟 Converter 接口和 ConverterFactory接口實現類的注冊方法是一樣的,這里就不再贅述了。
雖然 Converter 接口、 ConverterFactory 接口和 GenericConverter 接口之間沒有任何的關系,但是 Spring 內部在注冊 Converter 實現類和 ConverterFactory 實現類時是先把它們轉換為 GenericConverter ,之后再統一對 GenericConverter 進行注冊的。也就是說 Spring 內部會把 Converter 和 ConverterFactory 全部轉換為 GenericConverter 進行注冊,在 Spring 注冊的容器中只存在 GenericConverter 這一種類型轉換器。我想之所以給用戶開放 Converter 接口和 ConverterFactory 接口是為了讓我們能夠更方便的實現自己的類型轉換器。基于此, Spring 官方也提倡我們在進行一些簡單類型轉換器定義時更多的使用 Converter 接口和 ConverterFactory 接口,在非必要的情況下少使用 GenericConverter 接口。
1.6.2 ConditionalGenericConverter 接口
對于 GenericConverter 接口 Spring 還為我們提供了一個它的子接口,叫做 ConditionalGenericConverter ,在這個接口中只定義了一個方法: matches 方法。我們一起來看一下 ConditionalGenericConverter 接口的定義:
public interface ConditionalGenericConverter extends GenericConverter { boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType); }
顧名思義,從 Conditional 我們就可以看出來這個接口是用于定義有條件的類型轉換器的,也就是說不是簡單的滿足類型匹配就可以使用該類型轉換器進行類型轉換了,必須要滿足某種條件才能使用該類型轉換器。而該類型轉換器的條件控制就是通過 ConditionalGenericConverter 接口的 matches 方法來實現的。關于 ConditionalGenericConverter 的使用 Spring 內部已經實現了很多,這里我們來看一個 Spring 已經實現了的將 String 以逗號分割轉換為目標類型數組的實現:
final class StringToArrayConverter implements ConditionalGenericConverter { private final ConversionService conversionService; public StringToArrayConverter(ConversionService conversionService) { this.conversionService = conversionService; } public Set<ConvertiblePair> getConvertibleTypes() { return Collections.singleton(new ConvertiblePair(String.class, Object[].class)); } public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { return this.conversionService.canConvert(sourceType, targetType.getElementTypeDescriptor()); } public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } String string = (String) source; String[] fields = StringUtils.commaDelimitedListToStringArray(string); Object target = Array.newInstance(targetType.getElementType(), fields.length); for (int i = 0; i < fields.length; i++) { Object sourceElement = fields[i]; Object targetElement = this.conversionService.convert(sourceElement, sourceType, targetType.getElementTypeDescriptor()); Array.set(target, i, targetElement); } return target; } }
我們可以看到這個 StringToArrayConverter 就是實現了 ConditionalGenericConverter 接口的。根據里面的 matches 方法的邏輯我們知道當我們要把一個字符串轉換為一個數組的時候,只有我們已經定義了一個字符串到這個目標數組元素對應類型的類型轉換器時才可以使用 StringToArrayConverter 進行類型轉換。也就是說假如我們已經定義了一個 String 到 User 的類型轉換器,那么當我們需要將 String 轉換為對應的 User 數組的時候,我們就可以直接使用 Spring 為我們提供的 StringToArrayConverter 了。