1. 前言
在以往我们需要SpringMVC为我们自动进行类型转换的时候都是用的PropertyEditor
。通过PropertyEditor
的setAsText()
方法我们可以实现字符串向特定类型的转换。但是这里有一个限制是它只支持从String类型转为其他类型。在Spring3
中引入了一个Converter
接口,它支持从一个Object转为另一个Object。除了Converter
接口之外,实现ConverterFactory
接口和GenericConverter
接口也可以实现我们自己的类型转换逻辑。
2. 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;
}
}
3. 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
,之后每次返回的都是这个GenericConversionServic
对象。当使用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
直接注入到我们的程序中。
4. 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>
5. GenericConverter接口
5.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接口。
5.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
了。
欢迎关注我的公众号:程序员L札记