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