Spring Framework 7.0.9 深度剖析-12-类型转换与格式化体系

0 阅读39分钟

Spring Framework 7.0.9 深度剖析-12-类型转换与格式化体系

12.1 本章导读

Spring 容器里发生的一切"赋值",本质上都在跟类型打交道。XML 配置文件里写的 value="42" 是个字符串,而 bean 的 age 属性是 int;@Value("${timeout}") 注入进来的是字符串,而字段声明是 Duration;表单提交上来的请求参数全是 String,而命令对象的字段是 BigDecimal、LocalDate、List<Long>。把这些"声明出来的字符串"变成"运行期真正需要的对象",就是类型转换体系的职责。

本章围绕 Spring Framework 7.0.9 源码,完整剖析这套体系。它由两代技术叠加而成:

  1. JavaBeans PropertyEditor 体系(JDK 原生,Spring 1.x 起就有):以 java.beans.PropertyEditor 为核心,Spring 在 spring-beans 模块里封装为 PropertyEditorRegistry、PropertyEditorRegistrySupport、TypeConverter、TypeConverterSupport、TypeConverterDelegate。这是一套按 bean 实例持有、非线程安全的体系。
  2. ConversionService 体系(Spring 3.0 引入):以 org.springframework.core.convert.ConversionService 为核心,配套 Converter、ConverterFactory、GenericConverter、ConditionalConverter、TypeDescriptor 等 SPI,实现在 spring-core 模块。这是一套可线程安全共享、可注入容器的体系。
  3. 格式化体系(Spring 3.0 引入,位于 spring-context):Formatter<T>、Printer<T>、Parser<T>、AnnotationFormatterFactory<A>、FormattingConversionService、DefaultFormattingConversionService,以及 @NumberFormat、@DateTimeFormat 两个注解。它本质上是 ConversionService 体系在"字段级格式化"场景下的扩展——把 Formatter 桥接成 GenericConverter 注册进 GenericConversionService。

这三套东西不是三选一的关系,而是长期并存、互相桥接、由同一个执行者统一调度。在 7.0.9 中,真正的执行入口仍然是 TypeConverterDelegate.convertIfNecessary(...):它先问自定义 PropertyEditor,再问 ConversionService,再退回默认 PropertyEditor,最后走一条硬编码的标准转换规则链。读懂这一个方法,就读懂了 Spring 数据绑定的半壁江山;再读懂 GenericConversionService 的查找算法与缓存结构,就读懂了另一半。

本章涉及的模块与路径:

  • spring-beans/src/main/java/org/springframework/beans/:TypeConverter.java、TypeConverterSupport.java、TypeConverterDelegate.java、PropertyEditorRegistry.java、PropertyEditorRegistrySupport.java。
  • spring-core/src/main/java/org/springframework/core/convert/:ConversionService.java、TypeDescriptor.java、ConversionFailedException.java、ConverterNotFoundException.java。
  • spring-core/src/main/java/org/springframework/core/convert/converter/:Converter.java、ConverterFactory.java、ConverterRegistry.java、GenericConverter.java(含嵌套类 ConvertiblePair)、ConditionalConverter.java、ConditionalGenericConverter.java。
  • spring-core/src/main/java/org/springframework/core/convert/support/:GenericConversionService.java、DefaultConversionService.java、ConfigurableConversionService.java、ConversionServiceFactory.java、ConversionUtils.java、ConvertingPropertyEditorAdapter.java,以及四十余个内置转换器。
  • spring-context/src/main/java/org/springframework/format/:Formatter.java、Printer.java、Parser.java、FormatterRegistry.java、AnnotationFormatterFactory.java、FormatterRegistrar.java,以及 annotation/、number/、datetime/ 子包。
  • spring-context/src/main/java/org/springframework/format/support/:FormattingConversionService.java、DefaultFormattingConversionService.java、FormattingConversionServiceFactoryBean.java。
  • spring-context/src/main/java/org/springframework/validation/DataBinder.java、spring-context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java。

前置知识:Java 反射基础(java.beans.PropertyEditor、泛型擦除、MethodParameter)、Spring bean 生命周期中属性填充(populateBean)的位置(本章只讲与转换相关的切面)。

读完本章你将获得:① 一次 convertIfNecessary 从入口到结果的完整调用栈与每一步的判定条件;② GenericConversionService 的注册表、缓存结构与类型层次查找算法;③ 7.0.9 默认注册的全部转换器清单及各自实现要点(以本仓库实际代码统计为准);④ @NumberFormat/@DateTimeFormat 如何通过 AnnotationFormatterFactory 变成条件转换器;⑤ ConversionService 在容器中的装配路径与 DataBinder 的整合方式;⑥ 常见坑的源码级解释。

两个版本事实先立在这里(后文会反复回到):其一,7.0.9 的 GenericConversionService 已经没有旧资料里常讲的 classCache/cachedConverters 字段,类型层次列表在缓存未命中时是每次重新计算的;其二,旧资料里常引用的 org.springframework.context.support.ApplicationConversionService 在 7.0.9 源码树中已经不存在(它后来被挪出了框架核心、归 Spring Boot 所有),框架层默认配置入口就是 DefaultConversionService 与 DefaultFormattingConversionService 两个静态方法。

12.2 核心概念:两套体系的抽象与术语

12.2.1 PropertyEditor:JDK 的"字符串 ↔ 对象"契约

java.beans.PropertyEditor 是 JDK 自带的属性编辑接口,核心方法只有两个:void setAsText(String text) 把字符串解析成对象(结果存于内部,用 getValue() 取出),String getAsText() 把对象格式化成字符串。它的设计目标是 GUI 属性编辑器,但 Spring 从 1.x 开始把它借过来做数据绑定。

这个体系在 Spring 里的抽象位于 spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistry.java:

public interface PropertyEditorRegistry {
	void registerCustomEditor(Class<?> requiredType, PropertyEditor propertyEditor);
	void registerCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath,
			PropertyEditor propertyEditor);
	@Nullable PropertyEditor findCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath);
}
  • registerCustomEditor:注册自定义编辑器。两个重载——按类型注册,或按"类型 + 属性路径"注册(路径粒度更细,例如只为 user.address.zipcode 这个属性换一个编辑器)。
  • findCustomEditor:反查。给定期望类型与属性路径,返回匹配的编辑器。

注意它的设计特征:编辑器实例是有状态的、不可共享的(每次 setAsText 后内部保存当前值),因此 Spring 的 TypeConverter 接口注释里明确写着 TypeConverter 本身不是线程安全的——这是它必须被 BeanWrapperImpl 这类"每 bean 一个"的对象持有的根本原因。

12.2.2 TypeConverter:Spring 的转换语义接口

spring-beans/src/main/java/org/springframework/beans/TypeConverter.java 是转换语义的抽象,与 PropertyEditorRegistry 配套使用(但不强制):

public interface TypeConverter {
	<T> @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType) throws TypeMismatchException;
	<T> @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
			@Nullable MethodParameter methodParam) throws TypeMismatchException;
	<T> @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
			@Nullable Field field) throws TypeMismatchException;
	default <T> @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
			@Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {
		throw new UnsupportedOperationException("TypeDescriptor resolution not supported");
	}
}

四个重载的差异只在类型上下文的丰富程度:只给 Class → 丢失泛型;给 MethodParameter/Field → 可以解析出 List<User> 里的 User;直接给 TypeDescriptor → 连字段上的注解(如 @DateTimeFormat)都能拿到。方法名 convertIfNecessary("如有必要则转换")是灵魂:如果 value 本来就是 requiredType 的实例,就什么都不做直接返回。

12.2.3 ConversionService:可共享的转换服务

spring-core/src/main/java/org/springframework/core/convert/ConversionService.java:

public interface ConversionService {
	boolean canConvert(@Nullable Class<?> sourceType, Class<?> targetType);
	boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
	<T> @Nullable T convert(@Nullable Object source, Class<T> targetType);
	default @Nullable Object convert(@Nullable Object source, TypeDescriptor targetType) {
		return convert(source, TypeDescriptor.forObject(source), targetType);
	}
	@Nullable Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType);
}

与 TypeConverter 的本质区别:① 入参出参都是无状态的纯值,没有"当前对象上下文",实现天然可做成线程安全、全容器共享;② 类型上下文用 TypeDescriptor 显式传递,而不是靠反射现场解析。接口注释里有一条重要提醒(canConvert 的 javadoc):对集合/数组/Map 之间的转换,canConvert 返回 true 不代表 convert 一定成功——元素层面的可转换性是运行期才检查的,调用方要自己处理 ConversionException。

12.2.4 Converter SPI 四件套与 TypeDescriptor

spring-core/.../convert/converter/ 包下是注册侧 SPI:

  • Converter<S,T>:函数式接口,单方向、单类型对,T convert(S source)。注释明确:实现必须线程安全、可共享。
  • ConverterFactory<S,R>:"区间工厂"——getConverter(Class<T> targetType) 为同一个源类型 S 派生出指向 R 的任意子类型 T 的转换器,典型如 NumberToNumberConverterFactory(Number→Byte/Integer/Double…)。
  • GenericConverter:最灵活也最复杂的 SPI,Set<ConvertiblePair> getConvertibleTypes() 声明自己能转哪些类型对,Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) 在转换时能拿到字段级上下文(泛型、注解)。
  • ConditionalConverter:boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType)——运行期根据字段上下文(比如目标字段是否带 @DateTimeFormat)决定自己是否本次生效。ConditionalGenericConverter 只是它与 GenericConverter 的接口组合。
  • ConverterRegistry:注册 API(addConverter 三个重载 + addConverterFactory + removeConvertible)。
  • ConfigurableConversionService:ConversionService(读)与 ConverterRegistry(写)的接口组合。

TypeDescriptor(spring-core/.../convert/TypeDescriptor.java)是这套体系的"类型上下文载体":它包装一个 ResolvableType(携带泛型信息)+ 字段注解集合,提供 getElementTypeDescriptor()(集合元素类型)、getMapKeyTypeDescriptor()/getMapValueTypeDescriptor()、hasAnnotation(Class)/getAnnotation(Class)、isAssignableTo(TypeDescriptor) 等方法。没有它,"把 List<String> 转成 List<Long>"这种转换就不可能知道元素该往哪转。

12.2.5 Formatter:面向"字段显示"的格式化抽象

spring-context/.../format/Formatter.java:

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

一个空接口,继承 Printer<T>(String print(T field, Locale locale),对象→文本)与 Parser<T>(T parse(String text, Locale locale),文本→对象)。它和 Converter 的分工:Converter 回答"这个对象能不能变成那个对象",Formatter 回答"这个字段在特定 Locale 下该长什么样、该怎么被读回来"。AnnotationFormatterFactory<A extends Annotation> 则把"注解 → Formatter"的映射抽象成工厂:Set<Class<?>> getFieldTypes() 声明 A 能标注在哪些字段类型上,getPrinter(A annotation, Class<?> fieldType)/getParser(A annotation, Class<?> fieldType) 根据注解属性现造格式化器。

12.2.6 核心概念关系图

PropertyEditor 体系(spring-beans,每 bean 持有,非线程安全)
  PropertyEditorRegistry ──┐
  PropertyEditorRegistrySupport ── 默认编辑器表 + 自定义编辑器表
  TypeConverterSupport ── extends PropertyEditorRegistrySupport implements TypeConverter
  TypeConverterDelegate ── 真正的执行引擎:Editor/ConversionService 双通道

ConversionService 体系(spring-core,线程安全,全容器共享)
  ConversionService(canConvert/convert)┐
  ConverterRegistry(addConverter*3/addConverterFactory/removeConvertible)├─ ConfigurableConversionService
  GenericConversionService ──┬─ converters: ConvertiblePair→ConvertersForPair + globalConverters
                            ├─ converterCache: ConverterCacheKey→GenericConverter
                            └─ DefaultConversionService ── 注册全部内置转换器

格式化体系(spring-context)
  FormattingConversionService extends GenericConversionService implements FormatterRegistry
      Formatter/Printer/Parser ──(桥接)──> PrinterConverter/ParserConverter (GenericConverter)
      AnnotationFormatterFactory ──(桥接)──> AnnotationPrinterConverter/AnnotationParserConverter (ConditionalGenericConverter)
  DefaultFormattingConversionService ── addDefaultConverters + addDefaultFormatters

后续章节按"先执行引擎(TypeConverterDelegate),再服务内核(GenericConversionService),再注册表(Default*),再格式化层,再容器集成"的顺序展开。

12.3 源码剖析(一):PropertyEditor 注册表与默认编辑器

12.3.1 PropertyEditorRegistrySupport 的五个容器

spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java 是整个 PropertyEditor 体系的存储核心。字段区(第 95-112 行):

private @Nullable ConversionService conversionService;

private boolean defaultEditorsActive = false;

private boolean configValueEditorsActive = false;

private @Nullable PropertyEditorRegistrar defaultEditorRegistrar;

@SuppressWarnings("NullAway.Init")
private Map<Class<?>, PropertyEditor> defaultEditors;

private @Nullable Map<Class<?>, PropertyEditor> overriddenDefaultEditors;

private @Nullable Map<Class<?>, PropertyEditor> customEditors;

private @Nullable Map<String, CustomEditorHolder> customEditorsForPath;

private @Nullable Map<Class<?>, PropertyEditor> customEditorCache;

逐容器说明:

  • conversionService:本注册表关联的 ConversionService。注意它就躺在 PropertyEditor 体系的类里——这是两代体系握手的地方,TypeConverterDelegate 正是通过 getConversionService() 拿到它的(见 12.4)。setConversionService 是个普通 setter,谁装配它后面会讲(AbstractBeanFactory.getTypeConverter() 与 BeanWrapperImpl)。
  • defaultEditorsActive/configValueEditorsActive:两个开关。默认编辑器是惰性注册的——registerDefaultEditors() 只把开关打开,真正建表推迟到第一次 getDefaultEditor。configValueEditorsActive 控制是否注册 StringArrayPropertyEditor 这类"配置专用"编辑器(注释明确说它们不适合数据绑定场景)。
  • defaultEditors:类型→默认编辑器的主表,createDefaultEditors() 时一次性建好。
  • overriddenDefaultEditors:6.2.3 引入的"覆盖默认编辑器"表。overrideDefaultEditor 的 javadoc 点破了一条关键优先级规则:覆盖编辑器在语义上仍是默认编辑器,ConversionService 会盖过它;而 custom editors 通常会盖过 ConversionService。这条规则直接决定了 12.4 里 TypeConverterDelegate 先查 custom editor、再问 ConversionService 的顺序。
  • customEditors:类型→自定义编辑器(用户显式 registerCustomEditor 的)。
  • customEditorsForPath:属性路径→编辑器持有者(CustomEditorHolder),如 user.addresses[0].city。
  • customEditorCache:按超类/接口匹配命中的结果缓存,避免每次查找都遍历 customEditors 做可赋值性判断。

12.3.2 createDefaultEditors:默认编辑器全清单(7.0.9 实测)

createDefaultEditors()(第 212-281 行)一次性把默认编辑器铺进 defaultEditors 表。按代码原文分四组:

第一组(JDK 没有默认编辑器的简单类型,第 217-234 行):

this.defaultEditors.put(Charset.class, new CharsetEditor());
this.defaultEditors.put(Class.class, new ClassEditor());
this.defaultEditors.put(Class[].class, new ClassArrayEditor());
this.defaultEditors.put(Currency.class, new CurrencyEditor());
this.defaultEditors.put(File.class, new FileEditor());
this.defaultEditors.put(InputStream.class, new InputStreamEditor());
this.defaultEditors.put(InputSource.class, new InputSourceEditor());
this.defaultEditors.put(Locale.class, new LocaleEditor());
this.defaultEditors.put(Path.class, new PathEditor());
this.defaultEditors.put(Pattern.class, new PatternEditor());
this.defaultEditors.put(Properties.class, new PropertiesEditor());
this.defaultEditors.put(Reader.class, new ReaderEditor());
this.defaultEditors.put(Resource[].class, new ResourceArrayPropertyEditor());
this.defaultEditors.put(TimeZone.class, new TimeZoneEditor());
this.defaultEditors.put(URI.class, new URIEditor());
this.defaultEditors.put(URL.class, new URLEditor());
this.defaultEditors.put(UUID.class, new UUIDEditor());
this.defaultEditors.put(ZoneId.class, new ZoneIdEditor());

第二组(集合/Map 编辑器默认实例,第 238-242 行):Collection、Set、SortedSet、List 各一个 CustomCollectionEditor,SortedMap 一个 CustomMapEditor。注释说明这些可以被用户注册同类型 custom editor 覆盖。

第三组(基本类型与数字包装,第 245-271 行):byte[]/char[] 两个基本数组编辑器;char/Character 用 CharacterEditor(JDK 没有 char 的默认编辑器);boolean/Boolean 用 Spring 的 CustomBooleanEditor(比 JDK 默认接受更多标志值);随后是 byte/Byte、short/Short、int/Integer、long/Long、float/Float、double/Double 六对全部换成 CustomNumberEditor,外加 BigDecimal、BigInteger。注释解释了动机:JDK 不为 number wrapper 类型提供默认编辑器,Spring 用自己的 CustomNumberEditor 统一接管。

第四组(配置值编辑器,第 274-280 行,仅 configValueEditorsActive 时注册):

if (this.configValueEditorsActive) {
	StringArrayPropertyEditor sae = new StringArrayPropertyEditor();
	this.defaultEditors.put(String[].class, sae);
	this.defaultEditors.put(short[].class, sae);
	this.defaultEditors.put(int[].class, sae);
	this.defaultEditors.put(long[].class, sae);
}

即把逗号分隔字符串拆成数组/数值数组——这是 @Value("${...}") 注入 String[]、int[] 时能工作的原因之一。

12.3.3 findCustomEditor:路径优先、类型兜底的两级查找

findCustomEditor(requiredType, propertyPath)(第 325-349 行)是 TypeConverterDelegate 转换流程第一站调用的方法:

public @Nullable PropertyEditor findCustomEditor(@Nullable Class<?> requiredType, @Nullable String propertyPath) {
	Class<?> requiredTypeToUse = requiredType;
	if (propertyPath != null) {
		if (this.customEditorsForPath != null) {
			// Check property-specific editor first.
			PropertyEditor editor = getCustomEditor(propertyPath, requiredType);
			if (editor == null) {
				List<String> strippedPaths = new ArrayList<>();
				addStrippedPropertyPaths(strippedPaths, "", propertyPath);
				for (Iterator<String> it = strippedPaths.iterator(); it.hasNext() && editor == null;) {
					String strippedPath = it.next();
					editor = getCustomEditor(strippedPath, requiredType);
				}
			}
			if (editor != null) {
				return editor;
			}
		}
		if (requiredType == null) {
			requiredTypeToUse = getPropertyType(propertyPath);
		}
	}
	// No property-specific editor -> check type-specific editor.
	return getCustomEditor(requiredTypeToUse);
}

逐段解读:

  1. 先精确路径。getCustomEditor(propertyPath, requiredType) 直接查 customEditorsForPath 这张"路径→持有者"表。
  2. 再剥键重试。路径没命中时,用 addStrippedPropertyPaths 递归地把 map[key]、list[index] 这类键逐次剥掉,生成 accounts、accounts[key]、accounts[0]、accounts[0].name 等一系列变体,逐个再查。这样用户注册在 accounts 上的编辑器,对 accounts[0].name 的元素转换也能生效。
  3. 类型兜底。路径粒度都没有命中,才走 getCustomEditor(requiredTypeToUse) 按类型查;若调用方连 requiredType 都没给(集合元素场景常见),就调 getPropertyType(propertyPath) 反推——这个方法在本类返回 null,由 BeanWrapperImpl 覆盖实现去问内省结果。

按类型查的 getCustomEditor(Class)(第 408-439 行)还有一层"超类匹配 + 缓存":

private @Nullable PropertyEditor getCustomEditor(@Nullable Class<?> requiredType) {
	if (requiredType == null || this.customEditors == null) {
		return null;
	}
	// Check directly registered editor for type.
	PropertyEditor editor = this.customEditors.get(requiredType);
	if (editor == null) {
		// Check cached editor for type, registered for superclass or interface.
		if (this.customEditorCache != null) {
			editor = this.customEditorCache.get(requiredType);
		}
		if (editor == null) {
			// Find editor for superclass or interface.
			for (Map.Entry<Class<?>, PropertyEditor> entry : this.customEditors.entrySet()) {
				Class<?> key = entry.getKey();
				if (key.isAssignableFrom(requiredType)) {
					editor = entry.getValue();
					// Cache editor for search type, to avoid the overhead
					// of repeated assignable-from checks.
					if (this.customEditorCache == null) {
						this.customEditorCache = new HashMap<>();
					}
					this.customEditorCache.put(requiredType, editor);
					if (editor != null) {
						break;
					}
				}
			}
		}
	}
	return editor;
}

即:直接注册的类型没命中 → 查 customEditorCache → 遍历 customEditors 找"注册类型是 requiredType 的父类/接口"的编辑器,找到后把结果缓存。例如用户为 Number 注册了一个编辑器,那么给 Integer 属性找编辑器时就能命中它。注意 registerCustomEditor 在按类型注册成功后会执行 this.customEditorCache = null(第 320 行)——新编辑器注册进来,旧的超类匹配缓存整体作废,这是缓存一致性的代价。

12.3.4 CustomEditorHolder:路径编辑器的类型适配

路径注册的编辑器存在 CustomEditorHolder(第 530-568 行)里,它持有编辑器本身 + 注册时声明的类型。getPropertyEditor(requiredType) 的判定逻辑值得一看:

private PropertyEditor getPropertyEditor(@Nullable Class<?> requiredType) {
	// Special case: If no required type specified, which usually only happens for
	// Collection elements, or required type is not assignable to registered type,
	// which usually only happens for generic properties of type Object -
	// then return PropertyEditor if not registered for Collection or array type.
	if (this.registeredType == null ||
			(requiredType != null &&
			(ClassUtils.isAssignable(this.registeredType, requiredType) ||
			ClassUtils.isAssignable(requiredType, this.registeredType))) ||
			(requiredType == null &&
			(!Collection.class.isAssignableFrom(this.registeredType) && !this.registeredType.isArray()))) {
		return this.propertyEditor;
	}
	else {
		return null;
	}
}

三种放行:① 注册时根本没声明类型(路径专用编辑器,直接放行);② 声明类型与查询类型双向可赋值其一(父子关系,放行);③ 查询类型为 null(集合元素)且该编辑器不是为集合/数组类型注册的——此时假设它是"给元素用的",放行。其余情况返回 null,让查找继续。这个"放行规则"解释了一个常见现象:为什么你在路径上注册的编辑器对 List<Foo> 属性生效、但对 List 本身不生效。

12.3.5 PropertyEditor 体系的桥接者:ConvertingPropertyEditorAdapter

两代体系除了"同一个执行者内部双通道"之外,还有反向桥接:把 ConversionService 包装成 PropertyEditor。spring-core/src/main/java/org/springframework/core/convert/support/ConvertingPropertyEditorAdapter.java:

public class ConvertingPropertyEditorAdapter extends PropertyEditorSupport {

	private final ConversionService conversionService;

	private final TypeDescriptor targetDescriptor;

	private final boolean canConvertToString;

	public ConvertingPropertyEditorAdapter(ConversionService conversionService, TypeDescriptor targetDescriptor) {
		Assert.notNull(conversionService, "ConversionService must not be null");
		Assert.notNull(targetDescriptor, "TypeDescriptor must not be null");
		this.conversionService = conversionService;
		this.targetDescriptor = targetDescriptor;
		this.canConvertToString = conversionService.canConvert(this.targetDescriptor, TypeDescriptor.valueOf(String.class));
	}

	@Override
	public void setAsText(@Nullable String text) throws IllegalArgumentException {
		setValue(this.conversionService.convert(text, TypeDescriptor.valueOf(String.class), this.targetDescriptor));
	}

	@Override
	public @Nullable String getAsText() {
		if (this.canConvertToString) {
			return (String) this.conversionService.convert(getValue(), this.targetDescriptor, TypeDescriptor.valueOf(String.class));
		}
		else {
			return null;
		}
	}
}

它是一个 PropertyEditorSupport 子类:setAsText 把字符串交给 ConversionService 转成目标类型存入 value;getAsText 反向把当前对象转回字符串。构造时预先问一句 canConvert(targetDescriptor, String.class) 决定 getAsText 是否可用——不可转字符串时返回 null 而不是抛异常,这符合 PropertyEditor 契约里"返回 null 表示不支持"的惯例。有了它,任何只接受 PropertyEditor 的老代码路径(例如某些 PropertyEditorRegistrar、JavaBeans 内省工具)都能复用 ConversionService 里注册的转换器。

12.4 源码剖析(二):TypeConverterDelegate 的双通道执行链

12.4.1 TypeConverterSupport:四个重载如何收拢成一次委托

spring-beans/src/main/java/org/springframework/beans/TypeConverterSupport.java 是 TypeConverter 的基础实现,也是 BeanWrapperImpl 的父类链一环。它继承 PropertyEditorRegistrySupport(注册表能力)并实现 TypeConverter:

public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport implements TypeConverter {

	@Nullable TypeConverterDelegate typeConverterDelegate;

	@Override
	public <T> @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType) throws TypeMismatchException {
		return convertIfNecessary(null, value, requiredType, TypeDescriptor.valueOf(requiredType));
	}

	@Override
	public <T> @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
			@Nullable MethodParameter methodParam) throws TypeMismatchException {

		return convertIfNecessary((methodParam != null ? methodParam.getParameterName() : null), value, requiredType,
				(methodParam != null ? new TypeDescriptor(methodParam) : TypeDescriptor.valueOf(requiredType)));
	}

	@Override
	public <T> @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
			@Nullable Field field) throws TypeMismatchException {

		return convertIfNecessary((field != null ? field.getName() : null), value, requiredType,
				(field != null ? new TypeDescriptor(field) : TypeDescriptor.valueOf(requiredType)));
	}

	@Override
	public <T> @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
			@Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {

		return convertIfNecessary(null, value, requiredType, typeDescriptor);
	}

	private <T> @Nullable T convertIfNecessary(@Nullable String propertyName, @Nullable Object value,
			@Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {

		Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate");
		try {
			return this.typeConverterDelegate.convertIfNecessary(
					propertyName, null, value, requiredType, typeDescriptor);
		}
		catch (ConverterNotFoundException | IllegalStateException ex) {
			throw new ConversionNotSupportedException(value, requiredType, ex);
		}
		catch (ConversionException | IllegalArgumentException ex) {
			throw new TypeMismatchException(value, requiredType, ex);
		}
	}
}

逐段解读:

  1. 上下文收拢。四个 public 重载只是把不同的类型来源统一翻译成"属性名 + TypeDescriptor"二元组:Class → TypeDescriptor.valueOf;MethodParameter → new TypeDescriptor(methodParam)(连带参数名、泛型、注解);Field → new TypeDescriptor(field)。注意 propertyName 也在这里确定:方法参数取参数名、字段取字段名,其余为 null。
  2. 委托。真正干活的是包可见的 TypeConverterDelegate,由子类(BeanWrapperImpl、SimpleTypeConverter)在构造时装配。
  3. 异常翻译。这一层是异常语义的分界:ConverterNotFoundException(ConversionService 里找不到转换器)与 IllegalStateException("no matching editors or conversion strategy found")被翻译成 ConversionNotSupportedException——"系统根本不会这种转换";ConversionException(转换器执行失败)与 IllegalArgumentException(编辑器/构造器失败)被翻译成 TypeMismatchException——"转换尝试了但值不对"。数据绑定层正是靠这两个异常类型区分"配置问题"与"用户输入问题"。

12.4.2 TypeConverterDelegate 的构造与字段

spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java:

class TypeConverterDelegate {

	private static final Log logger = LogFactory.getLog(TypeConverterDelegate.class);

	private final PropertyEditorRegistrySupport propertyEditorRegistry;

	private final @Nullable Object targetObject;

	public TypeConverterDelegate(PropertyEditorRegistrySupport propertyEditorRegistry) {
		this(propertyEditorRegistry, null);
	}

	public TypeConverterDelegate(PropertyEditorRegistrySupport propertyEditorRegistry, @Nullable Object targetObject) {
		this.propertyEditorRegistry = propertyEditorRegistry;
		this.targetObject = targetObject;
	}
  • propertyEditorRegistry:编辑器注册表(就是宿主 BeanWrapperImpl 自己),custom editor、default editor、关联的 ConversionService 都从这里拿。
  • targetObject:正在操作的目标 bean 实例。它只被 attemptToConvertStringToEnum 用于拿 ClassLoader——当目标类型声明为裸 Enum 时,允许把 "com.xxx.Color.RED" 这种全限定名字符串解析成枚举常量。

12.4.3 主方法:convertIfNecessary 的七段判定链

带 TypeDescriptor 的五参版本是全章最重要的方法(第 113-284 行)。按执行顺序切成七段逐段讲。

第 1 段:找 custom editor(第 117-118 行)

// Custom editor for this type?
PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);

第一件事就是查 custom editor——用户显式注册的编辑器优先级最高,这与 overrideDefaultEditor javadoc 里"custom editors usually override the ConversionService"的规则严格一致。

第 2 段:有 editor 就跳过;否则先问 ConversionService(第 120-135 行)

ConversionFailedException conversionAttemptEx = null;

// No custom editor but custom ConversionService specified?
ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {
	TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
	if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
		try {
			return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
		}
		catch (ConversionFailedException ex) {
			// fallback to default conversion logic below
			conversionAttemptEx = ex;
		}
	}

四个条件缺一不可:没有 custom editor、有 ConversionService、新值非 null、目标 TypeDescriptor 非 null。满足后用 TypeDescriptor.forObject(newValue) 从值的实际类型反推源类型描述符,先 canConvert 再 convert。注意 newValue 本身必须非 null——null 走不了 ConversionService 通道,null 的语义在后文(convertNullSource/Optional.empty)单独处理。

失败被 catch 成 conversionAttemptEx 保存而不是直接抛出——这是"双通道互备"的关键设计:ConversionService 失败不代表世界末日,下面还有 PropertyEditor 与标准规则两条路;只有当所有路都走不通时,才把这个异常重新抛出去。

第 3 段:类型不匹配才走 PropertyEditor(第 137-159 行)

Object convertedValue = newValue;

// Value not of required type?
if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) {
	if (typeDescriptor != null && requiredType != null && Collection.class.isAssignableFrom(requiredType)) {
		TypeDescriptor elementTypeDesc = typeDescriptor.getElementTypeDescriptor();
		if (elementTypeDesc != null) {
			Class<?> elementType = elementTypeDesc.getType();
			if (convertedValue instanceof String text) {
				if (Class.class == elementType || Enum.class.isAssignableFrom(elementType)) {
					convertedValue = StringUtils.commaDelimitedListToStringArray(text);
				}
				if (editor == null && String.class != elementType) {
					editor = findDefaultEditor(elementType.arrayType());
				}
			}
		}
	}
	if (editor == null) {
		editor = findDefaultEditor(requiredType);
	}
	convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor);
}

进入编辑器通道的条件:editor != null(第 1 段已找到 custom editor)或值还不是 requiredType 的实例。第二个条件就是"如有必要则转换"的字面实现——值类型已经对,整个 if 块跳过。

集合元素的特殊处理:目标是 Collection 且值是字符串时,先把它按逗号拆成数组;若元素类型不是 String,再为"元素数组类型"找默认编辑器(例如 Integer[] 的默认编辑器),让逗号串里的每个元素后续能被逐个转换。

findDefaultEditor(requiredType)(第 341-352 行)本身也是两级:先查注册表默认编辑器表,没有再 BeanUtils.findEditorByConvention(requiredType)——即 JDK 约定俗成的 XxxEditor 同包同名查找。

第 4 段:标准转换规则链(第 161-233 行)

经过编辑器通道后(或根本没进),requiredType != null 时进入硬编码标准规则。按 if-else 顺序:

if (convertedValue != null) {
	if (Object.class == requiredType) {
		return (T) convertedValue;
	}
	else if (requiredType.isArray()) {
		// Array required -> apply appropriate conversion of elements.
		if (convertedValue instanceof String text &&
				Enum.class.isAssignableFrom(requiredType.componentType())) {
			convertedValue = StringUtils.commaDelimitedListToStringArray(text);
		}
		return (T) convertToTypedArray(convertedValue, propertyName, requiredType.componentType());
	}
	else if (convertedValue.getClass().isArray()) {
		if (Collection.class.isAssignableFrom(requiredType)) {
			convertedValue = convertToTypedCollection(CollectionUtils.arrayToList(convertedValue),
					propertyName, requiredType, typeDescriptor);
			standardConversion = true;
		}
		else if (Array.getLength(convertedValue) == 1) {
			convertedValue = Array.get(convertedValue, 0);
			standardConversion = true;
		}
	}
	else if (convertedValue instanceof Collection<?> coll) {
		convertedValue = convertToTypedCollection(coll, propertyName, requiredType, typeDescriptor);
		standardConversion = true;
	}
	else if (convertedValue instanceof Map<?, ?> map) {
		convertedValue = convertToTypedMap(map, propertyName, requiredType, typeDescriptor);
		standardConversion = true;
	}

要点:

  • Object 类型直接放行;目标是数组时走 convertToTypedArray——它对集合、数组、单值三种输入分别处理,元素逐个递归调用 convertIfNecessary(带 propName[i] 的索引属性名)。
  • 输入是数组而目标是集合 → 数组转 List 后 convertToTypedCollection;数组长度为 1 且目标是单值 → 拆开取第一个元素。这就是 "42" 经 StringArrayPropertyEditor 变成 ["42"] 后,还能喂给 int 属性的原因。
  • Collection/Map 输入 → convertToTypedCollection/convertToTypedMap:从 typeDescriptor.getElementTypeDescriptor() 拿元素泛型类型,逐元素/逐键值递归转换;originalAllowed 标记记录"元素是否原封不动",全没动就返回原集合,动了才造新集合。这里能正确处理 List<String>→List<Long>,全靠 TypeDescriptor 携带的泛型信息——这就是 TypeConverter.convertIfNecessary(value, Long.class, methodParam) 与 convertIfNecessary(value, Long.class) 两个重载行为不同的根源。

接着是字符串与数字两条收尾规则:

	if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) {
		// We can stringify any primitive value...
		return (T) convertedValue.toString();
	}
	else if (convertedValue instanceof String text && !requiredType.isInstance(convertedValue)) {
		if (conversionAttemptEx == null && !requiredType.isInterface() && !requiredType.isEnum()) {
			try {
				Constructor<T> strCtor = requiredType.getConstructor(String.class);
				return BeanUtils.instantiateClass(strCtor, convertedValue);
			}
			catch (NoSuchMethodException ex) {
				// proceed with field lookup
				……
			}
		}
		String trimmedValue = text.trim();
		if (requiredType.isEnum() && trimmedValue.isEmpty()) {
			// It's an empty enum identifier: reset the enum value to null.
			return null;
		}
		convertedValue = attemptToConvertStringToEnum(requiredType, trimmedValue, convertedValue);
		standardConversion = true;
	}
	else if (convertedValue instanceof Number num && Number.class.isAssignableFrom(requiredType)) {
		convertedValue = NumberUtils.convertNumberToTargetClass(num, (Class<Number>) requiredType);
		standardConversion = true;
	}
  • 基本类型 → String:直接 toString(),短路返回。
  • String → 自定义类型:若 ConversionService 没试过(conversionAttemptEx == null)、目标不是接口也不是枚举,就找 public Xxx(String) 构造器直接反射实例化。这条规则解释了为什么 java.net.URI、java.util.Locale 这类带 String 构造器的类型在 XML 里能直接配字符串——当然前提是没有更专门的编辑器/转换器抢先。
  • 空串 → 枚举:直接返回 null("空枚举标识符重置为 null")。
  • 非空串 → 枚举:attemptToConvertStringToEnum——先按裸 Enum 全限定名解析(com.xxx.Color.RED),再退到 requiredType.getField(trimmedValue) 反射拿枚举常量。
  • Number → Number 子类:NumberUtils.convertNumberToTargetClass(带溢出/精度处理,见 12.11 的坑)。

第 5 段:null 的归宿(第 234-239 行)

else {
	// convertedValue == null
	if (requiredType == Optional.class) {
		convertedValue = Optional.empty();
	}
}

null 值唯一的特殊待遇:目标声明是 Optional 时给 Optional.empty(),其余就保持 null。

第 6 段:最终校验与 ConversionService 兜底(第 241-272 行)

if (!ClassUtils.isAssignableValue(requiredType, convertedValue)) {
	if (conversionAttemptEx != null) {
		// Original exception from former ConversionService call above...
		throw conversionAttemptEx;
	}
	else if (conversionService != null && typeDescriptor != null) {
		// ConversionService not tried before, probably custom editor found
		// but editor couldn't produce the required type...
		TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
		if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
			return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
		}
	}

	// Definitely doesn't match: throw IllegalArgumentException/IllegalStateException
	StringBuilder msg = new StringBuilder();
	msg.append("Cannot convert value of type '").append(ClassUtils.getDescriptiveType(newValue));
	msg.append("' to required type '").append(ClassUtils.getQualifiedName(requiredType)).append('\'');
	if (propertyName != null) {
		msg.append(" for property '").append(propertyName).append('\'');
	}
	if (editor != null) {
		msg.append(": PropertyEditor [").append(editor.getClass().getName()).append(
				"] returned inappropriate value of type '").append(
				ClassUtils.getDescriptiveType(convertedValue)).append('\'');
		throw new IllegalArgumentException(msg.toString());
	}
	else {
		msg.append(": no matching editors or conversion strategy found");
		throw new IllegalStateException(msg.toString());
	}
}

产物仍不是 requiredType 的实例时:① 之前 ConversionService 已经失败过(conversionAttemptEx != null)→ 原异常抛出;② 之前没试过(通常因为第 1 段就找到了 custom editor,但编辑器没产出正确类型)→ 现在补问 ConversionService;③ 都不行 → 拼异常消息并抛出,区分两种情形:编辑器产出了错误类型(IllegalArgumentException,"PropertyEditor X returned inappropriate value")与根本没有任何策略(IllegalStateException,"no matching editors or conversion strategy found")。这条消息正是 TypeConverterSupport 里把 IllegalStateException 翻译成 ConversionNotSupportedException 的那个异常——异常消息里带原类型、目标类型、属性名、编辑器类名,定位问题非常友好。

第 7 段:ConversionService 失败被 PropertyEditor 成功吸收(第 275-281 行)

if (conversionAttemptEx != null) {
	if (editor == null && !standardConversion && requiredType != null && Object.class != requiredType) {
		throw conversionAttemptEx;
	}
	logger.debug("Original ConversionService attempt failed - ignored since " +
			"PropertyEditor based conversion eventually succeeded", conversionAttemptEx);
}

走到这里说明前面所有路径都成功产出了类型匹配的值。但如果 ConversionService 早先失败过,是否要吞掉那个异常?判定:custom editor 存在、或发生过标准规则转换、或目标就是 Object——满足任一,说明 PropertyEditor/标准规则这条路后来成功了,就把 ConversionService 的失败降级为 debug 日志吞掉;只有"editor 为 null、标准规则也没用、目标不是 Object"这种彻底没别的路的情况,才把原始 conversionAttemptEx 抛出去。这就是"双通道"的最终语义:两条路都失败才算失败。

12.4.4 doConvertValue:PropertyEditor 的两种调用形态

private Object doConvertValue(@Nullable Object oldValue, @Nullable Object newValue,
		@Nullable Class<?> requiredType, @Nullable PropertyEditor editor) {

	Object convertedValue = newValue;

	if (editor != null && !(convertedValue instanceof String)) {
		// Not a String -> use PropertyEditor's setValue.
		try {
			editor.setValue(convertedValue);
			Object newConvertedValue = editor.getValue();
			if (newConvertedValue != convertedValue) {
				convertedValue = newConvertedValue;
				// Reset PropertyEditor: It already did a proper conversion.
				// Don't use it again for a setAsText call.
				editor = null;
			}
		}
		catch (Exception ex) { …… }
	}

	Object returnValue = convertedValue;

	if (requiredType != null && !requiredType.isArray() && convertedValue instanceof String[] array) {
		convertedValue = StringUtils.arrayToCommaDelimitedString(array);
	}

	if (convertedValue instanceof String newTextValue) {
		if (editor != null) {
			return doConvertTextValue(oldValue, newTextValue, editor);
		}
		else if (String.class == requiredType) {
			returnValue = convertedValue;
		}
	}

	return returnValue;
}
  • 非字符串值 + 编辑器:先走 setValue 让编辑器做"对象→对象"转换;若 getValue 返回了不同对象,说明编辑器真的做了事,随后把 editor 置 null——防止同一个编辑器被再 setAsText 一次造成重复转换。
  • String[] → 逗号串:非数组目标收到 String[] 时,拼成 CSV 再交给编辑器 setAsText。
  • 字符串值 + 编辑器:走 doConvertTextValue——先 editor.setValue(oldValue) 把旧值喂回去(很多编辑器实现用旧值做增量编辑的上下文),再 setAsText(newTextValue),最后 getValue() 取结果。

至此,一次 convertIfNecessary 的完整判定顺序可以总结为:custom editor → ConversionService → 集合元素预处理 → 默认编辑器/JDK 约定编辑器 → 标准规则(数组/集合/Map/字符串构造器/枚举/数字)→ 最终校验(补试 ConversionService / 抛异常)。这就是 Spring 数据绑定的"总调度室",后文 12.8 会把它放回 populateBean 与 doBind 的调用栈里。

12.5 源码剖析(三):ConversionService 接口族

12.5.1 ConversionService:读侧契约

spring-core/src/main/java/org/springframework/core/convert/ConversionService.java 全文核心就四个方法(源码见 12.2.3,不再贴整段),这里逐条讲设计:

  • boolean canConvert(Class, Class):粗粒度探测。两个 Class 只能问"大概能不能转"。
  • boolean canConvert(TypeDescriptor, TypeDescriptor):细粒度探测,带着字段上下文。注意 javadoc 的特别声明:集合/数组/Map 之间,即使 canConvert 返回 true,convert 仍可能因元素不可转而抛 ConversionException——也就是说 canConvert 对容器类只承诺"容器结构可转换",不承诺元素。
  • <T> T convert(Object, Class):最常用的入口,源类型从值本身反推。
  • default Object convert(Object, TypeDescriptor):6.1 新增的默认方法,源码一行:convert(source, TypeDescriptor.forObject(source), targetType)——只指定目标描述符,源描述符现场构造。
  • Object convert(Object, TypeDescriptor, TypeDescriptor):三参终极版本。注意它的契约:sourceType == null 时 source 必须也是 null(Assert.isTrue(source == null, ...));sourceType 非 null 时,source 必须是它的实例(第 175-178 行的 IllegalArgumentException 校验)。

12.5.2 ConverterRegistry:写侧契约

spring-core/src/main/java/org/springframework/core/convert/converter/ConverterRegistry.java:

public interface ConverterRegistry {
	void addConverter(Converter<?, ?> converter);
	<S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter);
	void addConverter(GenericConverter converter);
	void addConverterFactory(ConverterFactory<?, ?> factory);
	void removeConvertible(Class<?> sourceType, Class<?> targetType);
}

三个 addConverter 重载对应三种注册来源:

  1. 裸 Converter:类型对从它的泛型参数里反射解析(Converter<String, Long> → String→Long)。解析失败抛 IllegalArgumentException。
  2. 显式类型对 + Converter:允许同一个 Converter 实例挂到多个类型对上复用,不必为每对都造一个类。
  3. GenericConverter:类型对由 getConvertibleTypes() 自报家门。

addConverterFactory 注册"区间工厂";removeConvertible(source, target) 删掉指定类型对的所有转换器。

ConfigurableConversionService(spring-core/.../convert/support/ConfigurableConversionService.java)只是 interface ConfigurableConversionService extends ConversionService, ConverterRegistry {}——把读侧与写侧合成一个接口,注释说明这是为了让"在 ConfigurableEnvironment 启动代码里临时增删转换器"方便。GenericConversionService 直接 implements ConfigurableConversionService。

12.5.3 Converter<S,T>:函数式 SPI

@FunctionalInterface
public interface Converter<S, T extends @Nullable Object> {
	T convert(S source);

	default <U> Converter<S, @Nullable U> andThen(Converter<? super T, ? extends @Nullable U> after) {
		Assert.notNull(after, "'after' Converter must not be null");
		return (S s) -> {
			T initialResult = convert(s);
			return (initialResult != null ? after.convert(initialResult) : null);
		};
	}
}
  • 注解是 @FunctionalInterface,所以可以用 lambda 写转换器:Converter<String, Account> c = s -> accountRepo.find(s);。
  • 泛型上界 T extends @Nullable Object——7.0.9 里用 JSpecify 的 @Nullable 标注了"目标可以是 null"(即转换器允许返回 null),而 source 契约是"never null"。
  • 5.3 加的 andThen 是经典组合子:StringToNumber.andThen(NumberToLong) 链式转换。返回的 lambda 里对中间结果做了 null 短路——中间转成 null 就不再调 after。

12.5.4 ConverterFactory<S,R>:区间工厂

public interface ConverterFactory<S, R> {
	<T extends R> Converter<S, ? extends @Nullable T> getConverter(Class<T> targetType);
}

一个工厂为"源类型 S → 基类 R 的任意子类型 T"一族转换负责。NumberToNumberConverterFactory(S=Number, R=Number)为每个具体数字子类派一个 NumberToNumber<T> 实例。与直接注册 N 个 Converter 相比,工厂省了 N 个类,也让"同方向一族"的策略可以集中演化。

12.5.5 GenericConverter 与 ConvertiblePair

public interface GenericConverter {
	@Nullable Set<ConvertiblePair> getConvertibleTypes();
	@Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType);

	final class ConvertiblePair {
		private final Class<?> sourceType;
		private final Class<?> targetType;
		// 构造器 Assert.notNull;equals/hashCode 用 == 比对 Class;toString: "S -> T"
	}
}

要点:

  • getConvertibleTypes() 返回 null 是合法的——但只允许条件转换器这么做(注册时 Converters.add 会断言 converter instanceof ConditionalConverter),表示"我声明不了类型对,运行期 matches() 说了算"。
  • convert 能拿到 source/target 两侧的 TypeDescriptor,因此可见泛型与字段注解——这是 CollectionToCollectionConverter 知道"目标集合元素是 Long"的依据。
  • ConvertiblePair 是值对象:equals 用 == 比 Class(Class 对象全局唯一,引用相等即可,比 getName() 快得多);hashCode = sourceType.hashCode()*31 + targetType.hashCode()。它是注册表 Map 的 key。

12.5.6 ConditionalConverter:运行期资格审查

public interface ConditionalConverter {
	boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType);
}

public interface ConditionalGenericConverter extends GenericConverter, ConditionalConverter {
}

matches 的典型用途 javadoc 里给了两个:① String→Date 时,目标字段带没带 @DateTimeFormat;② String→Account 时,目标类有没有 public static findAccount(String)。ConditionalGenericConverter 是纯接口组合,把"多类型对 + 上下文敏感"两个能力合在一个契约里——FormattingConversionService 的注解转换器、CollectionToCollectionConverter、ObjectToObjectConverter 都实现它。

12.5.7 TypeDescriptor:被忽略的一等公民

TypeDescriptor(spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java)是泛型擦除世界里的"类型全息图"。关键静态构造器:

public static @Nullable TypeDescriptor forObject(@Nullable Object source) { … }   // 第 561 行
public static TypeDescriptor valueOf(@Nullable Class<?> type) { … }              // 第 575 行
  • forObject(source):从值对象反推描述符(值非 null 时用 value.getClass(),并带着 ResolvableType.forType(value.getClass(), ...) 做窄化——第 222 行 ResolvableType.forType(value.getClass(), getResolvableType()),保留了声明类型里的泛型上下文)。
  • 实例方法:getResolvableType()(第 161 行)、hasAnnotation(Class)(第 280 行)、isAssignableTo(TypeDescriptor)(第 320 行)、getElementTypeDescriptor()(第 371-377 行:数组取组件类型、Stream 取泛型 0、其余按集合泛型 0)、getMapKeyTypeDescriptor()/getMapValueTypeDescriptor()(第 418/453 行)。

为什么它重要?JDK 泛型擦除后,运行期拿到一个 ArrayList 实例,无从知道它声明时是 List<String> 还是 List<Long>。TypeDescriptor 把"字段/参数的声明类型"(通过 ResolvableType)与"值的实际类型"合并成一个描述对象,转换器才能问出"目标集合元素是 Long"。TypeConverterSupport 把 MethodParameter/Field 包成 new TypeDescriptor(methodParam) 传下来,正是为了这个。

12.5.8 异常体系

三个异常(同包 spring-core/.../convert/):

  • ConversionException(基类)。
  • ConversionFailedException:转换过程启动了但失败了——构造时收 sourceType/targetType/source/cause 四元组。ConversionUtils.invokeConverter(见 12.6.5)把转换器抛的任意 Throwable 包装成它,已有的 ConversionFailedException 原样透传。
  • ConverterNotFoundException:根本找不到能处理这个类型对的转换器。

这套异常在 TypeConverterSupport 里再翻译成 TypeMismatchException/ConversionNotSupportedException(见 12.4.1),形成"底层精确、上层语义化"的两层异常模型。

12.6 源码剖析(四):GenericConversionService 的注册表与查找算法

spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java 是整个 ConversionService 体系的内核。先把它的"三件套"字段摆出来(第 63-79 行):

public class GenericConversionService implements ConfigurableConversionService {

	/**
	 * General NO-OP converter used when conversion is not required.
	 */
	private static final GenericConverter NO_OP_CONVERTER = new NoOpConverter("NO_OP");

	/**
	 * Used as a cache entry when no converter is available.
	 * This converter is never returned.
	 */
	private static final GenericConverter NO_MATCH = new NoOpConverter("NO_MATCH");


	private final Converters converters = new Converters();

	private final Map<ConverterCacheKey, GenericConverter> converterCache = new ConcurrentReferenceHashMap<>(64);

两个哨兵 + 两个容器:

  • NO_OP_CONVERTER:"不需要转换"的占位——当源类型可直接赋值给目标类型时命中,convert 里原样返回 source。
  • NO_MATCH:"查过了,没有"的缓存哨兵——避免每次转换失败都重新跑一遍昂贵的类型层次搜索。它永远不会被返回,只作为缓存标记。
  • converters:Converters 内部类实例,存所有已注册转换器。
  • converterCache:ConverterCacheKey(source, target) → GenericConverter 的结果缓存,ConcurrentReferenceHashMap(软引用、带容量 64 初始),这就是它线程安全与低内存压力兼得的原因。

版本差异提醒:旧版 Spring(5.x-6.x 前期)这里还有一个 Map<Class<?>, Set<ConvertiblePair>> classCache(缓存"某个类涉及哪些类型对")与 cachedConverters。7.0.9 中这两个缓存已被移除,类型层次列表在缓存未命中时由 getClassHierarchy 现场计算。老资料里"converterCache + converters + classCache 三缓存"的说法在本版本不再成立,请以这里的两容器为准。

12.6.1 注册路径:addConverter 的三个入口

入口一:裸 Converter(第 85-95 行)

@Override
public void addConverter(Converter<?, ?> converter) {
	ResolvableType[] typeInfo = getRequiredTypeInfo(converter.getClass(), Converter.class);
	if (typeInfo == null && converter instanceof DecoratingProxy decoratingProxy) {
		typeInfo = getRequiredTypeInfo(decoratingProxy.getDecoratedClass(), Converter.class);
	}
	if (typeInfo == null) {
		throw new IllegalArgumentException("Unable to determine source type <S> and target type <T> for your " +
				"Converter [" + converter.getClass().getName() + "]; does the class parameterize those types?");
	}
	addConverter(new ConverterAdapter(converter, typeInfo[0], typeInfo[1]));
}

getRequiredTypeInfo(第 260-272 行)用 ResolvableType.forClass(converterClass).as(Converter.class).getGenerics() 反射解析 <S, T>。两道补救:① 泛型解析不到但 converter 是 DecoratingProxy(AOP 代理对象)时,对被代理的原类再解析一次;② 还解析不到就抛异常——异常消息直接点破:"does the class parameterize those types?"。这就是为什么你的 Converter 必须写成 class MyConverter implements Converter<String, Long> 而不能裸写 Converter——类型对必须在 implements 处静态参数化。解析成功后包成 ConverterAdapter 走 GenericConverter 注册通道。

入口二:显式类型对(第 98-101 行)

public <S, T> void addConverter(Class<S> sourceType, Class<T> targetType, Converter<? super S, ? extends T> converter) {
	addConverter(new ConverterAdapter(
			converter, ResolvableType.forClass(sourceType), ResolvableType.forClass(targetType)));
}

跳过泛型解析,类型对由调用方直接给。

入口三:GenericConverter 与工厂(第 104-121 行)

public void addConverter(GenericConverter converter) {
	this.converters.add(converter);
	invalidateCache();
}

public void addConverterFactory(ConverterFactory<?, ?> factory) {
	ResolvableType[] typeInfo = getRequiredTypeInfo(factory.getClass(), ConverterFactory.class);
	……
	addConverter(new ConverterFactoryAdapter(factory,
			new ConvertiblePair(typeInfo[0].toClass(), typeInfo[1].toClass())));
}

共同点:注册动作完成后都调 invalidateCache()——converterCache.clear()(第 274-276 行)。注册是写操作、查转换是读操作,这里不做细粒度失效,而是整体清空缓存。由于容器内注册转换器通常发生在启动期(远早于批量转换),这个粗暴策略在实践上几乎零成本,但它也意味着:运行期动态注册转换器会触发全缓存失效,高吞吐线上慎在运行期注册。

12.6.2 Converters 与 ConvertersForPair:注册的存储结构

private static class Converters {

	private final Set<GenericConverter> globalConverters = new CopyOnWriteArraySet<>();

	private final Map<ConvertiblePair, ConvertersForPair> converters = new ConcurrentHashMap<>(256);

	public void add(GenericConverter converter) {
		Set<ConvertiblePair> convertibleTypes = converter.getConvertibleTypes();
		if (convertibleTypes == null) {
			Assert.state(converter instanceof ConditionalConverter,
					"Only conditional converters may return null convertible types");
			this.globalConverters.add(converter);
		}
		else {
			for (ConvertiblePair convertiblePair : convertibleTypes) {
				getMatchableConverters(convertiblePair).add(converter);
			}
		}
	}

	private ConvertersForPair getMatchableConverters(ConvertiblePair convertiblePair) {
		return this.converters.computeIfAbsent(convertiblePair, key -> new ConvertersForPair());
	}
}

注册时分流:

  • getConvertibleTypes() 返回 null → 必须是 ConditionalConverter,进 globalConverters(CopyOnWriteArraySet——注册极少、读取频繁,COW 集合适配)。
  • 返回集合 → 每个 ConvertiblePair 一个 ConvertersForPair 桶,computeIfAbsent 建桶。

ConvertersForPair(第 612-642 行):

private static class ConvertersForPair {

	private final Deque<GenericConverter> converters = new ConcurrentLinkedDeque<>();

	public void add(GenericConverter converter) {
		this.converters.addFirst(converter);
	}

	public @Nullable GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
		// Look for proper match among all converters (taking full generics into account)
		for (GenericConverter converter : this.converters) {
			if (!(converter instanceof ConditionalGenericConverter genericConverter) ||
					genericConverter.matches(sourceType, targetType)) {
				return converter;
			}
		}
		// Fallback to pre-6.2.3 behavior: accept Class match for unresolvable generics
		for (GenericConverter converter : this.converters) {
			if (converter instanceof ConverterAdapter converterAdapter &&
					converterAdapter.matchesFallback(sourceType, targetType)) {
				return converter;
			}
		}
		return null;
	}
}

两个要点:

  1. addFirst + 队列顺序:后注册的转换器排在队头,查找时先命中——后注册优先。这就是"用户自定义转换器可以盖过内置转换器"的实现基础:DefaultConversionService 先注册内置,用户后注册的同类型对转换器自然插队头。
  2. 两轮匹配:第一轮按"全泛型精确匹配"(ConditionalGenericConverter.matches 不过就跳过);第一轮全军覆没时,第二轮走 matchesFallback——6.2.3 之前的旧行为:对泛型不可解析的场景放宽到 Class 匹配。这是为了兼容"原始类型/泛型擦除导致 matches 过严"的历史行为,注释写明了是 fallback。

12.6.3 getConverter:缓存优先、层次遍历、默认兜底

protected @Nullable GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
	ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType);
	GenericConverter converter = this.converterCache.get(key);
	if (converter != null) {
		return (converter != NO_MATCH ? converter : null);
	}

	converter = this.converters.find(sourceType, targetType);
	if (converter == null) {
		converter = getDefaultConverter(sourceType, targetType);
	}

	if (converter != null) {
		this.converterCache.put(key, converter);
		return converter;
	}

	this.converterCache.put(key, NO_MATCH);
	return null;
}

流程三步:① 查 converterCache,命中直接用(NO_MATCH 翻译回 null);② 未命中走 converters.find 穷尽搜索;③ 注册表里没有就问 getDefaultConverter——它只做一件事:

protected @Nullable GenericConverter getDefaultConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
	return (sourceType.isAssignableTo(targetType) ? NO_OP_CONVERTER : null);
}

源可赋值到目标 → NO_OP(无需转换);否则 null。无论找到与否都写回缓存——找到了存真转换器,没找到存 NO_MATCH 哨兵。这就是"同一类型对第二次转换零查找成本"与"失败类型对不会重复搜索"的机制。

12.6.4 Converters.find:类型层次的双重循环

public @Nullable GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) {
	// Search the full type hierarchy
	List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType());
	List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType());
	for (Class<?> sourceCandidate : sourceCandidates) {
		for (Class<?> targetCandidate : targetCandidates) {
			ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate);
			GenericConverter converter = getRegisteredConverter(sourceType, targetType, convertiblePair);
			if (converter != null) {
				return converter;
			}
		}
	}
	return null;
}

为什么需要层次遍历?用户注册的是 Converter<Number, String>,而值的实际类型是 Integer——注册表的 key 是 Number→String,拿 Integer→String 直接查查不到。于是算法把源类型与目标类型各自的完整层次(父类 + 接口序列)展开,做笛卡尔积逐一尝试。

getClassHierarchy(Class)(第 541-567 行)产出的顺序有讲究:先 resolvePrimitiveIfNecessary 把 int 规整成 Integer;然后 BFS 式地展开——自己排最前,父类紧随(跳过 Object 与 Enum),所有接口也并入;枚举类型最后补 Enum.class 与其接口;末尾再补 Object.class。越靠前越具体,所以双重循环天然实现了"最具体的转换器优先":Integer→String 比 Number→String 先被尝试,Number→String 又比 Object→String 先。

getRegisteredConverter(第 516-534 行)对每个候选类型对:

private @Nullable GenericConverter getRegisteredConverter(TypeDescriptor sourceType,
		TypeDescriptor targetType, ConvertiblePair convertiblePair) {

	// Check specifically registered converters
	ConvertersForPair convertersForPair = this.converters.get(convertiblePair);
	if (convertersForPair != null) {
		GenericConverter converter = convertersForPair.getConverter(sourceType, targetType);
		if (converter != null) {
			return converter;
		}
	}
	// Check ConditionalConverters for a dynamic match
	for (GenericConverter globalConverter : this.globalConverters) {
		if (((ConditionalConverter) globalConverter).matches(sourceType, targetType)) {
			return globalConverter;
		}
	}
	return null;
}

先查类型对桶(桶内还要过 matches 精确筛选),再遍历全局条件转换器做动态匹配。注意全局转换器的遍历发生在每一个候选类型对上——所以全局转换器数量要克制,它们是 O(层次积 × 全局数) 的开销。

12.6.5 convert 主流程与异常包装

public @Nullable Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
	Assert.notNull(targetType, "Target type to convert to cannot be null");
	if (sourceType == null) {
		Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
		return handleResult(null, targetType, convertNullSource(null, targetType));
	}
	if (source != null && !sourceType.getObjectType().isInstance(source)) {
		throw new IllegalArgumentException("Source to convert from must be an instance of [" +
				sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
	}
	GenericConverter converter = getConverter(sourceType, targetType);
	if (converter != null) {
		Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
		return handleResult(sourceType, targetType, result);
	}
	return handleConverterNotFound(source, sourceType, targetType);
}
  • sourceType 为 null ⇒ source 必须为 null ⇒ 走 convertNullSource(第 206-211 行:目标是 Optional 返回 Optional.empty(),否则 null;子类可覆盖)。
  • 类型契约校验:source 非 null 时必须是 sourceType 的实例。
  • 找到转换器 → ConversionUtils.invokeConverter:
public static @Nullable Object invokeConverter(GenericConverter converter, @Nullable Object source,
		TypeDescriptor sourceType, TypeDescriptor targetType) {
	try {
		return converter.convert(source, sourceType, targetType);
	}
	catch (ConversionFailedException ex) {
		throw ex;
	}
	catch (Throwable ex) {
		throw new ConversionFailedException(sourceType, targetType, source, ex);
	}
}

转换器内部抛的任意异常(含 IllegalArgumentException)都被包装成 ConversionFailedException——这正是 TypeConverterDelegate 第 2 段 catch 的那个异常类型。

  • handleResult:结果为 null 时 assertNotPrimitiveTargetType——null 不能赋给基本类型,否则抛 ConversionFailedException(cause 为 "A null value cannot be assigned to a primitive type")。这条规则解释了 int 属性收到空串时为什么是转换失败而不是静默 0。
  • 找不到转换器 → handleConverterNotFound(第 278-290 行):source 为 null → 断言目标非基本类型后返回 null;source 实际可赋值到目标 → 直接返回 source(兜底兼容);否则抛 ConverterNotFoundException。

12.7 源码剖析(五):适配器三兄弟与默认注册表

12.7.1 ConverterAdapter:把 Converter 伪装成 GenericConverter

裸 Converter 与 ConverterFactory 注册进来后,都被适配成 ConditionalGenericConverter。ConverterAdapter(GenericConversionService.java 第 311-364 行):

private final class ConverterAdapter implements ConditionalGenericConverter {

	private final Converter<Object, Object> converter;

	private final ConvertiblePair typeInfo;

	private final ResolvableType targetType;

	@Override
	public Set<ConvertiblePair> getConvertibleTypes() {
		return Collections.singleton(this.typeInfo);
	}

	@Override
	public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
		// Check raw type first...
		if (this.typeInfo.getTargetType() != targetType.getObjectType()) {
			return false;
		}
		// Full check for complex generic type match required?
		ResolvableType rt = targetType.getResolvableType();
		if (!(rt.getType() instanceof Class) && !rt.isAssignableFromResolvedPart(this.targetType)) {
			return false;
		}
		return !(this.converter instanceof ConditionalConverter conditionalConverter) ||
				conditionalConverter.matches(sourceType, targetType);
	}

	public boolean matchesFallback(TypeDescriptor sourceType, TypeDescriptor targetType) {
		return (this.typeInfo.getTargetType() == targetType.getObjectType() &&
				this.targetType.hasUnresolvableGenerics() &&
				(!(this.converter instanceof ConditionalConverter conditionalConverter) ||
						conditionalConverter.matches(sourceType, targetType)));
	}

	@Override
	public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
		if (source == null) {
			return convertNullSource(sourceType, targetType);
		}
		return this.converter.convert(source);
	}
}

matches 三段:

  1. 裸类型快筛:适配器记录的目标类型与请求目标描述符的 raw type 不等 → 直接 false。注意这里用 != 比 Class——因为 find 阶段已经按类型层次把 raw type 对齐过一次,能进到桶里的 raw type 必然相等,这一步实际是双保险。
  2. 泛型精确校验:请求目标不是裸 Class(即携带泛型,如 List<Long>)时,要求请求泛型与注册泛型 isAssignableFromResolvedPart 兼容。这是"后注册优先"之外的第二道闸门:同桶里 Converter<String, List> 与 Converter<String, List<Long>> 并存时,靠它选对。
  3. 用户条件:如果被适配的 Converter 自己还实现了 ConditionalConverter(例如 StringToEnumConverterFactory 家族),把 matches 委托给它。

matchesFallback 是 6.2.3 引入的回退:泛型不可解析(hasUnresolvableGenerics(),典型如反射得到的原始类型)时,放宽到 raw Class 相等即接受——配合 12.6.2 讲的两轮匹配。

12.7.2 ConverterFactoryAdapter:工厂的运行期派生

private final class ConverterFactoryAdapter implements ConditionalGenericConverter {

	private final ConverterFactory<Object, Object> converterFactory;

	private final ConvertiblePair typeInfo;

	@Override
	public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
		boolean matches = true;
		if (this.converterFactory instanceof ConditionalConverter conditionalConverter) {
			matches = conditionalConverter.matches(sourceType, targetType);
		}
		if (matches) {
			Converter<?, ?> converter = this.converterFactory.getConverter(targetType.getType());
			if (converter instanceof ConditionalConverter conditionalConverter) {
				matches = conditionalConverter.matches(sourceType, targetType);
			}
		}
		return matches;
	}

	@Override
	public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
		if (source == null) {
			return convertNullSource(sourceType, targetType);
		}
		return this.converterFactory.getConverter(targetType.getObjectType()).convert(source);
	}

注意它的 matches 是两级的:工厂自己可以条件化;工厂现造出来的 Converter 也可以条件化——两层都过才算数。convert 里 getConverter(targetType.getObjectType()) 每次按实际目标类型现取派生转换器——工厂的"区间"语义就在这里兑现:注册表只存 String→Number 一个 ConvertiblePair,真转 String→Integer 时现造 StringToNumber(Integer)。

12.7.3 ConverterCacheKey 与 canConvert 族

private static final class ConverterCacheKey implements Comparable<ConverterCacheKey> {
	// equals: sourceType && targetType 各自 equals;hashCode: source*29 + target
	// compareTo: 两侧 ResolvableType.toString 字典序
}

key 的相等性完全委托给两个 TypeDescriptor.equals(后者比对 ResolvableType 与注解数组)。实现 Comparable 是为了潜在的有序容器使用(当前 Map 用不上,但无害)。

canConvert 两个重载(第 133-143 行)就是 getConverter 的布尔化:

public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
	Assert.notNull(targetType, "Target type to convert to cannot be null");
	return (sourceType == null || getConverter(sourceType, targetType) != null);
}

注意:sourceType 为 null(即 source 为 null 的探测)时直接返回 true——null 永远"可以转",真正的 null 兼容性校验(基本类型拒绝)在 convert 执行时才发生。这呼应了 ConversionService javadoc 里"canConvert 乐观、convert 才严格"的契约。另有个 canBypassConvert(第 156-159 行):判断是否可绕过转换(即命中 NO_OP),供性能敏感场景判断"这次转换是否零成本"。

12.7.4 DefaultConversionService:7.0.9 内置转换器全清单

spring-core/src/main/java/org/springframework/core/convert/support/DefaultConversionService.java。构造器一行 addDefaultConverters(this);addDefaultConverters(第 89-105 行)分三段注册,以下清单逐行对照仓库源码统计:

标量段 addScalarConverters(第 139-182 行):

converterRegistry.addConverterFactory(new NumberToNumberConverterFactory());

converterRegistry.addConverterFactory(new StringToNumberConverterFactory());
converterRegistry.addConverter(Number.class, String.class, new ObjectToStringConverter());

converterRegistry.addConverter(new StringToCharacterConverter());
converterRegistry.addConverter(Character.class, String.class, new ObjectToStringConverter());

converterRegistry.addConverter(new NumberToCharacterConverter());
converterRegistry.addConverterFactory(new CharacterToNumberFactory());

converterRegistry.addConverter(new StringToBooleanConverter());
converterRegistry.addConverter(Boolean.class, String.class, new ObjectToStringConverter());

converterRegistry.addConverterFactory(new StringToEnumConverterFactory());
converterRegistry.addConverter(new EnumToStringConverter((ConversionService) converterRegistry));

converterRegistry.addConverterFactory(new IntegerToEnumConverterFactory());
converterRegistry.addConverter(new EnumToIntegerConverter((ConversionService) converterRegistry));

converterRegistry.addConverter(new StringToLocaleConverter());
converterRegistry.addConverter(Locale.class, String.class, new ObjectToStringConverter());

converterRegistry.addConverter(new StringToCharsetConverter());
converterRegistry.addConverter(Charset.class, String.class, new ObjectToStringConverter());

converterRegistry.addConverter(new StringToCurrencyConverter());
converterRegistry.addConverter(Currency.class, String.class, new ObjectToStringConverter());

converterRegistry.addConverter(new StringToPropertiesConverter());
converterRegistry.addConverter(new PropertiesToStringConverter());

converterRegistry.addConverter(new StringToUUIDConverter());
converterRegistry.addConverter(UUID.class, String.class, new ObjectToStringConverter());

converterRegistry.addConverter(new StringToPatternConverter());
converterRegistry.addConverter(Pattern.class, String.class, new ObjectToStringConverter());

if (KotlinDetector.isKotlinPresent()) {
	converterRegistry.addConverter(new StringToRegexConverter());
	converterRegistry.addConverter(kotlin.text.Regex.class, String.class, new ObjectToStringConverter());
}

逐个要点:

  • NumberToNumberConverterFactory(Number→Number 区间,实现见 12.7.5)。
  • StringToNumberConverterFactory + 反向 Number→String(ObjectToStringConverter 就是调 toString())。
  • char 一族四个:StringToCharacterConverter、NumberToCharacterConverter、CharacterToNumberFactory、Character→String。
  • StringToBooleanConverter(实现:true/on/yes/1→TRUE,false/off/no/0→FALSE,其余抛 IllegalArgumentException;空串→null)。
  • 枚举一族四个:StringToEnumConverterFactory(Enum.valueOf,空串→null)、EnumToStringConverter、IntegerToEnumConverterFactory、EnumToIntegerConverter——任务提纲里猜的 "EnumToIntegerConverter?" 在 7.0.9 确实存在并被注册(构造还收 ConversionService,因为枚举序号转换要回退到枚举名转换)。
  • 随后成对注册 String→Locale/Charset/Currency/UUID/Pattern 及其反向 X→String。
  • StringToPropertiesConverter/PropertiesToStringConverter:Properties↔字符串行对。
  • Kotlin 在场时才追加 StringToRegexConverter 与 kotlin.text.Regex→String——条件注册的范例。

集合段 addCollectionConverters(第 114-137 行):

converterRegistry.addConverter(new ArrayToCollectionConverter(conversionService));
converterRegistry.addConverter(new CollectionToArrayConverter(conversionService));

converterRegistry.addConverter(new ArrayToArrayConverter(conversionService));
converterRegistry.addConverter(new CollectionToCollectionConverter(conversionService));
converterRegistry.addConverter(new MapToMapConverter(conversionService));

converterRegistry.addConverter(new ArrayToStringConverter(conversionService));
converterRegistry.addConverter(new StringToArrayConverter(conversionService));

converterRegistry.addConverter(new ArrayToObjectConverter(conversionService));
converterRegistry.addConverter(new ObjectToArrayConverter(conversionService));

converterRegistry.addConverter(new CollectionToStringConverter(conversionService));
converterRegistry.addConverter(new StringToCollectionConverter(conversionService));

converterRegistry.addConverter(new CollectionToObjectConverter(conversionService));
converterRegistry.addConverter(new ObjectToCollectionConverter(conversionService));

converterRegistry.addConverter(new StreamConverter(conversionService));

注意这些转换器构造器都收 ConversionService 自身——因为它们是"容器级转换器":自己只负责拆/装容器,元素转换递归回 ConversionService 处理。CollectionToCollectionConverter.convert 里那句 this.conversionService.convert(sourceElement, …)(第 89 行)就是递归点。StreamConverter 支持 Stream 与集合/数组互转。

尾部追加(第 93-104 行):

converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new DateToInstantConverter());
converterRegistry.addConverter(new InstantToDateConverter());
converterRegistry.addConverter(new StringToTimeZoneConverter());
converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());

converterRegistry.addConverter(new ObjectToObjectConverter());
converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new FallbackObjectToStringConverter());
converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new OptionalToObjectConverter((ConversionService) converterRegistry));
  • 时间一系:Date↔Instant 双向、String→TimeZone、ZoneId→TimeZone、ZonedDateTime→Calendar。
  • ObjectToObjectConverter(反射桥,见 12.7.6)、IdToEntityConverter(按静态查找方法把 id 转实体)、FallbackObjectToStringConverter(兜底 toString)、Optional 双向两个。

静态工具方法 getSharedInstance()(第 69-81 行)是双重检查锁的全局共享实例:volatile 字段 + synchronized 双检。javadoc 泼了冷水:强烈建议自定义场景自己 new,这个共享实例只是给"够不到长生命周期 ConversionService 的代码路径"兜底用的——共享意味着不可定制。

12.7.5 两个数字转换器的实现要点

NumberToNumberConverterFactory(全文见 12.3 节阅读记录,此处讲设计):它 implements ConverterFactory<Number, Number>, ConditionalConverter,matches 一句 !sourceType.equals(targetType)——同类型不转(避免 Integer→Integer 白绕一圈)。getConverter 每次 new NumberToNumber<>(targetType),转换委托 NumberUtils.convertNumberToTargetClass。

StringToNumberConverterFactory:StringToNumber.convert 里 if (source.isEmpty()) return null; 然后 NumberUtils.parseNumber(source, targetType)——空串→null 是数字一族的统一约定。

12.7.6 ObjectToObjectConverter 与 FallbackObjectToStringConverter:反射桥与兜底

ObjectToObjectConverter(spring-core/.../convert/support/ObjectToObjectConverter.java)是 Object→Object 的条件转换器,转换算法 javadoc 列了四步(源码第 42-60 行注释):① 源对象上找 to{TargetSimpleName}() 实例方法;② 目标类上找静态 valueOf/of/from(sourceType);③ 目标类上单参 sourceType 构造器;④ 都没有抛异常。关键实现细节:

  • conversionExecutableCache(ConcurrentReferenceHashMap,32 初始)缓存"目标类 → 上次命中的 Executable";isApplicable 校验缓存的可执行物是否适用于本次 sourceType。
  • determineToMethod/determineFactoryMethod 里两处 if (String.class == targetClass) return null;——明确拒绝 toString() 与 String.valueOf:对象→字符串不归它管,归 FallbackObjectToStringConverter。
  • 未命中时抛的 IllegalStateException 消息模板会展开成 "No toInteger() method exists on java.lang.Number, and no static valueOf/of/from(java.lang.Number) method or Integer(java.lang.Number) constructor exists on java.lang.Integer.",排查极友好。

FallbackObjectToStringConverter:Object→String 条件转换器,matches 规则——源不是 String 本身,且源是 CharSequence/StringWriter,或 ObjectToObjectConverter.hasConversionMethodOrConstructor(source, String.class)(即源类有 String 构造器或 valueOf(String)/of(String)/from(String) 静态工厂)。convert 就一句 source.toString()。它是"任何东西→字符串"的最后兜底,所以排在所有显式 X→String 之后生效。

12.8 源码剖析(六):格式化体系——Formatter 如何变成转换器

12.8.1 Printer / Parser / Formatter 三接口

spring-context/src/main/java/org/springframework/format/ 下三个接口:

public interface Printer<T> {
	String print(T field, Locale locale);
}

public interface Parser<T> {
	T parse(String text, Locale locale) throws ParseException;
}

public interface Formatter<T> extends Printer<T>, Parser<T> {
}

注意签名里的 Locale 参数——格式化是与 Locale 绑定的,这是它与 Converter 的本质分野:Converter 问"能不能转",Formatter 问"在这个 Locale 下显示成什么样、怎么读回来"。Formatter 是空接口,只是同时继承两者。

12.8.2 FormatterRegistry:注册侧 API

spring-context/.../format/FormatterRegistry.java 继承 ConverterRegistry,即格式化注册表首先就是一个转换注册表,再追加六个方法:

void addPrinter(Printer<?> printer);
void addParser(Parser<?> parser);
void addFormatter(Formatter<?> formatter);
void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter);
void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser);
void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory);

javadoc 里有一条关键语义(addFormatterForFieldType):打印时若字段类型与 Formatter 的 T 不一致,会先强转到 T 再打印;解析后若结果不能直接赋给字段类型,也会先强转再返回。也就是说 Formatter 层允许"类型接力",而这层强转恰恰是 FormattingConversionService 自己实现的(见下)。

12.8.3 FormattingConversionService:两个缓存 + 四个桥接转换器

spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java 继承 GenericConversionService 并实现 FormatterRegistry, EmbeddedValueResolverAware。字段只有三个:

private @Nullable StringValueResolver embeddedValueResolver;

private final Map<AnnotationConverterKey, GenericConverter> cachedPrinters = new ConcurrentHashMap<>(64);

private final Map<AnnotationConverterKey, GenericConverter> cachedParsers = new ConcurrentHashMap<>(64);

注册入口全部翻译成 GenericConverter 注册:

public void addFormatterForFieldType(Class<?> fieldType, Formatter<?> formatter) {
	addConverter(new PrinterConverter(fieldType, formatter, this));
	addConverter(new ParserConverter(fieldType, formatter, this));
}

public void addFormatterForFieldType(Class<?> fieldType, Printer<?> printer, Parser<?> parser) {
	addConverter(new PrinterConverter(fieldType, printer, this));
	addConverter(new ParserConverter(fieldType, parser, this));
}

一个 Formatter 拆成两个方向相反的 GenericConverter:PrinterConverter 注册类型对 fieldType → String,ParserConverter 注册 String → fieldType。注册进父类的 converters 表后,就完全融入 GenericConversionService 的查找体系。

PrinterConverter(第 142-185 行):

public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
	if (!sourceType.isAssignableTo(this.printerObjectType)) {
		source = this.conversionService.convert(source, sourceType, this.printerObjectType);
	}
	if (source == null) {
		return "";
	}
	return this.printer.print(source, LocaleContextHolder.getLocale());
}

这就是 12.8.2 javadoc 说的"先强转再打印":字段类型不直接是 Printer 的 T 时,先借 ConversionService 把对象转成 Printer 能吃的类型(例如 BigDecimal 字段先转 Number),再 print。空值打印成空串而不是 "null"。Locale 从 LocaleContextHolder.getLocale() 取——这就是 Spring MVC 里 LocaleResolver 影响格式化输出的接线点。

ParserConverter(第 188-234 行):

public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
	String text = (String) source;
	if (!StringUtils.hasText(text)) {
		return null;
	}
	Object result;
	try {
		result = this.parser.parse(text, LocaleContextHolder.getLocale());
	}
	catch (IllegalArgumentException ex) {
		throw ex;
	}
	catch (Throwable ex) {
		throw new IllegalArgumentException("Parse attempt failed for value [" + text + "]", ex);
	}
	TypeDescriptor resultType = TypeDescriptor.valueOf(result.getClass());
	if (!resultType.isAssignableTo(targetType)) {
		result = this.conversionService.convert(result, resultType, targetType);
	}
	return result;
}

空串→null(与前面枚举/数字的空串约定一致);ParseException 等 Throwable 包装成 IllegalArgumentException("Parse attempt failed for value [x]")——这个异常后来会被 TypeConverterDelegate 一路翻成 TypeMismatchException 进 BindingResult;解析产物与目标字段类型不符时再借 ConversionService 强转一次(Parser 返回 Long、字段是 AtomicLong 这类场景)。

12.8.4 注解桥接:AnnotationPrinterConverter / AnnotationParserConverter

addFormatterForFieldAnnotation(第 100-111 行):

public void addFormatterForFieldAnnotation(AnnotationFormatterFactory<? extends Annotation> annotationFormatterFactory) {
	Class<? extends Annotation> annotationType = getAnnotationType(annotationFormatterFactory);
	if (this.embeddedValueResolver != null &&
			annotationFormatterFactory instanceof EmbeddedValueResolverAware embeddedValueResolverAware) {
		embeddedValueResolverAware.setEmbeddedValueResolver(this.embeddedValueResolver);
	}
	Set<Class<?>> fieldTypes = annotationFormatterFactory.getFieldTypes();
	for (Class<?> fieldType : fieldTypes) {
		addConverter(new AnnotationPrinterConverter(annotationType, annotationFormatterFactory, fieldType));
		addConverter(new AnnotationParserConverter(annotationType, annotationFormatterFactory, fieldType));
	}
}

三步走:① 反射解析工厂的泛型参数拿到注解类型 A;② 若工厂自己实现了 EmbeddedValueResolverAware(NumberFormatAnnotationFormatterFactory 就是),把容器的占位符解析器塞给它——这是 @NumberFormat(pattern = "${fmt.pattern}") 能生效的接线;③ 对工厂声明的每个字段类型,注册一对条件转换器。

AnnotationParserConverter(第 291-342 行)是"@DateTimeFormat 生效"的核心:

@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
	return targetType.hasAnnotation(this.annotationType);
}

@Override
public @SuppressWarnings("unchecked")
public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
	Annotation ann = targetType.getAnnotation(this.annotationType);
	if (ann == null) {
		throw new IllegalStateException(
				"Expected [" + this.annotationType.getName() + "] to be present on " + targetType);
	}
	AnnotationConverterKey converterKey = new AnnotationConverterKey(ann, targetType.getObjectType());
	GenericConverter converter = cachedParsers.get(converterKey);
	if (converter == null) {
		Parser<?> parser = this.annotationFormatterFactory.getParser(
				converterKey.getAnnotation(), converterKey.getFieldType());
		converter = new ParserConverter(this.fieldType, parser, FormattingConversionService.this);
		cachedParsers.put(converterKey, converter);
	}
	return converter.convert(source, sourceType, targetType);
}
  • matches:目标字段描述符带不带这个注解——带,才轮到它解析。这就是 ConditionalConverter 教科书式的用法:同一个 String→LocalDate 类型对,普通路径走 ObjectToObjectConverter/Temporal 解析,带 @DateTimeFormat 的字段走这条注解路径。
  • convert:从 targetType 现场取出注解实例(getAnnotation),以"注解实例 + 字段类型"为 key 查 cachedParsers——同一种注解配置只造一次 Parser,第二次起直接复用。未命中才调工厂的 getParser(ann, fieldType) 现场造 Parser,包成 ParserConverter 缓存后执行。
  • AnnotationPrinterConverter 镜像对称:matches 看的是 sourceType.hasAnnotation(打印方向,注解在字段上即源描述符携带),结果缓存进 cachedPrinters。
  • AnnotationConverterKey(第 345-374 行):equals 比 fieldType(==)+ annotation(annotation.equals)——同一字段类型下同值注解视为同一个 key,@DateTimeFormat(pattern="yyyy") 不同 pattern 的字段各自缓存一份 Parser。

12.8.5 getFieldType:泛型实参解析

static Class<?> getFieldType(T instance, Class<T> genericInterface) {
	Class<?> fieldType = GenericTypeResolver.resolveTypeArgument(instance.getClass(), genericInterface);
	if (fieldType == null && instance instanceof DecoratingProxy decoratingProxy) {
		fieldType = GenericTypeResolver.resolveTypeArgument(decoratingProxy.getDecoratedClass(), genericInterface);
	}
	Assert.notNull(fieldType, () -> "Unable to extract the parameterized field type from " +
				ClassUtils.getShortName(genericInterface) + " [" + instance.getClass().getName() +
				"]; does the class parameterize the <T> generic type?");
	return fieldType;
}

与 GenericConversionService.addConverter(Converter) 同款逻辑:Formatter 必须写成 class PhoneFormatter implements Formatter<Phone>,泛型实参解析不到(含 AOP 代理补救一次)就断言失败。这就是"自定义 Formatter 必须显式参数化"的来源。

12.9 源码剖析(七):DefaultFormattingConversionService 与 @NumberFormat/@DateTimeFormat

12.9.1 组装顺序

spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java 构造链(第 86-96 行):

public DefaultFormattingConversionService(
		@Nullable StringValueResolver embeddedValueResolver, boolean registerDefaultFormatters) {

	if (embeddedValueResolver != null) {
		setEmbeddedValueResolver(embeddedValueResolver);
	}
	DefaultConversionService.addDefaultConverters(this);
	if (registerDefaultFormatters) {
		addDefaultFormatters(this);
	}
}

注意顺序:先 addDefaultConverters(装全部内置转换器),再 addDefaultFormatters(装格式化器)。由于转换器注册是 addFirst(后注册排前),格式化器桥接出来的 Printer/Parser 在同类型对上天然优先于通用 Converter——例如 String→LocalDate,带注解的字段永远走 AnnotationParserConverter。

addDefaultFormatters(第 105-123 行):

public static void addDefaultFormatters(FormatterRegistry formatterRegistry) {
	// Default handling of number values
	formatterRegistry.addFormatterForFieldAnnotation(new NumberFormatAnnotationFormatterFactory());

	// Default handling of monetary values
	if (JSR_354_PRESENT) {
		formatterRegistry.addFormatter(new CurrencyUnitFormatter());
		formatterRegistry.addFormatter(new MonetaryAmountFormatter());
		formatterRegistry.addFormatterForFieldAnnotation(new Jsr354NumberFormatAnnotationFormatterFactory());
	}

	// Default handling of date-time values
	// just handling JSR-310 specific date and time types
	new DateTimeFormatterRegistrar().registerFormatters(formatterRegistry);
	// regular DateFormat-based Date, Calendar, Long converters
	new DateFormatterRegistrar().registerFormatters(formatterRegistry);
}

JSR_354_PRESENT 是静态块里 ClassUtils.isPresent("javax.money.MonetaryAmount", classLoader) 的探测结果——类路径上有 JSR-354 API 才注册货币格式化器,这是 Spring 一贯的"可选依赖按存在性装配"策略。

12.9.2 @NumberFormat 与 NumberFormatAnnotationFormatterFactory

注解本体(spring-context/.../format/annotation/NumberFormat.java):@Target({METHOD, FIELD, PARAMETER, ANNOTATION_TYPE})、@Retention(RUNTIME),两个属性 style()(DEFAULT/NUMBER/PERCENT/CURRENCY)与 pattern(),javadoc 明确 pattern 优先于 style。

工厂实现(spring-context/.../format/number/NumberFormatAnnotationFormatterFactory.java):

public class NumberFormatAnnotationFormatterFactory extends EmbeddedValueResolutionSupport
		implements AnnotationFormatterFactory<NumberFormat> {

	@Override
	public Set<Class<?>> getFieldTypes() {
		return NumberUtils.STANDARD_NUMBER_TYPES;
	}

	@Override
	public Printer<Number> getPrinter(NumberFormat annotation, Class<?> fieldType) {
		return configureFormatterFrom(annotation);
	}

	@Override
	public Parser<Number> getParser(NumberFormat annotation, Class<?> fieldType) {
		return configureFormatterFrom(annotation);
	}

	private Formatter<Number> configureFormatterFrom(NumberFormat annotation) {
		String pattern = resolveEmbeddedValue(annotation.pattern());
		if (StringUtils.hasLength(pattern)) {
			return new NumberStyleFormatter(pattern);
		}
		else {
			Style style = annotation.style();
			if (style == Style.CURRENCY) {
				return new CurrencyStyleFormatter();
			}
			else if (style == Style.PERCENT) {
				return new PercentStyleFormatter();
			}
			else {
				return new NumberStyleFormatter();
			}
		}
	}
}
  • getFieldTypes() 直接复用 NumberUtils.STANDARD_NUMBER_TYPES——Byte/Short/Integer/Long/Float/Double/BigDecimal/BigInteger 这组标准数字类型都可以挂 @NumberFormat。
  • getPrinter/getParser 同一个 configureFormatterFrom——数字格式化是双向对称的。
  • resolveEmbeddedValue(annotation.pattern()):继承 EmbeddedValueResolutionSupport,pattern 里的 ${...} 占位符先解析再用于建格式化器——配合 12.8.4 讲的 setEmbeddedValueResolver 注入。
  • 三分支:有 pattern → NumberStyleFormatter(pattern);style=CURRENCY → CurrencyStyleFormatter;PERCENT → PercentStyleFormatter;否则默认 NumberStyleFormatter()。

AbstractNumberFormatter(number 包)内部包 java.text.NumberFormat,parse 走 NumberFormat.parse,print 走 format——Locale 敏感。

12.9.3 @DateTimeFormat 的双工厂分发

@DateTimeFormat 注解(spring-context/.../format/annotation/DateTimeFormat.java)属性:style()(默认 "SS")、iso()(NONE/DATE/TIME/DATE_TIME)、pattern()、fallbackPatterns()。javadoc 有一条对 JDK 20+ 的警告:style 模式依赖 locale-sensitive 模式串,JDK 20 起可能有不兼容变化,建议用 ISO 或自管 pattern,并给出 fallbackPatterns 作为兼容手段。

注解注册后由两个工厂分工处理(注意它们管的字段类型完全不重叠):

旧时代(spring-context/.../format/datetime/DateTimeFormatAnnotationFormatterFactory.java):

private static final Set<Class<?>> FIELD_TYPES = Set.of(Date.class, Calendar.class, Long.class);

三个字段类型:java.util.Date、Calendar、Long。getFormatter 建一个 DateFormatter,把注解的 iso/style/pattern/fallbackPatterns 逐个设进去(style 与 pattern 同样过 resolveEmbeddedValue)。

JSR-310 新时代(spring-context/.../format/datetime/standard/Jsr310DateTimeFormatAnnotationFormatterFactory.java):

private static final Set<Class<?>> FIELD_TYPES = Set.of(
		Instant.class,
		LocalDate.class,
		LocalTime.class,
		LocalDateTime.class,
		ZonedDateTime.class,
		OffsetDateTime.class,
		OffsetTime.class,
		YearMonth.class,
		MonthDay.class);

九个 java.time 类型。getPrinter 有个性能细节:

DateTimeFormatter formatter = getFormatter(annotation, fieldType);
// Efficient ISO_LOCAL_* variants for printing since they are twice as fast...
if (formatter == DateTimeFormatter.ISO_DATE) {
	if (isLocal(fieldType)) {
		formatter = DateTimeFormatter.ISO_LOCAL_DATE;
	}
}
……
return new TemporalAccessorPrinter(formatter);

本地时间类型打印时,把 ISO_DATE/TIME/DATE_TIME 换成不带时区处理的 ISO_LOCAL_* 变体——注释说快一倍。getParser 则把 fallbackPatterns 解析后连同 formatter 交给 TemporalAccessorParser(fieldType, formatter, fallbackPatterns, annotation):主 pattern 解析失败时按顺序试兜底 pattern。

12.9.4 DateTimeFormatterRegistrar:JSR-310 的默认注册员

spring-context/.../format/datetime/standard/DateTimeFormatterRegistrar.java 实现 FormatterRegistrar(FormatterRegistrar 接口只有一个方法 registerFormatters(FormatterRegistry),是"一组格式化器的自描述工厂"——DefaultFormattingConversionService 直接 new 出来调它)。registerFormatters(第 156-202 行)注册内容:

public void registerFormatters(FormatterRegistry registry) {
	DateTimeConverters.registerConverters(registry);

	DateTimeFormatter df = getFormatter(Type.DATE);
	DateTimeFormatter tf = getFormatter(Type.TIME);
	DateTimeFormatter dtf = getFormatter(Type.DATE_TIME);

	registry.addFormatterForFieldType(LocalDate.class,
			new TemporalAccessorPrinter(
					df == DateTimeFormatter.ISO_DATE ? DateTimeFormatter.ISO_LOCAL_DATE : df),
			new TemporalAccessorParser(LocalDate.class, df));
	// LocalTime / LocalDateTime / ZonedDateTime / OffsetDateTime / OffsetTime 同款……

	registry.addFormatterForFieldType(Instant.class, new InstantFormatter());
	registry.addFormatterForFieldType(Period.class, new PeriodFormatter());
	registry.addFormatterForFieldType(Duration.class, new DurationFormatter());
	registry.addFormatterForFieldType(Year.class, new YearFormatter());
	registry.addFormatterForFieldType(Month.class, new MonthFormatter());
	registry.addFormatterForFieldType(YearMonth.class, new YearMonthFormatter());
	registry.addFormatterForFieldType(MonthDay.class, new MonthDayFormatter());

	registry.addFormatterForFieldAnnotation(new Jsr310DateTimeFormatAnnotationFormatterFactory());
	registry.addFormatterForFieldAnnotation(new DurationFormatAnnotationFormatterFactory());
}

要点:

  • DateTimeConverters.registerConverters(registry) 先补一批 JSR-310 类型之间的双向转换器(如 LocalDate→Date 之类桥接)。
  • 默认格式化风格由 getFallbackFormatter 决定:DATE/TIME/DATE_TIME 全部 FormatStyle.SHORT——即默认用当前 Locale 的短格式解析 LocalDate 等。用户可以通过 setter(setDateStyle/setUseIsoFormat/setDateFormatter 等)覆盖,这就是"全局统一日期格式"的配置入口。
  • 末尾注册两个注解工厂:@DateTimeFormat(JSR-310 九个类型)与 @DurationFormat(Duration/Period)。

至此格式化层全貌闭合:DefaultFormattingConversionService = DefaultConversionService 的全部转换器 + 数字注解工厂 + 货币格式化器(可选)+ JSR-310 默认格式化器 + DateFormatterRegistrar(老 Date/Calendar 的 SHORT 风格默认格式化)。

12.10 源码剖析(八):容器中的集成点

12.10.1 容器如何拿到 ConversionService:finishBeanFactoryInitialization

spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java 第 953-958 行:

// Initialize conversion service for this context.
if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
		beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
	beanFactory.setConversionService(
			beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
}

这是容器级装配的唯一入口,发生在 finishBeanFactoryInitialization 最早期(所有单例 bean 创建之前)。判定条件两条:① 容器里存在名为 conversionService(CONVERSION_SERVICE_BEAN_NAME)的 bean;② 它的类型确实是 ConversionService。满足就把它取出来塞进 BeanFactory。命名是契约:你自定义的 ConversionService 必须起名叫 conversionService——随便起名的 ConversionService bean 容器看不到,这是初学者最常踩的坑。

注意这里没有什么 ConversionServiceBeanFactoryPostProcessor——任务提纲里的这个猜测名在 7.0.9 不存在,机制就是这段直截了当的 bean 名探测。

12.10.2 AbstractBeanFactory:BeanFactory 持有的 ConversionService

spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanFactory.java:

/** Spring ConversionService to use instead of PropertyEditors. */
private @Nullable ConversionService conversionService;   // 第 134 行

public void setConversionService(@Nullable ConversionService conversionService) {  // 第 867 行
	this.conversionService = conversionService;
}

它有两个消费点。其一,getTypeConverter()(第 927-939 行):

public TypeConverter getTypeConverter() {
	TypeConverter customConverter = getCustomTypeConverter();
	if (customConverter != null) {
		return customConverter;
	}
	else {
		// Build default TypeConverter, registering custom editors.
		SimpleTypeConverter typeConverter = new SimpleTypeConverter();
		typeConverter.setConversionService(getConversionService());
		registerCustomEditors(typeConverter);
		return typeConverter;
	}
}

BeanFactory 需要临时转个值(比如解析 @Value、查 isTypeMatch)时,就 new 一个 SimpleTypeConverter,把容器的 ConversionService 塞进去。其二,prepareBeanWrapper 路径(第 1297 行 bw.setConversionService(getConversionService()))——给正在装配的 bean 的 BeanWrapper 注入同一个 ConversionService。于是链条闭合:容器级 ConversionService → 每个 BeanWrapper 的 TypeConverterDelegate → 每次 convertIfNecessary 的第 2 段。

12.10.3 BeanWrapperImpl:属性填充时的转换

BeanWrapperImpl(spring-beans/.../BeanWrapperImpl.java)构造时 this.typeConverterDelegate = new TypeConverterDelegate(this, this.wrappedObject)(第 137 行)——把自己(注册表)和目标 bean 一起交给 delegate。真正赋值走父类 AbstractNestablePropertyAccessor,其中第 298/324/357/365 行都调 convertIfNecessary(...),最终收拢到第 580-586 行:

private @Nullable Object convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue,
		Object newValue, @Nullable Class<?> requiredType, @Nullable TypeDescriptor td) {
	……
	return this.typeConverterDelegate.convertIfNecessary(propertyName, oldValue, newValue, requiredType, td);
}

即 12.4 讲的那个七段方法。AbstractAutowireCapableBeanFactory.applyPropertyValues(spring-beans/.../support/AbstractAutowireCapableBeanFactory.java 第 1660 行起)做的事就是:遍历 PropertyValues,通过 bw.setPropertyValues(mpvs) 把每个属性值交给 BeanWrapper,而 BeanWrapper 在 setPropertyValue 内部对每个属性调上面这个转换。属性值里的 SpEL、${} 占位符在更早的阶段(BeanDefinitionValueResolver/embeddedValueResolvers)已经解析成具体对象,这里拿到的基本就是"字符串或已解析对象"——转换层不再管占位符,只管类型。

12.10.4 ConversionServiceFactoryBean:XML 时代的配置入口

spring-context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java:

public class ConversionServiceFactoryBean implements FactoryBean<ConversionService>, InitializingBean {

	private @Nullable Set<?> converters;

	private @Nullable GenericConversionService conversionService;

	public void setConverters(Set<?> converters) {
		this.converters = converters;
	}

	@Override
	public void afterPropertiesSet() {
		this.conversionService = createConversionService();
		ConversionServiceFactory.registerConverters(this.converters, this.conversionService);
	}

	protected GenericConversionService createConversionService() {
		return new DefaultConversionService();
	}
}
  • 默认产物是 DefaultConversionService(全内置转换器),子类可覆盖 createConversionService 换成 DefaultFormattingConversionService。
  • setConverters 收一个集合,元素可以是 Converter、ConverterFactory、GenericConverter 三者之一——ConversionServiceFactory.registerConverters(spring-core/.../support/ConversionServiceFactory.java)按 instanceof 分发注册:
public static void registerConverters(@Nullable Set<?> converters, ConverterRegistry registry) {
	if (converters != null) {
		for (Object candidate : converters) {
			if (candidate instanceof GenericConverter genericConverter) {
				registry.addConverter(genericConverter);
			}
			else if (candidate instanceof Converter<?, ?> converter) {
				registry.addConverter(converter);
			}
			else if (candidate instanceof ConverterFactory<?, ?> converterFactory) {
				registry.addConverterFactory(converterFactory);
			}
			else {
				throw new IllegalArgumentException("Each converter object must implement one of the " +
						"Converter, ConverterFactory, or GenericConverter interfaces");
			}
		}
	}
}

注意它注册出来的 bean 默认 singleton、bean 名若为 conversionService,就会被 12.10.1 的探测接住。@Configuration 风格下不需要它:直接 @Bean public ConversionService conversionService() { … } 返回一个配好的 DefaultFormattingConversionService 即可——javadoc 明说了这一点。也请注意:框架层并不会自动收集容器里所有 Converter 类型的 bean;"@Component 实现 Converter 就自动注册"是 Spring Boot 的 ConversionService 后置收集逻辑,不是本框架的行为。纯框架里自定义转换器要么显式 addConverter,要么通过 ConversionServiceFactoryBean.setConverters 列举。

12.10.5 DataBinder:Web 数据绑定的转换整合

spring-context/src/main/java/org/springframework/validation/DataBinder.java 第 130 行 public class DataBinder implements PropertyEditorRegistry, TypeConverter——它自己就实现了转换两接口,内部委托给一个 ExtendedTypeConverter(静态内部类,第 1468-1469 行:extends SimpleTypeConverter implements PropertyEditorRegistrar)。懒创建(第 381-388 行):

protected SimpleTypeConverter getSimpleTypeConverter() {
	if (this.typeConverter == null) {
		this.typeConverter = new ExtendedTypeConverter();
		this.typeConverter.setConversionService(this.conversionService);
	}
	return this.typeConverter;
}

绑定主链:

public void bind(PropertyValues pvs) {                    // 第 1213 行
	if (shouldNotBindPropertyValues()) {
		return;
	}
	MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues mutablePropertyValues ?
			mutablePropertyValues : new MutablePropertyValues(pvs));
	doBind(mpvs);
}

protected void doBind(MutablePropertyValues mpvs) {        // 第 1241 行
	checkAllowedFields(mpvs);
	checkRequiredFields(mpvs);
	applyPropertyValues(mpvs);
}

protected void applyPropertyValues(MutablePropertyValues mpvs) {   // 第 1355 行
	try {
		// Bind request parameters onto target object.
		getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields());
	}
	catch (PropertyBatchUpdateException ex) {
		// Use bind error processor to create FieldErrors.
		for (PropertyAccessException pae : ex.getPropertyAccessExceptions()) {
			getBindingErrorProcessor().processPropertyAccessException(pae, getInternalBindingResult());
		}
	}
	……
}

链条:请求参数 → PropertyValues → checkAllowedFields(剥掉不在白名单的字段,防路径穿越与越权绑定)→ checkRequiredFields(缺失必填字段记 FieldError)→ getPropertyAccessor().setPropertyValues——这个 accessor 就是包着目标对象的 BeanWrapper,于是每个属性值再次走进 TypeConverterDelegate.convertIfNecessary。转换抛出来的 TypeMismatchException 被 PropertyBatchUpdateException 聚合,再由 BindingErrorProcessor 逐个翻译成 BindingResult 里的 FieldError——类型转换失败在 Web 层不是异常抛出,而是错误收集,这就是表单回显"输入 4.5 到 age 字段时页面提示 typeMismatch"的完整链路。

setConversionService(第 749-750 行)有一句守卫:Assert.state(this.conversionService == null, "DataBinder is already initialized with ConversionService")——DataBinder 一旦完成初始化就不许换 ConversionService,避免绑定制程中途策略突变。WebDataBinder(spring-web 包)在此之上做 Web 相关扩展,细节归 Web 章,本章只需要知道:MVC 的参数绑定最终也落在同一个 DataBinder → BeanWrapperImpl → TypeConverterDelegate 链上。

12.11 运行机制:一次"字符串属性值 → bean 属性对象"的完整旅程

把前面所有源码串成一条时序。以 XML 里 <property name="age" value="42"/> 装配 User.age: int 为例:

容器启动
  AbstractApplicationContext.refresh()
    └─ finishBeanFactoryInitialization(beanFactory)
         ├─ 探测名为 conversionService 的 bean(类型匹配 ConversionService)
         │    └─ beanFactory.setConversionService(cs)        ← 12.10.1
         └─(随后开始创建所有非懒加载单例)

创建 user bean
  AbstractAutowireCapableBeanFactory.doCreateBean
    └─ populateBean(beanName, mbd, bw)
         ├─(bw = BeanWrapperImpl,构造时 new TypeConverterDelegate(this, user))
         ├─ prepareBeanWrapper: bw.setConversionService(beanFactory.getConversionService())  ← 12.10.2
         └─ applyPropertyValues("age"="42", …)
              └─ bw.setPropertyValues(mpvs)
                   └─ AbstractNestablePropertyAccessor.setPropertyValue → setPropertyValue(BeanWrapperImpl#)
                        └─ convertIfNecessary("age", null, "42", int.class, td)   ← AbstractNestablePropertyAccessor:580
                             └─ TypeConverterDelegate.convertIfNecessary(...)      ← 12.4
                                  ① findCustomEditor(int, "age") → null
                                  ② cs != null → canConvert(String, int) → true
                                  │    └─ GenericConversionService.convert:
                                  │         getConverter(String,int):
                                  │           converterCache 未命中 → converters.find
                                  │           (层次遍历:String→Integer? …→String→Number?)
                                  │           命中 StringToNumberConverterFactory 派生的 StringToNumber(Integer)
                                  │           (首次)写入 converterCache
                                  │         invokeConverter → NumberUtils.parseNumber("42", Integer) → 42
                                  ③ return 42
                             └─ bw.setProperty("age", 42)(内省写方法,反射 setAge(42))

关键时序结论:

  1. 占位符先于类型转换。"42" 若写成 "${user.age}",解析发生在更早:AbstractBeanFactory.embeddedValueResolvers(环境 PropertySources)在属性值进入 BeanWrapper 之前就把它解成字符串 42。顺序永远是:占位符/SpEL 求值 → 类型转换。所以 @Value("${timeout:1000}") 注入 Duration 时,先拿到字符串 "1000",再由 TypeConverterDelegate 走 ConversionService 转成 Duration。
  2. 第一次转换慢、后续快。converterCache 在第一次 canConvert/convert 同类型对时填充;之后 String→int 这类热路径只是一次 ConcurrentReferenceHashMap.get。
  3. Editor 与 ConversionService 是双通道互备,不是互斥。conversionAttemptEx 的捕获与重放(12.4.3 第 2/6/7 段)保证了:ConversionService 失败时 PropertyEditor 还能救场,两边都失败才算失败。
  4. Web 层同一链路。请求参数字符串 → DataBinder.bind → doBind → applyPropertyValues → 同一个 BeanWrapper → 同一个 convertIfNecessary;区别仅在"异常不抛出而进 BindingResult",以及 DataBinder 自己挂的 ConversionService 是 WebDataBinder 初始化时从容器拿到的那一个。

12.12 扩展用法与最佳实践

12.12.1 三种自定义转换器写法与选型

写法一:Converter<S,T>(首选,90% 场景)

public class StringToOrderConverter implements Converter<String, Order> {
	private final OrderRepository repo;
	public StringToOrderConverter(OrderRepository repo) { this.repo = repo; }
	@Override public Order convert(String source) { return repo.findById(source).orElseThrow(); }
}

单类型对、无字段上下文需求时用它。注意泛型实参必须写在 implements 处——注册时靠反射解析。

写法二:ConverterFactory<S,R>(一族转换)

当你要为同一个源类型适配一组目标子类型时(如 String→Duration、String→Period、String→Instant 共享一套解析逻辑),实现 ConverterFactory<String, Temporal>,在 getConverter(targetType) 里按目标类型分派。

写法三:ConditionalGenericConverter(需要看字段上下文)

public class MyConditionalConverter implements ConditionalGenericConverter {
	public Set<ConvertiblePair> getConvertibleTypes() {
		return Collections.singleton(new ConvertiblePair(String.class, BigDecimal.class));
	}
	public boolean matches(TypeDescriptor source, TypeDescriptor target) {
		return target.hasAnnotation(MyMoney.class);   // 只接带注解的字段
	}
	public Object convert(Object source, TypeDescriptor s, TypeDescriptor t) { … }
}

凡是"同一类型对、不同字段要不同行为"的(注解驱动、泛型元素类型驱动),都走这条。FormattingConversionService 的注解转换器就是范例。

选型口诀:Converter 不够用 → ConverterFactory;需要字段上下文 → ConditionalGenericConverter;需要 Locale/文本格式 → 直接写 Formatter<T> 并注册成 FormatterRegistry(打印/解析双向白得)。

12.12.2 注册方式(框架层,非 Boot)

纯框架下,转换器不会因为实现了接口就自动进 ConversionService。三条正规路径:

  1. @Bean 方法显式组装:
@Bean
public ConversionService conversionService(OrderRepository repo) {
	DefaultFormattingConversionService cs = new DefaultFormattingConversionService();
	cs.addConverter(new StringToOrderConverter(repo));
	cs.addFormatterForFieldType(Phone.class, new PhoneFormatter());
	return cs;
}

bean 名必须是 conversionService(12.10.1 的契约)。返回 DefaultFormattingConversionService 可同时获得内置转换器 + 格式化支持;只要 DefaultConversionService 就没有 @NumberFormat 能力。 2. XML:<bean id="conversionService" class="...ConversionServiceFactoryBean"><property name="converters"><set>…</set></property></bean>。 3. 编程式临时使用:DefaultConversionService.getSharedInstance()(兜底场景,javadoc 明确不推荐定制)。

12.12.3 性能与线程安全

  • GenericConversionService 的注册结构(ConcurrentHashMap/ConcurrentLinkedDeque/ConcurrentReferenceHashMap/CopyOnWriteArraySet)与缓存都为并发读设计——注册完成后,convert 调用是线程安全的。转换器实现本身也被接口契约要求无状态、可共享。
  • 热路径成本:命中 converterCache ≈ 一次哈希查找;未命中要做源/目标类型层次笛卡尔积遍历(find)。层次平均十几层,未命中开销不可小觑——所以缓存设计成"失败也缓存(NO_MATCH)"。
  • 运行期不要注册转换器:addConverter 会 converterCache.clear(),高并发下全缓存重建有毛刺。启动期注册完就别动。
  • ObjectToObjectConverter 的 conversionExecutableCache、FormattingConversionService 的 cachedPrinters/cachedParsers 都是"按目标类/注解配置"的缓存——转换器/格式化器一旦注册,运行期反射探查成本只付一次。
  • PropertyEditor 一侧相反:BeanWrapperImpl 每 bean 一个,不可跨线程共享——这是体系代差,不是设计失误。

12.12.4 Formatter 与 Converter 的选择

需要"值 ↔ 字符串"且与 Locale/注解格式相关(货币、百分比、日期时间、带格式的数字显示)→ Formatter;需要"任意两种对象类型"、无 Locale 语义(String→实体、Number→Number、List→List)→ Converter。混用没问题:DefaultFormattingConversionService 本来就是两者叠加。

12.13 常见问题与坑

12.13.1 Number 转换的溢出与精度

NumberToNumberConverterFactory 与 TypeConverterDelegate 第 4 段的数字规则都最终走 NumberUtils.convertNumberToTargetClass。源码行为:Long.MAX_VALUE 转 Integer 时截断抛 IllegalArgumentException("Overflow"),而 Double 4.5 转 Integer 是四舍五入还是取整?——以 NumberUtils 实现为准(doConvertNumberToTargetClass 用 xxxValue() 强转,即取整截断,小数部分丢弃不报错)。坑的本质:宽转窄、浮点转整型时的精度损失是静默的。建议:对金额/计数场景,字段直接声明 BigDecimal,让 NumberStyleFormatter/CustomNumberEditor 按字符串解析保住精度,不要走 Double 中转。

12.13.2 String → Enum 的名称匹配

StringToEnumConverterFactory.StringToEnum.convert(12.7.4 已贴):

public @Nullable T convert(String source) {
	if (source.isEmpty()) {
		return null;
	}
	return (T) Enum.valueOf(this.enumType, source.trim());
}

三个行为要点:① 严格按枚举常量名匹配(Enum.valueOf),大小写敏感——"red" 匹配不到 RED;② 匹配前 trim(),首尾空格容忍;③ 空串直接 null,不抛异常。而在 ConversionService 之前的 TypeConverterDelegate 第 4 段还有一条 PropertyEditor 时代的枚举路径:空串→null、非空串→attemptToConvertStringToEnum(先全限定名后 getField)——两条路径对空串语义一致(null),但对非法值:Converter 路径抛 IllegalArgumentException(被包成 ConversionFailedException),Editor 路径 trace 日志后保持原值再由最终校验抛错。另外注意 ConversionUtils.getEnumType:targetType 是枚举子类($1 匿名子类)时向上找最近的 enum 父类——这就是 enum Color { RED } 写成 Color.RED 后反射类型是匿名子类仍能正确转换的原因。

12.13.3 泛型擦除与 TypeDescriptor 的必要性

最经典的现场:

List<Integer> ids = bean.getIds();          // 字段
// 注入:@Value("1,2,3") List<Long> ids

没有泛型信息时,运行期拿到的只是 List,转换器不知道元素该转成 Long。Spring 靠 TypeConverterSupport.convertIfNecessary(value, Long.class, methodParam) 把 MethodParameter 包成 TypeDescriptor,getElementTypeDescriptor() 解出 Long,StringToCollectionConverter/CollectionToCollectionConverter 再逐元素递归转换。坑:直接调 typeConverter.convertIfNecessary(value, List.class)(只传 Class)会导致元素不被转换——List 里还是 String。这是编程式使用 TypeConverter 时最隐蔽的 bug:必须传 methodParam/field/typeDescriptor 重载。

12.13.4 ObjectToObjectConverter 的反射桥副作用

它会静默地按约定找 toXxx()/静态 valueOf/of/from/单参构造器(12.7.6)。副作用场景:你的类恰好看似有 public Foo(String) 构造器——任何 String→Foo 都会被它接住并反射调用,包括你不希望自动发生的转换。例如构造器有副作用(建连、查表),属性绑定时就会触发。排查方法:异常消息里那句 "No to… method exists…" 或打开 DEBUG 日志看 GenericConversionService.toString() 列出的全部已注册转换器。另外它明确拒绝 String 方向(两处 String.class == targetClass → null),对象→字符串由 FallbackObjectToStringConverter 负责——别指望它给你 toString。

12.13.5 null 的转换语义汇总

把散在各章的 null 规则收拢成一张表:

场景行为源码位置
source 为 null,目标 Optional返回 Optional.empty()GenericConversionService.convertNullSource / TypeConverterDelegate 第 5 段
source 为 null,目标其他类型返回 null同上
source 为 null,目标基本类型抛 ConversionFailedException("null cannot be assigned to a primitive")assertNotPrimitiveTargetType
空串 → 数字/布尔/字符/枚举返回 null(不抛错)StringToNumberConverterFactory、StringToBooleanConverter、StringToEnumConverterFactory
空串 → 注解格式化 Parser返回 nullParserConverter(!StringUtils.hasText)
sourceType 为 null 的 canConvert永远 trueGenericConversionService.canConvert

推论:int 字段收到空串,在 Web 绑定里最终是 FieldError 而不是 0;Integer 字段收到空串则静默变 null。

12.14 本章小结

Spring 的类型转换体系是三代技术叠出来的"夹心层":

  1. 执行引擎在 spring-beans:TypeConverterDelegate.convertIfNecessary 是唯一的总调度。它的优先级链——custom editor → ConversionService → 默认/JDK 约定编辑器 → 标准规则(数组/集合/Map/字符串构造器/枚举/数字)→ 补试与异常收尾——保证了"两条通道互备、后注册优先、失败信息友好"。conversionAttemptEx 的捕获与重放是这套互备语义的精髓。
  2. 服务内核在 spring-core:GenericConversionService 用 ConvertiblePair→ConvertersForPair 注册表 + ConverterCacheKey→GenericConverter 结果缓存(含 NO_OP/NO_MATCH 哨兵)+ 类型层次笛卡尔积查找,把"注册时多态、运行期精确、热路径零成本"三个目标同时达成。7.0.9 已无 classCache,层次列表在缓存未命中时现场计算——这是与旧资料的重要差异。
  3. 格式化在 spring-context:FormattingConversionService 把 Printer/Parser/AnnotationFormatterFactory 全部桥接成 GenericConverter 塞进同一个注册表,@NumberFormat/@DateTimeFormat 靠 ConditionalGenericConverter.matches 在带注解字段上精确接管,且按"注解配置"缓存 Parser 实例。
  4. 容器集成靠命名契约:名为 conversionService 的 bean 在 finishBeanFactoryInitialization 早期被探测并注入 BeanFactory,再由 prepareBeanWrapper 与 getTypeConverter() 分发给每个 BeanWrapper 与临时转换器;DataBinder 在 Web 侧复用同一条链,把转换异常翻译成 BindingResult 的 FieldError。

扩展时记住三件事:转换器必须静态参数化泛型(否则注册即报错);框架层不自动收集 Converter bean,要显式注册且 bean 名为 conversionService;null/空串在数字、枚举、注解 Parser 三处统一约定为"静默转 null",基本类型除外。