天天看點

SpringMVC 之類型轉換 Converter

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 了。

以上是本人摘自推酷網:http://www.tuicool.com/articles/uUjaum