Spring反序列化YAML时数组转为LinkedHashMap而非ArrayList的问题
问题场景
当将以下YAML配置中的props反序列化为泛型Map时:
props: array: - name: foo1 bar: bar1 - name: foo2 bar: bar2
出现不符合预期的行为:
props实例为LinkedHashMaparray被错误反序列化为LinkedHashMap,而非预期的List
但直接使用new ObjectMapper(new YAMLFactory())读取配置时,array会正确转为ArrayList;将属性映射到带有List类型array属性的自定义对象时,结果也符合预期。重写ObjectMapper Bean无法解决该问题,使用环境为Spring 5.3.21 + Java 17。
核心原因
Spring处理配置绑定(尤其是泛型类型)时,并非直接依赖Jackson的ObjectMapper,而是通过**Binder和ConversionService**体系完成。当目标类型是无具体泛型约束的Map时,Spring无法推断嵌套节点的容器类型,会默认用LinkedHashMap处理嵌套结构,这是设计行为而非Bug。而Jackson会直接根据YAML语法结构推断数组类型,因此二者行为存在差异。
解决方法
1. 自定义GenericConverter
实现GenericConverter,针对Map类型的转换逻辑做调整,识别出需要转为List的嵌套节点并处理:
import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.convert.TypeDescriptor; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; public class MapToListConverter implements GenericConverter { @Override public Set<ConvertiblePair> getConvertibleTypes() { return Set.of(new ConvertiblePair(Map.class, Map.class)); } @Override public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (!(source instanceof Map<?, ?> sourceMap)) { return source; } Map<String, Object> resultMap = new LinkedHashMap<>(sourceMap.size()); sourceMap.forEach((key, value) -> { // 可根据实际场景调整判断逻辑,比如判断值是否为嵌套Map且符合数组结构特征 if ("array".equals(key) && value instanceof Map<?, ?> nestedMap) { resultMap.put((String) key, new ArrayList<>(nestedMap.values())); } else { resultMap.put((String) key, value); } }); return resultMap; } }
将转换器注册到Spring的ConversionService:
import org.springframework.context.annotation.Configuration; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.web.bind.support.ConfigurableWebBindingInitializer; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import javax.annotation.PostConstruct; @Configuration public class ConverterRegisterConfig { private final RequestMappingHandlerAdapter handlerAdapter; public ConverterRegisterConfig(RequestMappingHandlerAdapter handlerAdapter) { this.handlerAdapter = handlerAdapter; } @PostConstruct public void registerConverter() { ConfigurableWebBindingInitializer initializer = (ConfigurableWebBindingInitializer) handlerAdapter.getWebBindingInitializer(); if (initializer != null && initializer.getConversionService() instanceof GenericConversionService conversionService) { conversionService.addConverter(new MapToListConverter()); } } }
2. 使用@ConfigurationProperties绑定明确类型
定义带有明确泛型的配置类,让Spring能准确推断类型:
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.List; import java.util.Map; @Component @ConfigurationProperties(prefix = "props") public class PropsConfig { private List<Map<String, String>> array; // getter、setter方法 public List<Map<String, String>> getArray() { return array; } public void setArray(List<Map<String, String>> array) { this.array = array; } }
这种方式无需自定义转换器,Spring会根据配置类的字段类型正确反序列化array为List。
3. 自定义配置绑定逻辑(进阶)
若需要全局调整泛型Map的绑定行为,可以重写ConfigurationPropertiesBindingPostProcessor,替换默认的Binder配置,指定嵌套数组结构的默认容器类型为ArrayList。此方式复杂度较高,需熟悉Spring配置绑定的底层逻辑,谨慎使用。
内容的提问来源于stack exchange,提问作者user2668735

