HTTP 消息转换器:JSON、XML 与内容协商

3 阅读31分钟

概述

在《参数解析与类型转换》一文中,我们深入探讨了 @RequestParam@ModelAttribute 如何通过 WebDataBinderConversionService,将 HTTP 请求参数从 String 转化为 Java 对象的属性。那条路径的核心是 “字段级别的绑定与转换”,它擅长处理 application/x-www-form-urlencoded 或 Query String 这种扁平化的键值对数据。

然而,在现代 RESTful 架构中,API 的交互主体是结构化的 请求体响应体,通常为 JSON 或 XML 等复杂格式。这条通路不再通过 WebDataBinder,而是由一套完全独立的机制——HttpMessageConverter——来承担。它是 HTTP 协议中的“字节流”世界与 Java “对象”世界之间的直接桥梁,负责在两种表示之间进行整体序列化与反序列化。

本文将深入解剖 HttpMessageConverter 接口、典型实现(MappingJackson2HttpMessageConverter 等)的原理,以及如何通过 ContentNegotiationManager 实现内容协商,最终为你揭示 @RequestBody@ResponseBody 注解背后的完整工作流程。

文章组织架构图

flowchart 
    subgraph S1 ["1. 消息转换器体系总览"]
        direction LR
        A["接口与契约"] --> B["默认转换器注册"]
        B --> C["遍历选择策略"]
    end

    subgraph S2 ["2-3. 核心转换器实现剖析"]
        direction LR
        D["JSON: MappingJackson2HttpMessageConverter"] --> E["XML: Jaxb2RootElementHttpMessageConverter"]
        E --> F["其他: String/Form/ByteArray"]
    end

    subgraph S3 ["4. 内容协商机制"]
        direction LR
        G["ContentNegotiationManager"] --> H["策略链解析"]
        H --> I["确定响应MediaType"]
    end

    subgraph S4 ["5. 完整读写流程"]
        direction LR
        J["@RequestBody 读取流程"] --> K["@ResponseBody 写入流程"]
    end

    subgraph S5 ["6-8. 实战与问题排查"]
        direction LR
        L["自定义转换器"] --> M["生产事故排查"]
        M --> N["面试高频题"]
    end

    S1 --> S2
    S2 --> S3
    S3 --> S4
    S4 --> S5

    classDef topic fill:#f8f9fa,stroke:#333,stroke-width:2px,rx:5,color:#333;
    class S1,S2,S3,S4,S5 topic;

架构图说明

  • 总览说明:本文 8 个模块严格按照认知路径递进。模块 1 建立对 HttpMessageConverter 接口、注册及选择机制的全局认知。模块 2 和 3 深入两种最核心的媒体类型实现——JSON 和 XML,剖析其内部运作。模块 4 讲解当有多个转换器都能写入时,内容协商如何决定最终输出格式。模块 5 将前三模块的知识串接,完整展示 @RequestBody@ResponseBody 的处理全流程。最后,模块 6 至 8 通过实战、事故和面试,将理论落地到工程实践。
  • 逐模块说明:模块 1-4 回答了“有哪些转换器、它们如何工作、以及如何选择一个”。模块 5 阐述“它们在一次请求生命周期中如何协作”。模块 6 提供了“如何扩展以支持自定义格式”的能力。模块 7 和 8 则聚焦于解决“为什么返回 HTTP 406/415 错误”等现实问题和应试需求。
  • 关键结论HttpMessageConverter 是 Spring MVC 实现 RESTful Web 服务的基石。理解其基于 canRead/canWrite 的遍历匹配逻辑,以及 ContentNegotiationManager 的内容协商流程,是诊断和解决“返回了错误的 Content-Type”或“HTTP 406/415 错误”等问题的关键所在。

1. HttpMessageConverter 体系总览:接口、注册与选择

HttpMessageConverter 是 Spring 定义的一个策略接口,其职责是在 HTTP 请求/响应体与 Java 对象之间进行互相转换。它不仅是 Spring MVC 的重要组成部分,也因其纯粹性被 Spring 的 RestTemplate 等客户端工具所复用。

1.1 接口设计剖析

HttpMessageConverter 接口是整个转换器体系的灵魂,它清晰地定义了四个核心方法,划分了“能力检测”和“执行操作”两个阶段。

// org.springframework.http.converter.HttpMessageConverter
public interface HttpMessageConverter<T> {

    /**
     * 判断此转换器是否能读取给定的类型,在支持的媒体类型列表中查找匹配。
     * @param clazz 要读取的Java类型
     * @param mediaType 请求的Content-Type,可为null
     * @return 如果可以读取,返回true
     */
    boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);

    /**
     * 判断此转换器是否能写入给定的类型,在支持的媒体类型列表中查找匹配。
     * @param clazz 要写入的Java类型
     * @param mediaType 请求的Accept或协商后的MediaType,可为null
     * @return 如果可以写入,返回true
     */
    boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

    /**
     * 返回此转换器支持的所有媒体类型。
     */
    List<MediaType> getSupportedMediaTypes();

    /**
     * 从HttpInputMessage读取对象。
     */
    T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException;

    /**
     * 将对象写入HttpOutputMessage。
     */
    void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException;
}

设计意图解读

  • 策略模式HttpMessageConverter 接口定义了一系列转换策略,每一种转换器代表处理特定媒体类型的算法。canRead/canWrite 是策略选择的标准。
  • 关注点分离:接口将 类型检查canRead/canWrite)与 实际转换read/write)分离。调用方先轮询判断哪个转换器可用,再执行操作,这是一种典型的 两阶段执行 模式,避免了在 read 方法中抛出大量“不支持”的异常。
  • MediaType 的关键角色:每个转换器都通过 getSupportedMediaTypes() 声明自己能处理的媒体类型集合。MediaType 类封装了 MIME 类型(如 text/html)、子类型(如 json)以及参数(如 charset=UTF-8),为类型匹配提供了标准的抽象。

1.2 默认转换器的注册

一个全新的 Spring MVC 应用,即便不添加任何配置,也天然拥有一组功能完备的 HttpMessageConverter。这块魔术发生在 WebMvcConfigurationSupport

// org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
protected final List<HttpMessageConverter<?>> getMessageConverters() {
    // ... 省略部分代码
    this.messageConverters = new ArrayList<>();
    // 1. 给子类(如EnableWebMvcConfiguration)自定义的机会
    configureMessageConverters(this.messageConverters);
    if (this.messageConverters.isEmpty()) {
        // 2. 若无自定义,则添加默认的转换器
        addDefaultHttpMessageConverters(this.messageConverters);
    }
    // 3. 给子类扩展的机会(不覆盖默认)
    extendMessageConverters(this.messageConverters);
    return this.messageConverters;
}
// org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
protected final void addDefaultHttpMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
    messageConverters.add(new ByteArrayHttpMessageConverter());
    messageConverters.add(new StringHttpMessageConverter());
    messageConverters.add(new ResourceHttpMessageConverter());
    // ... 省略其他Resource处理

    // 检查Classpath下是否存在相关库,动态注册
    if (jackson2Present) {
        messageConverters.add(new MappingJackson2HttpMessageConverter());
    }
    else if (gsonPresent) { /* ... */ }
    else if (jsonbPresent) { /* ... */ }

    if (jaxb2Present) {
        messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
    }
    // ... FormHttpMessageConverter 等
}

解读

  • 双重定制机会configureMessageConverters 用于完全替换默认列表,而 extendMessageConverters 用于在保留默认转换器的基础上进行增删,后者更为常用和安全。
  • 类路径探测:默认转换器的注册是动态的,依赖于 classpath 下是否存在特定的库。例如,只有引入了 jackson-databindMappingJackson2HttpMessageConverter 才会被加入。这保证了在没有依赖的情况下不会出错。

1.3 策略遍历与选择算法

当一个 @RequestBody 注解的控制器方法需要处理时,HttpMessageConverter 如何被选中?其核心算法在 AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters 方法中。

// org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
        Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
    // ...
    MediaType contentType = inputMessage.getHeaders().getContentType();
    Class<?> contextClass = (parameter != null ? parameter.getContainingClass() : null);
    Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);

    // 1. 遍历所有已注册的转换器
    for (HttpMessageConverter<?> converter : this.messageConverters) {
        // 2. 第一步:类型检查。转换器是否声明可以处理此Java类型?
        if (converter instanceof GenericHttpMessageConverter) {
            if (((GenericHttpMessageConverter<T>) converter).canRead(targetType, contextClass, contentType)) {
                // 3. 找到匹配的,执行读取并返回
                return ((GenericHttpMessageConverter<T>) converter).read(targetType, contextClass, inputMessage);
            }
        }
        else if (targetClass != null) {
            if (converter.canRead(targetClass, contentType)) {
                return ((HttpMessageConverter<T>) converter).read(targetClass, inputMessage);
            }
        }
    }

    // 4. 遍历完成,无匹配转换器,抛出异常
    throw new HttpMediaTypeNotSupportedException(contentType, ...);
}

设计原理映射: 这正是策略模式的完美体现。messageConverters 列表就是一组策略对象,遍历过程即是在运行时根据 targetType 和请求的 contentType 动态选择合适的策略。canRead/canWrite 方法构成了策略的“选择器”。这种设计使得添加对新媒体类型的支持极其容易,只需向列表中添加一个新的 HttpMessageConverter 策略即可,完全符合开闭原则

1.4 HttpMessageConverter 体系类图

classDiagram
    class HttpMessageConverter~T~ {
        <<interface>>
        +canRead(Class, MediaType) boolean
        +canWrite(Class, MediaType) boolean
        +getSupportedMediaTypes() List~MediaType~
        +read(Class~T~, HttpInputMessage) T
        +write(T, MediaType, HttpOutputMessage) void
    }

    class GenericHttpMessageConverter~T~ {
        <<interface>>
        +canRead(Type, Class, MediaType) boolean
        +canWrite(Type, Class, MediaType) boolean
        +read(Type, Class, HttpInputMessage) T
        +write(T, Type, MediaType, HttpOutputMessage) void
    }

    class AbstractHttpMessageConverter~T~ {
        <<abstract>>
        +read(Class, HttpInputMessage) T
        +write(T, MediaType, HttpOutputMessage) void
        +canRead(Class, MediaType) boolean
        +canWrite(Class, MediaType) boolean
        #readInternal(Class, HttpInputMessage) T*
        #writeInternal(T, HttpOutputMessage) void*
        #supports(Class) boolean*
    }

    class AbstractGenericHttpMessageConverter~T~ {
        <<abstract>>
        #supports(Type, Class) boolean
    }

    class MappingJackson2HttpMessageConverter {
        -ObjectMapper objectMapper
        +read(Type, Class, HttpInputMessage) T
        +write(T, Type, MediaType, HttpOutputMessage) void
    }

    class Jaxb2RootElementHttpMessageConverter {
        +supports(Class) boolean
    }

    class StringHttpMessageConverter {
        +supports(Class) boolean
    }

    class FormHttpMessageConverter {
        +read(Class, HttpInputMessage) MultiValueMap
        +write(MultiValueMap, MediaType, HttpOutputMessage) void
    }

    HttpMessageConverter <|-- GenericHttpMessageConverter
    HttpMessageConverter <|.. AbstractHttpMessageConverter
    GenericHttpMessageConverter <|.. AbstractGenericHttpMessageConverter

    AbstractHttpMessageConverter <|-- StringHttpMessageConverter
    AbstractHttpMessageConverter <|-- Jaxb2RootElementHttpMessageConverter
    AbstractHttpMessageConverter <|-- FormHttpMessageConverter
    
    AbstractGenericHttpMessageConverter <|-- MappingJackson2HttpMessageConverter
  • 图表主旨概括:此图展示了 HttpMessageConverter 核心接口及其关键实现类的层次关系,揭示了模板方法模式的应用。
  • 逐层/逐元素分解
    • 接口层HttpMessageConverter 是基础接口;GenericHttpMessageConverter 扩展了它,以支持 Type 级别的转换(如带泛型的 List<User>),而不仅仅是 Class。Jackson 需要这种能力。
    • 抽象实现层AbstractHttpMessageConverterAbstractGenericHttpMessageConverter 为两个接口提供了骨架实现。它们实现了 read/write/canRead/canWrite 的模板逻辑,并将核心差异点抽象为 readInternalwriteInternalsupports
    • 具体实现层MappingJackson2HttpMessageConverter 处理 JSON,Jaxb2RootElementHttpMessageConverter 处理 XML,StringHttpMessageConverter 处理文本,FormHttpMessageConverter 处理表单。
  • 设计原理映射:这里应用了模板方法模式AbstractHttpMessageConverter 定义了转换流程的骨架(如读写前的日志、资源清理),而将具体的媒体类型转换逻辑委托给子类实现的 readInternal/writeInternal 等。同时,整个转换器体系是策略模式的实例,每个具体实现类都是一个可插拔的策略。
  • 工程联系与关键结论:在实际调试中,当你发现请求体没有被正确解析时,首先应该检查 contentType 头,然后通过断点或在 supports 方法中检查,看看是哪个转换器被遍历到并成功匹配。理解这套类体系,是精准定位消息转换问题的第一步。

2. JSON 消息转换器深度剖析

在现代微服务架构中,JSON 是当之无愧的数据交换格式之王。MappingJackson2HttpMessageConverter 作为 Spring MVC 对 Jackson 库的集成,是使用频率最高的转换器。它继承自 AbstractGenericHttpMessageConverter,充分利用了其对泛型的支持。

2.1 支持的媒体类型

// org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
public class MappingJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> {

    public MappingJackson2HttpMessageConverter() {
        this(Jackson2ObjectMapperBuilder.json().build());
    }

    public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {
        super(objectMapper, 
              MediaType.APPLICATION_JSON, // application/json
              new MediaType("application", "*+json")); // application/*+json
    }
    // ...
}

解读

  • application/json:标准的 JSON 媒体类型。
  • application/*+json:这是一个关键设计,使得此转换器不仅可以处理 application/json,还能处理任何后缀为 +json 的自定义媒体类型,例如专门用于版本化的 application/vnd.myapp.v1+json。这为 API 版本控制提供了原生支持。

2.2 反序列化:输入流到对象的桥梁

read 方法被调用时,最终会委托给 Jackson 的 ObjectMapper

// org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter
@Override
public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
        throws IOException, HttpMessageNotReadableException {

    JavaType javaType = getJavaType(type, contextClass);
    return readJavaType(javaType, inputMessage);
}

private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
    MediaType contentType = inputMessage.getHeaders().getContentType();
    Charset charset = getCharset(contentType);

    ObjectReader reader = this.objectMapper.readerFor(javaType);
    // 根据contentType应用特定配置,如字符集
    if (charset != null) {
        reader = reader.with(Charset.forName(charset.name()));
    }
    // 核心:将输入流反序列化为指定类型的Java对象
    return reader.readValue(inputMessage.getBody());
}

解读

  1. 类型转换: 方法首先通过 getJavaType 将 Java 的 Type 转换为 Jackson 内部的 JavaType,这一步能保留完整的泛型信息,是准确反序列化 List<User> 这类泛型集合的基础。
  2. 读取器构建objectMapper.readerFor(javaType) 创建了一个针对目标类型的反序列化读取器 ObjectReader。这是一个线程安全可配置的对象。
  3. 流式处理: 最终调用 reader.readValue(inputMessage.getBody()),直接从 HttpInputMessage 提供的输入流中读取并解析 JSON,避免了将整个请求体加载到内存字符串中,这种流式操作对内存非常友好。

2.3 序列化:对象到输出流的桥梁

// org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter
@Override
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
        throws IOException, HttpMessageNotWritableException {

    MediaType contentType = outputMessage.getHeaders().getContentType();
    JsonEncoding encoding = getJsonEncoding(contentType);

    // 1. 为特定类型构建写入器
    ObjectWriter writer = (type != null ? 
                           this.objectMapper.writerFor(this.objectMapper.constructType(type)) :
                           this.objectMapper.writer());
    // 2. 应用配置,如 pretty printing
    if (this.jsonPrefix != null) {
        writer = writer.withAttribute(...);
    }
    // 3. 生成JsonGenerator并写入
    try (JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding)) {
        writer.writeValue(generator, object);
        generator.flush();
    }
    catch (InvalidDefinitionException ex) {
        throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
    }
    // ...
}

解读

  1. ObjectWriter 的构建:类似读取时的 ObjectReaderObjectWriter 也是线程安全的。通过 objectMapper.writerFor(...) 创建,可以绑定特定的类型和配置(如视图、过滤器)。
  2. 生成器模式objectMapper.getFactory().createGenerator(...) 创建了一个 JsonGenerator,它负责将事件序列(通常是开始对象、字段名、值等)写到底层的输出流中。这种方法非常高效。
  3. InvalidDefinitionException 处理:当要序列化的对象定义有问题时(例如,循环引用且未使用 @JsonManagedReference/@JsonBackReference),Jackson 会抛出此异常。Spring 将其包装为 HttpMessageConversionException,最终可能成为一个 500 内部错误返回给客户端。

2.4 自定义 ObjectMapper 的途径

Jackson 的 ObjectMapper 是这一切魔法背后的核心引擎。在生产项目中,我们几乎总是需要定制它,例如配置日期格式、忽略空值等。

2.4.1 通过 Spring Boot 自动配置

在 Spring Boot 2.7.x 环境下,JacksonAutoConfiguration 会自动探测 classpath 上的模块,并提供一个 Jackson2ObjectMapperBuilder

// demo app
@Configuration
public class JacksonConfig {
    /**
     * 通过Jackson2ObjectMapperBuilderCustomizer来自定义ObjectMapper
     * 这种方式不会完全替换Boot自动配置的ObjectMapper,
     * 而是在其基础上进行微调,风险最小。
     */
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
        return builder -> {
            // 1. 日期格式化为ISO 8601而非时间戳
            builder.dateFormat(new StdDateFormat());
            // 2. 序列化时忽略 null 值字段
            builder.serializationInclusion(JsonInclude.Include.NON_NULL);
            // 3. 美化输出(开发环境适用)
            builder.featuresToEnable(SerializationFeature.INDENT_OUTPUT);
            // 4. 忽略未知属性,防止反序列化时报错
            builder.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
        };
    }
}

2.4.2 通过 WebMvcConfigurer 完全控制

如果你需要完全替换 Spring MVC 使用的 ObjectMapper

// demo app
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 1. 首先,找到原来的MappingJackson2HttpMessageConverter并移除
        converters.removeIf(converter -> converter instanceof MappingJackson2HttpMessageConverter);
        
        // 2. 创建一个全新的、深度定制的ObjectMapper
        ObjectMapper myMapper = new ObjectMapper()
                .registerModule(new JavaTimeModule()) // 支持JSR310
                .setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"))
                .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

        // 3. 使用自定义的ObjectMapper实例化转换器,并添加到列表首位
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(myMapper);
        converters.add(0, converter); // 使用 add(0, ...) 提升优先级
    }
}

3. XML 与其他转换器解析

虽然 JSON 是主流,但在许多金融、保险或者老旧系统集成领域,XML 依然占据重要地位。Spring MVC 通过 Jaxb2RootElementHttpMessageConverter 提供了对 JAXB(Java Architecture for XML Binding)的原生支持。

3.1 Jaxb2RootElementHttpMessageConverter 深度

此转换器专门用于处理标记了 @XmlRootElement 注解的对象。

// org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter
public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessageConverter<Object> {

    @Override
    protected boolean supports(Class<?> clazz) {
        // 核心条件:类必须被 @XmlRootElement 注解标记
        return clazz.isAnnotationPresent(XmlRootElement.class);
    }

    @Override
    protected Object readFromSource(Class<?> clazz, HttpHeaders headers, Source source) throws Exception {
        JAXBContext context = getJaxbContext(clazz);
        Unmarshaller unmarshaller = context.createUnmarshaller();
        return unmarshaller.unmarshal(source);
    }

    @Override
    protected void writeToResult(Object o, HttpHeaders headers, Result result) throws Exception {
        JAXBContext context = getJaxbContext(o.getClass());
        Marshaller marshaller = context.createMarshaller();
        marshaller.marshal(o, result);
    }
}

解读:

  • supports 是硬性约束:它通过反射检查 @XmlRootElement 注解。如果你有一个需要序列化为 XML 但没有此注解的类,例如一个简单的 POJO,这个转换器会直接忽略它。这是导致 406 错误的一个常见原因。
  • JAXB 上下文MarshallerUnmarshaller 的创建成本很高,因此 AbstractJaxb2HttpMessageConverter 内部使用了 ConcurrentHashMap 缓存不同类的 JAXBContext,这是一个重要的性能优化。
  • 对比 Jackson:Jackson 处理的是未标记任何注解的 POJO(Plain Old Java Object),即“运行时一切皆可序列化”;而 JAXB 要求类型的定义必须显式符合 XML 标准,是“契约先行”的设计。前者灵活,后者严谨。

3.2 其他内置转换器概览

  • StringHttpMessageConverter
    • 支持类型text/plain, text/*, */*
    • 行为: 处理 String 类型。其 supports 方法仅检查 String.class == clazz。默认字符集为 ISO-8859-1,如果您的 API 需要使用 UTF-8,务必配置 defaultCharset
  • FormHttpMessageConverter
    • 支持类型application/x-www-form-urlencoded
    • 行为: 专门用于读写 MultiValueMap<String, String>。它将表单数据解析为键值对,但不支持读写对象。注意,处理带有文件上传的 multipart/form-data 通常由 MultipartResolver@RequestPart 体系处理,详见系列前文。
  • ByteArrayHttpMessageConverter
    • 支持类型application/octet-stream, */*
    • 行为: 处理 byte[]。直接将请求输入流的字节读出,或将字节数组原样写入响应输出流。优先级较高,遍历时靠前。

4. 内容协商机制:ContentNegotiationManager 与策略

当一个控制器方法返回一个对象,且需要将其写入 HTTP 响应体时,Spring MVC 面临一个抉择:客户端想要什么格式?是 application/json 还是 application/xml?这个决策过程就是内容协商

4.1 内容协商的核心:ContentNegotiationManager

ContentNegotiationManager 是内容协商流程的总入口。它本身不执行解析逻辑,而是将任务委托给一组有序的 ContentNegotiationStrategy 策略。

// org.springframework.web.accept.ContentNegotiationManager
public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {

    private final List<ContentNegotiationStrategy> strategies = new ArrayList<>();

    public ContentNegotiationManager(ContentNegotiationStrategy... strategies) {
        this.strategies.addAll(Arrays.asList(strategies));
    }

    @Override
    public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
        for (ContentNegotiationStrategy strategy : this.strategies) {
            // 遍历所有策略,一旦某个返回非空列表,立即返回
            List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
            if (!mediaTypes.isEmpty() && !mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
                return mediaTypes;
            }
        }
        // 所有策略都无法决定,返回MediaType.ALL,表示不干涉
        return MEDIA_TYPE_ALL_LIST;
    }
}

设计模式: 这又是策略模式的一个绝佳案例,且是责任链模式的变体。多个 ContentNegotiationStrategy 负责从请求的不同位置(头、参数等)寻找媒体类型信息,ContentNegotiationManager 按顺序询问它们。一旦某个策略找到了明确的信息,它便终止链条并返回。

4.2 默认协商策略

在 Spring MVC 的默认配置下,通过 WebMvcConfigurationSupport 注册了一个 ContentNegotiationManager,其包含的策略链只有一项。

// org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
@Bean
public ContentNegotiationManager mvcContentNegotiationManager() {
    // ...
    // 默认仅使用解析Accept请求头的策略
    return new ContentNegotiationManager(new HeaderContentNegotiationStrategy());
}
  • HeaderContentNegotiationStrategy: 这是最标准的 RESTful 做法。它解析 HTTP 请求的 Accept 头。 请求示例Accept: application/json, text/plain;q=0.9 策略会解析出 [application/json, text/plain],并根据 q 值(quality/质量值)进行排序。q=1.0 是最高优先级,0.9 次之。

4.3 可选的策略与配置

你可以通过 WebConfig.configureContentNegotiation 轻松添加其他策略。

// demo app
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    configurer
        // 1. 启用基于请求参数(format)的策略
        .parameterName("format")
        .mediaType("json", MediaType.APPLICATION_JSON)
        .mediaType("xml", MediaType.APPLICATION_XML)
        // 2. 启用基于固定值作为回退的策略
        .defaultContentType(MediaType.APPLICATION_JSON)
        // 3. 配置Accept头的策略(默认已启用,这里只是为了明确展示)
        .favorParameter(true) // 开启format参数策略
        .ignoreAcceptHeader(false); // 不忽略Accept头
}
  • ParameterContentNegotiationStrategy: 通过 URL 参数指定格式,如 GET /users?format=json。这种方式简单直接,但被认为不够 RESTful,因为它污染了 URL。
  • FixedContentNegotiationStrategy (defaultContentType): 当所有其他策略都无法确定媒体类型时,使用一个固定的默认值。例如,总是返回 JSON。
  • PathExtensionContentNegotiationStrategy (已废弃): 通过 URL 路径后缀判断,如 GET /users.json。从 5.3.x 开始,这个策略默认不启用不推荐使用,因为它与 URL 内容本身无关、易出错,并且有安全风险(如 RFD 攻击)。

4.4 内容协商如何影响写入流程

内容协商的结果(一个或多个 MediaType)在 writeWithMessageConverters 方法中被用作筛选条件。该逻辑可以理解为:首先遍历候选转换器,如果转换器的 canWrite 对返回类型返回 true,再检查其 getSupportedMediaTypes 中是否有与协商出的媒体类型兼容的项。最终,找到一个在类型媒体类型上都匹配的转换器执行写入。

内容协商决策流程图

flowchart TD
    Start["开始: 处理@ResponseBody返回值"] --> CallManager["调用ContentNegotiationManager.resolveMediaTypes"]
    
    subgraph 策略链解析 ["策略链解析"]
        CallManager --> Strategy1["1. ParameterContentNegotiationStrategy<br>检查请求参数 'format'"]
        Strategy1 -- "已配置且存在参数" --> CheckParam{"能找到匹配的MediaType?"}
        CheckParam -- "是" --> ReturnMediaType["返回确定的MediaType列表"]
        CheckParam -- "否" --> Strategy2["2. HeaderContentNegotiationStrategy<br>解析 Accept 请求头"]
        
        CallManager -- "未配置参数策略" --> Strategy2
        Strategy2 --> CheckHeader{"Accept头是否为空或 */* ?"}
        CheckHeader -- "否, 有明确值" --> ReturnMediaType
        CheckHeader -- "是" --> Strategy3["3. FixedContentNegotiationStrategy<br>返回配置的默认Content-Type"]
    end
    
    Strategy3 --> CheckDefault{"配置了defaultContentType?"}
    CheckDefault -- "是" --> ReturnMediaType
    CheckDefault -- "否" --> ReturnAll["返回 MediaType.ALL"]

    ReturnMediaType --> Filter["在writeWithMessageConverters中<br>作为过滤器"]
    ReturnAll --> Filter
    Filter --> Match{"遍历转换器,canWrite & MediaType兼容?"}
    Match -- "找到" --> Write["执行write操作"]
    Match -- "未找到" --> Error406["抛出 HttpMediaTypeNotAcceptableException -> HTTP 406"]

    classDef decision fill:#fff4e6,stroke:#ff9800,stroke-width:2px,color:#333;
    classDef process fill:#f8f9fa,stroke:#333,stroke-width:1px,color:#333;
    classDef endpoint fill:#ffebee,stroke:#b71c1c,stroke-width:2px,color:#333;
    class Start,CallManager,Strategy1,Strategy2,Strategy3,ReturnMediaType,ReturnAll,Filter,Write process;
    class CheckParam,CheckHeader,CheckDefault,Match decision;
    class Error406 endpoint;
  • 图表主旨概括:此流程图清晰展示了当需要确定响应格式时,ContentNegotiationManager 如何通过其内部的策略链,解析出目标 MediaType,并最终影响 HttpMessageConverter 的写入选择。
  • 逐层/逐元素分解
    1. 策略解析阶段:流程从 ContentNegotiationManager.resolveMediaTypes 开始,依次尝试已配置的策略。注意,一旦某个策略返回确定结果,后续策略不再执行。
    2. 策略链构成:图中展示了一个典型组合:先是参数策略,然后是 Accept 头策略,最后是固定默认值策略。开发人员可通过 ContentNegotiationConfigurer 调整此链条的顺序和成员。
    3. 结果应用:解析出的 MediaType 被传入 writeWithMessageConverters,作为选择转换器的关键过滤条件。
    4. 异常分支:如果协商出 MediaType.ALL 但没有任何转换器可用,或者协商出了明确类型但无匹配转换器,都会导致 406 错误。
  • 设计原理映射:这是责任链模式过滤器模式的结合。ContentNegotiationManager 内部的 strategies 列表是责任链;而在 writeWithMessageConverters 中,协商出的 MediaTypeHttpMessageConverter 列表的组合筛选,其行为就像一个管道-过滤器。
  • 工程联系与关键结论:排查 406 错误时,首要任务是确定内容协商的结果是什么。在调试器中观察 ContentNegotiationManager.resolveMediaTypes 的返回值,是诊断 406 问题的“银弹”。

5. 完整的读写流程:从 @RequestBody 到 @ResponseBody

现在,我们将前面所有零散的知识点串联起来,完整地走通一次 RESTful 请求的处理闭环。这里的主角是 RequestResponseBodyMethodProcessor,它一个类同时实现了 HandlerMethodArgumentResolverHandlerMethodReturnValueHandler 接口,专门处理 @RequestBody@ResponseBody 注解。

5.1 读取流程:@RequestBody 的注入

sequenceDiagram
    participant DispatcherServlet
    participant RequestMappingHandlerAdapter as HandlerAdapter
    participant RRBodyProcessor as RequestResponseBodyMethodProcessor
    participant ConverterList as List<HttpMessageConverter>
    participant JacksonConverter as MappingJackson2HttpMessageConverter

    DispatcherServlet->>HandlerAdapter: handle()
    HandlerAdapter->>RRBodyProcessor: resolveArgument(标记了@RequestBody的参数)
    
    RRBodyProcessor->>RRBodyProcessor: readWithMessageConverters(...)
    Note over RRBodyProcessor: 1. 获取ServletRequest的Content-Type
    
    loop 遍历所有转换器
        RRBodyProcessor->>ConverterList: 遍历,调用converter.canRead(targetType, contentType)
        alt 匹配成功
            RRBodyProcessor->>JacksonConverter: converter.read(...)
            JacksonConverter->>JacksonConverter: 内部调用objectMapper.readerFor(...).readValue(inputStream)
            JacksonConverter-->>RRBodyProcessor: 返回User实例
            Note over RRBodyProcessor: 2. 找到匹配,直接返回结果
        else 未匹配
            RRBodyProcessor->>ConverterList: 继续下一个转换器
        end
    end
    
    alt 成功解析
        RRBodyProcessor-->>HandlerAdapter: 返回解析好的对象
    else 无匹配转换器
        RRBodyProcessor-->>HandlerAdapter: 抛出HttpMediaTypeNotSupportedException (415)
    end
  1. 进入解析器RequestMappingHandlerAdapter 在处理请求时发现某一个参数带有 @RequestBody 注解,于是调用 RequestResponseBodyMethodProcessorresolveArgument 方法。
  2. 读取并转换:解析器不经过任何 WebDataBinder,直接调用内部方法 readWithMessageConverters。该方法首先读取请求头中的 Content-Type
  3. 策略遍历:它遍历应用中已注册的所有 HttpMessageConverter。对于列表中的每一个转换器,都调用其 canRead(targetClass, contentType) 方法。
  4. 执行转换:当找到第一个 canRead 返回 true 的转换器(如 MappingJackson2HttpMessageConverter)时,遍历停止,并调用其 read 方法。该方法内部会使用 Jackson 的 ObjectReaderServletInputStream 中直接读取字节流并反序列化为 Java 对象。
  5. 异常处理:如果整个列表遍历完毕也没有找到匹配的转换器,将抛出 HttpMediaTypeNotSupportedException,最终导致客户端收到 415 Unsupported Media Type 错误。

5.2 写入流程:@ResponseBody 的返回

sequenceDiagram
    participant HandlerAdapter
    participant RRBodyProcessor as RequestResponseBodyMethodProcessor
    participant CtnManager as ContentNegotiationManager
    participant Strategy as HeaderContentNegotiationStrategy
    participant ConverterList as List<HttpMessageConverter>
    participant JacksonConverter as MappingJackson2HttpMessageConverter

    HandlerAdapter->>RRBodyProcessor: handleReturnValue(返回对象)
    
    Note over RRBodyProcessor: 方法开始
    RRBodyProcessor->>CtnManager: resolveMediaTypes(request)
    CtnManager->>Strategy: resolveMediaTypes(request)
    Strategy-->>CtnManager: 解析Accept头,返回 [application/json, application/xml]
    CtnManager-->>RRBodyProcessor: 返回协商结果: [application/json, application/xml]
    
    RRBodyProcessor->>RRBodyProcessor: writeWithMessageConverters(返回值, 协商结果)
    
    loop 遍历所有转换器
        RRBodyProcessor->>ConverterList: 遍历,获取每个converter的supportedMediaTypes
        alt 协商结果列表与supportedMediaTypes有交集,且canWrite为true
            Note over RRBodyProcessor: 选择最佳匹配
            RRBodyProcessor->>JacksonConverter: converter.write(user, application/json, outputMessage)
            JacksonConverter->>JacksonConverter: 内部调用objectMapper.writer().writeValue(outputStream)
            JacksonConverter-->>RRBodyProcessor: 写入完成
            Note over RRBodyProcessor: 转换成功,结束流程
        else 无交集或不支持
            RRBodyProcessor->>ConverterList: 继续下一个转换器
        end
    end
    
    alt 写入成功
        RRBodyProcessor-->>HandlerAdapter: 返回值已处理
    else 无匹配转换器
        RRBodyProcessor-->>HandlerAdapter: 抛出HttpMediaTypeNotAcceptableException (406)
    end
  1. 进入处理器:控制器方法成功执行并返回一个值后,HandlerAdapter 发现该方法或类上有 @ResponseBody 注解,调用 RequestResponseBodyMethodProcessorhandleReturnValue
  2. 内容协商:处理器首先调用 ContentNegotiationManager.resolveMediaTypes,确定客户端能够接受的一组媒体类型。例如,根据 Accept 头解析出 [application/json, application/xml]
  3. 双重匹配:流程进入 writeWithMessageConverters。它会遍历所有注册的 HttpMessageConverter。对于每个转换器,执行一个“与”逻辑:
    • 转换器必须 canWrite 返回值的类型。
    • 转换器支持的媒体类型,必须与内容协商出的媒体类型列表有兼容的。
  4. 执行写入:找到第一个满足双重条件的转换器后,调用其 write 方法。该方法内部利用 Jackson 的 ObjectWriter 将 Java 对象序列化到 ServletOutputStream 中。
  5. 异常处理:如果没有任何匹配的转换器,将抛出 HttpMediaTypeNotAcceptableException,导致客户端收到 406 Not Acceptable 错误。

5.3 与参数解析体系的关系

这是一个重要的横向对比点:

  • @RequestParam@ModelAttribute 经过的是 WebDataBinderConversionService 体系,它们处理的是从 URL 查询参数或表单键值对到对象属性的拼凑式绑定字段级转换
  • @RequestBody 则完全不经过 WebDataBinder,它由 HttpMessageConverter 直接对请求体的整个输入流进行整体反序列化
  • 前者是“散装零件组装”,后者是“整车进口”。这种架构分离,使得Spring MVC在同个框架内优雅地处理了传统Web表单和现代RESTful API两种截然不同的应用场景。RequestResponseBodyMethodProcessor 正是这条分界线的守门人。

6. 自定义消息转换器实战

Spring MVC 的扩展性在此处体现得淋漓尽致。假设我们要支持导出 CSV 格式的数据。

6.1 实现一个 CSV 转换器

首先,我们需要定义一个媒体类型 text/csv

// demo app
public class CsvHttpMessageConverter extends AbstractHttpMessageConverter<List<User>> {

    // 1. 定义支持的媒体类型
    public CsvHttpMessageConverter() {
        super(new MediaType("text", "csv"));
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        // 2. 简单支持List类型即可,更精确的判断在writeInternal中做
        return List.class.isAssignableFrom(clazz);
    }

    @Override
    protected List<User> readInternal(Class<? extends List<User>> clazz, HttpInputMessage inputMessage) {
        // CSV输入流解析通常较复杂,这里略过
        throw new UnsupportedOperationException("CSV reading is not supported yet.");
    }

    @Override
    protected void writeInternal(List<User> users, HttpOutputMessage outputMessage) throws IOException {
        // 3. 设置响应头
        outputMessage.getHeaders().setContentDispositionFormData("attachment", "users.csv");
        
        Charset charset = getCharset(outputMessage.getHeaders().getContentType());
        try (OutputStreamWriter writer = new OutputStreamWriter(outputMessage.getBody(), charset)) {
            // 4. 写入CSV头
            writer.write("ID,Name,Email\n");
            // 5. 写入每一行用户数据
            for (User user : users) {
                writer.write(String.format("%d,%s,%s\n",
                        user.getId(), user.getName(), user.getEmail()));
            }
            writer.flush();
        }
    }
    
    private Charset getCharset(MediaType contentType) {
        return contentType != null && contentType.getCharset() != null ?
               contentType.getCharset() : StandardCharsets.UTF_8;
    }
}

6.2 注册自定义转换器

// demo app
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        // 添加自定义的CSV转换器
        converters.add(new CsvHttpMessageConverter());
    }
}

7. 生产事故排查专题

理论是完美的,但线上环境总会给我们惊喜。

7.1 事故一:突如其来的 415 Unsupported Media Type

  • 现象:服务端刚升级了 Spring 版本,一个旧的内部服务调用 /api/users 的 POST 请求突然返回 HTTP 415 Unsupported Media Type。调用方发送的是 Content-Type: application/xml 的 XML 数据。
  • 排查思路
    1. 检查 Controller,确认有 @PostMapping(consumes = MediaType.APPLICATION_XML_VALUE)。这是 415 错误的典型触发点。
    2. 检查新版本服务的依赖树,发现 jaxb-api 这个依赖在升级时被误去除。由于之前项目引入了另一个包含 JAXB 的传递性依赖,此次升级后该传递依赖断裂。
    3. 在启动日志中搜索 “registering Jaxb2”,确认 Jaxb2RootElementHttpMessageConverter 因为 classpath 上缺少 JAXB 相关类而未被注册。
  • 根因:依赖缺失导致 Jaxb2RootElementHttpMessageConverter 未被注册。当请求到达时,readWithMessageConverters 遍历所有转换器,MappingJackson2HttpMessageConverterapplication/xmlcanRead 返回 false,而默认的 StringHttpMessageConvertercanRead 对于非 String 的 User 类型也返回 false。最终遍历结束,抛出 HttpMediaTypeNotSupportedException
  • 解决:在 pom.xml 中重新添加 javax.xml.bind:jaxb-apiorg.glassfish.jaxb:jaxb-runtime 依赖。
  • 最佳实践
    • 核心依赖监控:对 jacksonjaxb 等核心序列化库的依赖变更进行严格审查。
    • 启动检查:编写集成测试或启动时 Actuator 端点来检查 DispatcherServlet 已注册的 HttpMessageConverter 列表,确保预期转换器存在。

7.2 事故二:API 版本化引发的 406 Not Acceptable

  • 现象:我们设计了一个用户服务的 v2 版本,通过自定义媒体类型 application/vnd.myapp.user-v2+json 进行区分。部分老旧的内部调用方(使用 v1 接口)开始间歇性收到 HTTP 406 Not Acceptable 错误。
  • 排查思路
    1. 查看日志,发现有 HttpMediaTypeNotAcceptableException 堆栈信息。
    2. 观察调用方发送的请求头,发现 Accept: application/vnd.myapp.user-v1+json。这是正确的 v1 调用方。
    3. 检查 Controller,发现新的 v2 接口方法上明确写了 @GetMapping(produces = "application/vnd.myapp.user-v2+json")。这是 v2 的端点,本应只处理 v2 请求。
    4. 关键点:为什么 v1 的请求会路由到 v2 的处理器上?排查发现,在 @RequestMapping 路径相同时,produces 条件不幸地匹配上了 v1 的 Accept 头?不,是反过来,由于某种路径或参数配置错误,v1 的请求被错误地分发到了 v2 的方法上。
    5. 修正假设:假设 v1 和 v2 本就在同一个方法上处理,通过 Accept 头区分。但该方法仅返回 v2 的 UserV2 对象,且全局只注册了 MappingJackson2HttpMessageConverter。这个转换器虽然能写 UserV2,但它支持的媒体类型是 application/jsonapplication/*+json
    6. v1 的请求 Accept: application/vnd.myapp.user-v1+json,其媒体类型是 application/vnd.myapp.user-v1+json,这个类型与我们转换器支持的 application/*+json 完全匹配!那为何会出 406?
  • 根因:深入 produces 条件的内部,Spring MVC 在根据 produces 条件选择处理器方法时,对媒体类型的匹配非常严格。虽然 MappingJackson2HttpMessageConverter 能处理 application/*+json,但 @GetMapping(produces = "application/vnd.myapp.user-v2+json") 却和请求的 Acceptapplication/vnd.myapp.user-v1+json 不兼容。因此,请求根本无法到达任何方法,导致在方法定位阶段就抛出了 HttpMediaTypeNotAcceptableException,这甚至发生在内容协商和转换器选择之前。
  • 解决:修改 v2 方法的 produces 条件,使用数组同时声明 v1 和 v2 的类型:@GetMapping(produces = {"application/vnd.myapp.user-v1+json", "application/vnd.myapp.user-v2+json"})。或者,更彻底的方案是为 v1 和 v2 创建不同的 Controller。
  • 最佳实践
    • produces 是硬性过滤器:要明确区分 MediaType 匹配发生在两个不同阶段(方法选择和转换器选择)。@RequestMappingproduces/consumes 是第一道关卡,它们比 HttpMessageConverter 更严格。
    • 避免在一个方法上处理多个版本:尽管技术上可行,但会导致混乱的 produces 条件和复杂的返回逻辑,强烈建议通过 URL 路径或独立的 Controller 进行版本控制。

8. 面试高频专题

  1. 简述 HttpMessageConverter 的作用和工作原理。

    • 标准回答:它是 HTTP 消息体与 Java 对象之间的双向转换器。工作原理是基于策略模式,通过 canRead/canWrite 检查是否能处理给定类型和媒体类型,进而用 read/write 完成序列化和反序列化。
    • 追问与加分
      • 追问GenericHttpMessageConverter 和普通的有何区别?
      • 回答GenericHttpMessageConverter 可以处理带泛型的类型,如 List<User>,它能获取到完整的泛型信息,而普通转换器只能获取到 List 这个原始类型。MappingJackson2HttpMessageConverter 就是前者的实现。
      • 追问canRead 的实现逻辑一般是怎样的?
      • 回答:一般在 supports 方法中检查 Java 类型,再检查传入的 MediaType 是否被 getSupportedMediaTypes() 包含。
  2. Spring MVC 是如何处理 @RequestBody 注解的?它和 @RequestParam 的处理方式有什么不同?

    • 标准回答@RequestBodyRequestResponseBodyMethodProcessor 处理,它通过 HttpMessageConverter 直接反序列化整个请求体为对象,不经过 WebDataBinder。而 @RequestParamRequestParamMethodArgumentResolver 处理,它从请求参数中获取单个值,然后通过 ConversionService 进行类型转换。前者是整体序列化,后者是字段级转换。
    • 追问与加分
      • 追问:为什么它们的设计如此不同?
      • 回答:因为它们面对的输入源不同。@RequestParam 处理键值对文本,天然适合字段级操作。@RequestBody 处理复杂或多层的结构化数据(JSON/XML),整体解析为对象树是最高效和自然的做法。
      • 追问@ModelAttribute 呢?
      • 回答@ModelAttribute 也是从请求参数中获取字段并绑定到对象的属性上,它会经过 WebDataBinderConversionService,与 @RequestParam 是同一体系,但粒度更粗。
  3. 如果客户端希望同时支持 JSON 和 XML,应该怎么配置?

    • 标准回答:首先确保 classpath 下有 jackson-databindjaxb-api。其次,Controller 方法上不写或用数组声明 produces/consumes 包含两种类型。最后,通过 accept 头的 q 值或在 ContentNegotiationConfigurer 中配置 favorParameter 等方式来控制首选格式。
    • 追问与加分
      • 追问:如果只用默认配置,同时支持 JSON 和 XML 的依赖都在,当请求 Accept 头为空时,会返回哪个?
      • 回答:默认只有 HeaderContentNegotiationStrategy,空 Accept 头会被解析为 */*,此时会使用列表中的第一个能写入的转换器,通常是 ByteArrayHttpMessageConverterStringHttpMessageConverter。若要明确默认返回 JSON,必须配置 defaultContentType
  4. 内容协商的策略有哪些?Spring MVC 默认使用什么?

    • 标准回答:主要有三种策略:基于请求头、基于参数、基于固定值。Spring MVC 默认使用基于请求头(HeaderContentNegotiationStrategy)的策略。
    • 追问与加分
      • 追问:为什么 PathExtensionContentNegotiationStrategy 被废弃?
      • 回答:因为它依赖于 URL 后缀,与现代 REST URL 设计理念冲突,且容易引发安全风险(如 RFD 反射文件下载攻击)和歧义。
      • 追问:如果同时配置了参数和请求头策略,谁的优先级高?
      • 回答:取决于它们在 ContentNegotiationManagerstrategies 列表的顺序。通过 ContentNegotiationConfigurer 配置时,通常参数策略会被添加在请求头策略之前,使其优先级更高。
  5. MappingJackson2HttpMessageConverter 是如何将 JSON 转换为 Java 对象的?

    • 标准回答:它内部持有一个 ObjectMapper 实例。在 read 方法中,它根据目标类型创建一个 ObjectReader,然后直接从 HttpInputMessage 的输入流中调用 readValue 进行反序列化。这个过程是流式的,节省内存。
    • 追问与加分
      • 追问:如何自定义这个 ObjectMapper
      • 回答:可以通过 Jackson2ObjectMapperBuilderCustomizer Bean(推荐),或通过 WebMvcConfigurer.extendMessageConverters 完全替换转换器中的 ObjectMapper
      • 追问ObjectMapper 是线程安全的吗?ObjectReader 呢?
      • 回答ObjectMapper 是线程安全的,可以全局共享。ObjectReaderObjectWriter 同样是不可变且线程安全的。这个设计保证了在高并发下的性能和安全性。
  6. 如何自定义一个 HttpMessageConverter?需要注意什么?

    • 标准回答:创建一个类实现 HttpMessageConverter 或继承 AbstractHttpMessageConverter,实现 supportsreadInternalwriteInternal 方法,并在 supports 中严格校验类型。然后通过 WebMvcConfigurerextendMessageConvertersconfigureMessageConverters 注册。
    • 追问与加分
      • 追问extendMessageConvertersconfigureMessageConverters 的区别?
      • 回答:前者在默认列表基础上添加,不会破坏原有功能。后者会清空默认列表,只使用你提供的。绝大多数场景应使用前者。
      • 追问:自定义转换器应放在列表的什么位置?
      • 回答:如果希望优先使用,应放在列表头部。特别是当你的转换器处理的媒体类型与默认转换器重叠时,排序至关重要。
  7. 当 Accept 头包含多个媒体类型时,Spring MVC 如何决定用哪个?

    • 标准回答HeaderContentNegotiationStrategy 会解析 Accept 头,并根据 q 值(质量因子)和相对优先级对媒体类型列表进行排序。在 writeWithMessageConverters 中,Spring 会遍历这个排序后的列表,并尝试找到第一个匹配的转换器。这体现了客户端偏好的优先级。
  8. 415 和 406 错误的根源分别是什么?

    • 标准回答:415 是服务端无法解析请求体,根源在于没有 HttpMessageConvertercanRead 匹配请求的 Content-Type。406 是服务端无法生成客户端可接受的响应体,根源在于没有 HttpMessageConvertercanWrite 匹配内容协商出的 MediaType 列表。
  9. 为什么通常建议避免使用 PathExtension 内容协商策略?

    • 标准回答:主要有三点原因:一是使 URL 不纯净,与现代 RESTful 设计理念相悖;二是无法区分资源层级和格式后缀,如 /users/1.json 是格式还是 ID?三是存在安全风险,如反射文件下载(RFD)攻击,可能被恶意构造的 URL 诱骗。
    • 追问与加分
      • 追问:如果由于历史原因必须使用,如何配置?
      • 回答:可以通过 ContentNegotiationConfigurer 启用,但强烈建议禁用 ignoreInvalidPathExtensionsuseJaf 等选项,并显式注册允许的扩展名映射,以避免歧义和安全问题。
  10. 内置的消息转换器有哪些?它们的排序重要吗?

    • 标准回答:重要的有 ByteArrayHttpMessageConverterStringHttpMessageConverterResourceHttpMessageConverterSourceHttpMessageConverterFormHttpMessageConverterMappingJackson2HttpMessageConverterJaxb2RootElementHttpMessageConverter。排序至关重要,因为遍历是顺序进行的。例如 StringHttpMessageConvertersupportsString 返回 true,如果它在 Jackson 之前,并且内容协商出 application/json,它的 canWrite 会失败(因为它不支持 application/json),遍历会继续。但如果内容协商返回 */*StringHttpMessageConverter 就会第一个匹配,导致返回的 JSON 被当成普通文本处理,可能被二次编码,产生乱码。
    • 追问与加分
      • 追问:如果我想让我自定义的 JSON 转换器优先于默认的,但不想失去 ByteArray 等处理能力,如何操作最好?
      • 回答:使用 extendMessageConverters,先 removeIf 移除默认的 Jackson 转换器,再 add(0, myJacksonConverter) 将其放在列表头部。
  11. 如何在 Spring Boot 中自定义 Jackson 的 ObjectMapper?它会影响消息转换器吗?

    • 标准回答:最标准的方式是定义 Jackson2ObjectMapperBuilderCustomizer Bean 或直接定义 ObjectMapper Bean。在 Spring Boot 自动配置下,Context 中的 ObjectMapper Bean 会自动注入到 MappingJackson2HttpMessageConverter 中,因此会影响消息转换器的行为。
  12. (系统设计题)设计一个支持多版本 API 的 RESTful 服务,版本控制通过 Accept 头(如 application/vnd.myapp.v1+json)实现。请说明如何利用内容协商和自定义 HttpMessageConverter 来处理不同版本的数据序列化差异。

    • 标准回答:核心思路是,利用 produce"vnd.myapp.v1+json" 格式能被 MappingJackson2HttpMessageConverter 识别这一特点。可以为不同版本编写独立的 DTO,但用同一个 Controller 方法处理,在方法内部根据请求的 Accept 头版本信息返回不同 DTO。更优雅的方式是,使用继承或组合统一返回对象,注册一个自定义的 VndJsonMessageConverter 扩展自 MappingJackson2HttpMessageConverter,重写 writeInternal 方法,根据具体版本执行不同的序列化逻辑。
    • 追问与加分
      • 追问:如果 v1 和 v2 的返回结构差异巨大,如何处理?
      • 回答:可以在 Controller 方法中,通过注入 HttpServletRequest 来获取 Accept 头,进行简单的版本判断,然后调用不同的 Service 方法并返回完全不同的 DTO 类型。这虽然不够“自动”,但清晰直接,适合差异大的场景。
      • 追问:如何保证 Swagger 或 OpenAPI 文档也能正确区分版本?
      • 回答:这是当前主流 API 文档工具的难点。通常需要为不同版本建立不同的 Controller 分组,并利用注解区分 produces 条件。有些工具支持基于 Accept 头的分组渲染,但实现较复杂。一个工程上的最佳实践是,通过不同 URL 路径(如 /v1/users, /v2/users)进行版本控制,这是最清晰、最不容易出错,且对网关、文档等所有基础设施都最友好的方式。 仅当无法更改 URL 时才考虑使用 Accept 头进行版本控制。

HttpMessageConverter 与内容协商速查表

主题核心组件/方法关键点与默认行为
接口契约canRead/Write, read/write, getSupportedMediaTypes策略模式基础,两阶段执行(检查 -> 执行)。
注册与定制addDefaultHttpMessageConverters, extendMessageConverters通过 classpath 探测动态添加;extendconfigure 更安全。
JSON 核心MappingJackson2HttpMessageConverter基于 Jackson ObjectMapper,支持 application/*+json,线程安全。
XML 核心Jaxb2RootElementHttpMessageConverter基于 JAXB,需 @XmlRootElement 注解,设计为“契约先行”。
内容协商入口ContentNegotiationManager.resolveMediaTypes责任链模式,按序询问策略。
协商策略HeaderContentNegotiationStrategy (默认), Parameter..., Fixed...避免使用 PathExtensionContentNegotiationStrategy
参数解析器RequestResponseBodyMethodProcessor同时处理 @RequestBody@ResponseBody
读取触发HttpMediaTypeNotSupportedException (415)无匹配的转换器 canRead 请求的 Content-Type
写入触发HttpMediaTypeNotAcceptableException (406)无匹配的转换器 canWrite 内容协商出的 MediaType
常见问题415 错误,406 错误,JSON 字段映射错误,XML 缺少注解优先检查依赖、转换器列表、Content-Type/Accept 头和时间格式配置。

延伸阅读

  1. Spring Framework 官方文档:Web on Servlet Stack -> Message Converters, Content Negotiation.
  2. 《Spring 实战 (第 5 版)》:第 2 章与第 6 章,提供了大量关于构建 REST API 的可执行示例。
  3. Jackson 官方文档:特别是 ObjectMapperObjectReaderObjectWriter 以及模块(如 java-time)的部分,是深入掌握 JSON 处理的必备资料。
  4. JAXB 规范与教程:用于理解 @XmlRootElement 等注解在对象与 XML 映射中扮演的角色。