Skip to content

Spring 类型转换 (Spring Type Conversion)

core.convert 包提供了一个通用的类型转换系统。该系统定义了用于实现类型转换逻辑的 SPI(服务提供者接口)以及用于在运行时执行类型转换的 API。在 Spring 容器中,你可以使用此系统作为 PropertyEditor 实现的替代方案,将外部化的 Bean 属性值字符串转换为所需的属性类型。你还可以在应用程序中任何需要类型转换的地方使用该公共 API。

Converter SPI

用于实现类型转换逻辑的 SPI 是简单且强类型的,如下面的接口定义所示:

java
package org.springframework.core.convert.converter;

public interface Converter<S, T> {

	T convert(S source);
}

要创建你自己的转换器,请实现 Converter 接口,并将 S 参数化为你转换自的类型,将 T 参数化为你转换到的类型。如果需要将 S 的集合或数组转换为 T 的数组或集合,只要已经注册了委托数组或集合转换器(DefaultConversionService 默认会这样做),你也可以透明地应用此类转换器。

对于每次对 convert(S) 的调用,保证源参数(source)不为 null。如果转换失败,你的 Converter 可以抛出任何运行时异常。具体而言,它应该抛出 IllegalArgumentException 来报告无效的源值。请务必确保你的 Converter 实现是线程安全的。

core.convert.support 包中提供了几种转换器实现作为便利,包括从字符串到数字和其他常见类型的转换。以下列表展示了 StringToInteger 类,这是一个典型的 Converter 实现:

java
package org.springframework.core.convert.support;

final class StringToInteger implements Converter<String, Integer> {

	public Integer convert(String source) {
		return Integer.valueOf(source);
	}
}

使用 ConverterFactory

当你需要集中处理整个类层次结构的转换逻辑时(例如,从 String 转换为 Enum 对象时),可以实现 ConverterFactory,如下例所示:

java
package org.springframework.core.convert.converter;

public interface ConverterFactory<S, R> {

	<T extends R> Converter<S, T> getConverter(Class<T> targetType);
}

S 参数化为你转换自的类型,将 R 参数化为定义你可以转换到的类 范围 的基础类型。然后实现 getConverter(Class<T>),其中 TR 的子类。

StringToEnumConverterFactory 为例:

java
package org.springframework.core.convert.support;

final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {

	public <T extends Enum> Converter<String, T> getConverter(Class<T> targetType) {
		return new StringToEnumConverter(targetType);
	}

	private final class StringToEnumConverter<T extends Enum> implements Converter<String, T> {

		private Class<T> enumType;

		public StringToEnumConverter(Class<T> enumType) {
			this.enumType = enumType;
		}

		public T convert(String source) {
			return (T) Enum.valueOf(this.enumType, source.trim());
		}
	}
}

使用 GenericConverter

当你需要更复杂的 Converter 实现时,请考虑使用 GenericConverter 接口。与 Converter 相比,GenericConverter 具有更灵活但非强类型的签名,支持在多个源类型和目标类型之间进行转换。此外,GenericConverter 还提供了源和目标类型描述符(Type Descriptors),你可以在实现转换逻辑时使用它们。这些类型描述符允许类型转换由描述符来源(如字段或方法)上的注解或由字段签名、方法签名等中声明的泛型信息驱动。以下列表显示了 GenericConverter 接口的定义:

java
package org.springframework.core.convert.converter;

public interface GenericConverter {

	public Set<ConvertiblePair> getConvertibleTypes();

	Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}

要实现 GenericConverter,让 getConvertibleTypes() 返回支持的“源 → 目标”类型对。然后实现 convert(Object, TypeDescriptor, TypeDescriptor) 来包含你的转换逻辑。源 TypeDescriptor 提供了对持有被转换值的源字段或方法的访问。目标 TypeDescriptor 提供了对要设置转换值的目标字段或方法的访问。

GenericConverter 的一个很好例子是在 Java 数组和集合之间转换的转换器。这种 ArrayToCollectionConverter 会自省声明目标集合类型的字段或方法,以解析集合的元素类型。这使得源数组中的每个元素在集合设置到目标字段或提供给目标方法或构造函数之前,都能转换为集合元素类型。

注意

由于 GenericConverter 是一个更复杂的 SPI 接口,因此你应该仅在需要时使用它。对于基本的类型转换需求,请优先使用 ConverterConverterFactory

使用 ConditionalGenericConverter

有时,你希望仅在特定条件成立时才运行 Converter。例如,你可能希望仅当目标字段或方法上存在特定注解时才运行 Converter,或者你可能希望仅当目标类型上定义了特定方法(如 static valueOf 方法)时才运行 ConverterConditionalGenericConverterGenericConverterConditionalConverter 接口的并集,允许你定义此类自定义匹配标准:

java
public interface ConditionalConverter {

	boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

ConditionalGenericConverter 的一个很好例子是 IdToEntityConverter,它在持久化实体标识符(ID)和实体引用(Entity)之间进行转换。如果目标实体类型声明了静态查找方法(例如 findAccount(Long)),则此类 IdToEntityConverter 可能仅匹配。你可以在 matches(TypeDescriptor, TypeDescriptor) 的实现中执行此类查找方法检查。

ConversionService API

ConversionService 定义了一个统一的 API,用于在运行时执行类型转换逻辑。转换器通常在这个门面(Facade)接口之后运行:

java
package org.springframework.core.convert;

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 实现还实现了 ConverterRegistry,它提供了一个用于注册转换器的 SPI。在内部,ConversionService 实现委托其注册的转换器来执行类型转换逻辑。

core.convert.support 包中提供了一个强大的 ConversionService 实现。GenericConversionService 是适用于大多数环境的通用实现。ConversionServiceFactory 提供了一个方便的工厂,用于创建常见的 ConversionService 配置。

配置 ConversionService

ConversionService 是一个无状态对象,旨在应用启动时实例化,然后在多个线程之间共享。在 Spring 应用程序中,你通常为每个 Spring 容器(或 ApplicationContext)配置一个 ConversionService 实例。Spring 会识别该 ConversionService,并在框架需要执行类型转换时使用它。你也可以将此 ConversionService 注入到你的任何 Bean 中并直接调用它。

注意

如果没有在 Spring 中注册 ConversionService,则使用原始的基于 PropertyEditor 的系统。

要在 Spring 中注册默认的 ConversionService,请添加以下 idconversionService 的 Bean 定义:

xml
<bean id="conversionService"
	class="org.springframework.context.support.ConversionServiceFactoryBean"/>

默认的 ConversionService 可以在字符串、数字、枚举、集合、Map 和其他常见类型之间进行转换。要使用你自己的自定义转换器补充或覆盖默认转换器,请设置 converters 属性。属性值可以实现 ConverterConverterFactoryGenericConverter 接口中的任何一个。

xml
<bean id="conversionService"
		class="org.springframework.context.support.ConversionServiceFactoryBean">
	<property name="converters">
		<set>
			<bean class="example.MyCustomConverter"/>
		</set>
	</property>
</bean>

在 Spring MVC 应用程序中使用 ConversionService 也很常见。请参阅 Spring MVC 章节中的转换和格式化

在某些情况下,你可能希望在转换期间应用格式化。有关使用 FormattingConversionServiceFactoryBean 的详细信息,请参阅FormatterRegistry SPI

编程式使用 ConversionService

要以编程式使用 ConversionService 实例,你可以像对待任何其他 Bean 一样注入对它的引用。以下示例展示了如何操作:

java
@Service
public class MyService {

	private final ConversionService conversionService;

	public MyService(ConversionService conversionService) {
		this.conversionService = conversionService;
	}

	public void doIt() {
		// 编程式调用转换
		Integer result = this.conversionService.convert("123", Integer.class);
	}
}
kotlin
@Service
class MyService(private val conversionService: ConversionService) {

	fun doIt() {
		// 编程式调用转换
		val result = conversionService.convert("123", Integer::class.java)
	}
}

对于大多数用例,你可以使用指定 targetTypeconvert 方法,但它不适用于更复杂的类型,例如泛型元素的集合。例如,如果你想编程式地将 List<Integer> 转换为 List<String>,你需要提供源类型和目标类型的正式定义。

幸运的是,TypeDescriptor 提供了各种选项来使操作变得简单,如下例所示:

java
DefaultConversionService cs = new DefaultConversionService();

List<Integer> input = Arrays.asList(1, 2, 3);
List<String> result = (List<String>) cs.convert(input,
	TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(Integer.class)), // (1)
	TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class))); // (2)
kotlin
val cs = DefaultConversionService()

val input = listOf(1, 2, 3)
val result = cs.convert(input,
	TypeDescriptor.collection(List::class.java, TypeDescriptor.valueOf(Int::class.javaObjectType)), // (1)
	TypeDescriptor.collection(List::class.java, TypeDescriptor.valueOf(String::class.java))) as List<String> // (2)
  1. List<Integer> 的类型描述符
  2. List<String> 的类型描述符

请注意,DefaultConversionService 会自动注册适用于大多数环境的转换器。这包括集合转换器、标量转换器和基本的 ObjectString 转换器。通过使用 DefaultConversionService 类上的静态 addDefaultConverters 方法,你可以将相同的转换器注册到任何 ConverterRegistry

值类型的转换器会被数组和集合重用,因此无需创建特定的转换器来将 S 的集合转换为 T 的集合(假设标准集合处理是合适的)。


补充教学

1. 为什么推荐使用 ConversionService 而非 PropertyEditor?

虽然两者都负责类型转换,但 ConversionService 有显著优势:

  • 线程安全ConversionService 及其转换器(Converter 等)通常是线程安全的,可以作为单例共享。而 PropertyEditor 是有状态的(持有 value),不是线程安全的。
  • 任意类型转换PropertyEditor 主要为 String <-> Object 设计,而 ConversionService 支持 Object <-> Object(例如 LocalDatejava.util.Date)。
  • API 统一ConversionService 提供了一个干净的门面 API,隐藏了底层复杂的匹配和查找逻辑。

2. ConverterFactory 的妙用:Enum 转换

在处理枚举时,ConverterFactory 非常强大。你不需要为每个枚举类(如 StatusEnum, TypeEnum)写一个转换器,只需要写一个 StringToEnumConverterFactory 即可处理所有的枚举。Spring 内部就是这样处理表单提交中的枚举参数的。

3. TypeDescriptor:解决泛型擦除

由于 Java 的泛型擦除,你在运行时无法直接通过 List.class 知道它包含的是 Integer 还是 StringTypeDescriptor 是 Spring 解决这个问题的利器。它能捕捉复杂的泛型信息,不仅能处理 List<String>,甚至能处理 Map<String, List<Integer>> 这样的深度嵌套。

4. Spring Boot 中的注册方式

在 Spring Boot 中,自定义转换器非常简单:

  • 手动注册:只需将你的 Converter 实现类声明为一个 @Component,Spring Boot 会自动探测到它并将其注册到全局的 ConversionService 中。
  • Web 层专用:如果你希望转换器仅在 Web 层起作用,可以实现 WebMvcConfigureraddFormatters 方法。

5. 性能提示

GenericConversionService 内部维护了一个转换器缓存。当你频繁执行相同的类型转换(如大量的 StringInteger)时,系统会跳过查找过程直接从缓存中获取对应的 Converter 实例,这使得其在高性能 Web 场景下表现出色。

Based on Spring Framework.