概述
在《参数解析与类型转换》一文中,我们深入探讨了 @RequestParam 和 @ModelAttribute 如何通过 WebDataBinder 与 ConversionService,将 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-databind,MappingJackson2HttpMessageConverter才会被加入。这保证了在没有依赖的情况下不会出错。
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 需要这种能力。 - 抽象实现层:
AbstractHttpMessageConverter和AbstractGenericHttpMessageConverter为两个接口提供了骨架实现。它们实现了read/write/canRead/canWrite的模板逻辑,并将核心差异点抽象为readInternal、writeInternal和supports。 - 具体实现层:
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());
}
解读:
- 类型转换: 方法首先通过
getJavaType将 Java 的Type转换为 Jackson 内部的JavaType,这一步能保留完整的泛型信息,是准确反序列化List<User>这类泛型集合的基础。 - 读取器构建:
objectMapper.readerFor(javaType)创建了一个针对目标类型的反序列化读取器ObjectReader。这是一个线程安全且可配置的对象。 - 流式处理: 最终调用
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);
}
// ...
}
解读:
ObjectWriter的构建:类似读取时的ObjectReader,ObjectWriter也是线程安全的。通过objectMapper.writerFor(...)创建,可以绑定特定的类型和配置(如视图、过滤器)。- 生成器模式:
objectMapper.getFactory().createGenerator(...)创建了一个JsonGenerator,它负责将事件序列(通常是开始对象、字段名、值等)写到底层的输出流中。这种方法非常高效。 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 上下文:
Marshaller和Unmarshaller的创建成本很高,因此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的写入选择。 - 逐层/逐元素分解:
- 策略解析阶段:流程从
ContentNegotiationManager.resolveMediaTypes开始,依次尝试已配置的策略。注意,一旦某个策略返回确定结果,后续策略不再执行。 - 策略链构成:图中展示了一个典型组合:先是参数策略,然后是
Accept头策略,最后是固定默认值策略。开发人员可通过ContentNegotiationConfigurer调整此链条的顺序和成员。 - 结果应用:解析出的
MediaType被传入writeWithMessageConverters,作为选择转换器的关键过滤条件。 - 异常分支:如果协商出
MediaType.ALL但没有任何转换器可用,或者协商出了明确类型但无匹配转换器,都会导致 406 错误。
- 策略解析阶段:流程从
- 设计原理映射:这是责任链模式与过滤器模式的结合。
ContentNegotiationManager内部的strategies列表是责任链;而在writeWithMessageConverters中,协商出的MediaType与HttpMessageConverter列表的组合筛选,其行为就像一个管道-过滤器。 - 工程联系与关键结论:排查 406 错误时,首要任务是确定内容协商的结果是什么。在调试器中观察
ContentNegotiationManager.resolveMediaTypes的返回值,是诊断 406 问题的“银弹”。
5. 完整的读写流程:从 @RequestBody 到 @ResponseBody
现在,我们将前面所有零散的知识点串联起来,完整地走通一次 RESTful 请求的处理闭环。这里的主角是 RequestResponseBodyMethodProcessor,它一个类同时实现了 HandlerMethodArgumentResolver 和 HandlerMethodReturnValueHandler 接口,专门处理 @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
- 进入解析器:
RequestMappingHandlerAdapter在处理请求时发现某一个参数带有@RequestBody注解,于是调用RequestResponseBodyMethodProcessor的resolveArgument方法。 - 读取并转换:解析器不经过任何
WebDataBinder,直接调用内部方法readWithMessageConverters。该方法首先读取请求头中的Content-Type。 - 策略遍历:它遍历应用中已注册的所有
HttpMessageConverter。对于列表中的每一个转换器,都调用其canRead(targetClass, contentType)方法。 - 执行转换:当找到第一个
canRead返回true的转换器(如MappingJackson2HttpMessageConverter)时,遍历停止,并调用其read方法。该方法内部会使用 Jackson 的ObjectReader从ServletInputStream中直接读取字节流并反序列化为 Java 对象。 - 异常处理:如果整个列表遍历完毕也没有找到匹配的转换器,将抛出
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
- 进入处理器:控制器方法成功执行并返回一个值后,
HandlerAdapter发现该方法或类上有@ResponseBody注解,调用RequestResponseBodyMethodProcessor的handleReturnValue。 - 内容协商:处理器首先调用
ContentNegotiationManager.resolveMediaTypes,确定客户端能够接受的一组媒体类型。例如,根据Accept头解析出[application/json, application/xml]。 - 双重匹配:流程进入
writeWithMessageConverters。它会遍历所有注册的HttpMessageConverter。对于每个转换器,执行一个“与”逻辑:- 转换器必须
canWrite返回值的类型。 - 转换器支持的媒体类型,必须与内容协商出的媒体类型列表有兼容的。
- 转换器必须
- 执行写入:找到第一个满足双重条件的转换器后,调用其
write方法。该方法内部利用 Jackson 的ObjectWriter将 Java 对象序列化到ServletOutputStream中。 - 异常处理:如果没有任何匹配的转换器,将抛出
HttpMediaTypeNotAcceptableException,导致客户端收到 406 Not Acceptable 错误。
5.3 与参数解析体系的关系
这是一个重要的横向对比点:
@RequestParam、@ModelAttribute经过的是WebDataBinder→ConversionService体系,它们处理的是从 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 数据。 - 排查思路:
- 检查 Controller,确认有
@PostMapping(consumes = MediaType.APPLICATION_XML_VALUE)。这是 415 错误的典型触发点。 - 检查新版本服务的依赖树,发现
jaxb-api这个依赖在升级时被误去除。由于之前项目引入了另一个包含 JAXB 的传递性依赖,此次升级后该传递依赖断裂。 - 在启动日志中搜索 “registering Jaxb2”,确认
Jaxb2RootElementHttpMessageConverter因为 classpath 上缺少 JAXB 相关类而未被注册。
- 检查 Controller,确认有
- 根因:依赖缺失导致
Jaxb2RootElementHttpMessageConverter未被注册。当请求到达时,readWithMessageConverters遍历所有转换器,MappingJackson2HttpMessageConverter对application/xml的canRead返回 false,而默认的StringHttpMessageConverter的canRead对于非 String 的User类型也返回 false。最终遍历结束,抛出HttpMediaTypeNotSupportedException。 - 解决:在
pom.xml中重新添加javax.xml.bind:jaxb-api和org.glassfish.jaxb:jaxb-runtime依赖。 - 最佳实践:
- 核心依赖监控:对
jackson、jaxb等核心序列化库的依赖变更进行严格审查。 - 启动检查:编写集成测试或启动时 Actuator 端点来检查
DispatcherServlet已注册的HttpMessageConverter列表,确保预期转换器存在。
- 核心依赖监控:对
7.2 事故二:API 版本化引发的 406 Not Acceptable
- 现象:我们设计了一个用户服务的 v2 版本,通过自定义媒体类型
application/vnd.myapp.user-v2+json进行区分。部分老旧的内部调用方(使用 v1 接口)开始间歇性收到HTTP 406 Not Acceptable错误。 - 排查思路:
- 查看日志,发现有
HttpMediaTypeNotAcceptableException堆栈信息。 - 观察调用方发送的请求头,发现
Accept: application/vnd.myapp.user-v1+json。这是正确的 v1 调用方。 - 检查 Controller,发现新的 v2 接口方法上明确写了
@GetMapping(produces = "application/vnd.myapp.user-v2+json")。这是 v2 的端点,本应只处理 v2 请求。 - 关键点:为什么 v1 的请求会路由到 v2 的处理器上?排查发现,在
@RequestMapping路径相同时,produces条件不幸地匹配上了 v1 的Accept头?不,是反过来,由于某种路径或参数配置错误,v1 的请求被错误地分发到了 v2 的方法上。 - 修正假设:假设 v1 和 v2 本就在同一个方法上处理,通过
Accept头区分。但该方法仅返回 v2 的UserV2对象,且全局只注册了MappingJackson2HttpMessageConverter。这个转换器虽然能写UserV2,但它支持的媒体类型是application/json和application/*+json。 - 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")却和请求的Accept头application/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匹配发生在两个不同阶段(方法选择和转换器选择)。@RequestMapping的produces/consumes是第一道关卡,它们比HttpMessageConverter更严格。- 避免在一个方法上处理多个版本:尽管技术上可行,但会导致混乱的
produces条件和复杂的返回逻辑,强烈建议通过 URL 路径或独立的 Controller 进行版本控制。
8. 面试高频专题
-
简述 HttpMessageConverter 的作用和工作原理。
- 标准回答:它是 HTTP 消息体与 Java 对象之间的双向转换器。工作原理是基于策略模式,通过
canRead/canWrite检查是否能处理给定类型和媒体类型,进而用read/write完成序列化和反序列化。 - 追问与加分:
- 追问:
GenericHttpMessageConverter和普通的有何区别? - 回答:
GenericHttpMessageConverter可以处理带泛型的类型,如List<User>,它能获取到完整的泛型信息,而普通转换器只能获取到List这个原始类型。MappingJackson2HttpMessageConverter就是前者的实现。 - 追问:
canRead的实现逻辑一般是怎样的? - 回答:一般在
supports方法中检查 Java 类型,再检查传入的MediaType是否被getSupportedMediaTypes()包含。
- 追问:
- 标准回答:它是 HTTP 消息体与 Java 对象之间的双向转换器。工作原理是基于策略模式,通过
-
Spring MVC 是如何处理 @RequestBody 注解的?它和 @RequestParam 的处理方式有什么不同?
- 标准回答:
@RequestBody由RequestResponseBodyMethodProcessor处理,它通过HttpMessageConverter直接反序列化整个请求体为对象,不经过WebDataBinder。而@RequestParam由RequestParamMethodArgumentResolver处理,它从请求参数中获取单个值,然后通过ConversionService进行类型转换。前者是整体序列化,后者是字段级转换。 - 追问与加分:
- 追问:为什么它们的设计如此不同?
- 回答:因为它们面对的输入源不同。
@RequestParam处理键值对文本,天然适合字段级操作。@RequestBody处理复杂或多层的结构化数据(JSON/XML),整体解析为对象树是最高效和自然的做法。 - 追问:
@ModelAttribute呢? - 回答:
@ModelAttribute也是从请求参数中获取字段并绑定到对象的属性上,它会经过WebDataBinder和ConversionService,与@RequestParam是同一体系,但粒度更粗。
- 标准回答:
-
如果客户端希望同时支持 JSON 和 XML,应该怎么配置?
- 标准回答:首先确保 classpath 下有
jackson-databind和jaxb-api。其次,Controller 方法上不写或用数组声明produces/consumes包含两种类型。最后,通过accept头的q值或在ContentNegotiationConfigurer中配置favorParameter等方式来控制首选格式。 - 追问与加分:
- 追问:如果只用默认配置,同时支持 JSON 和 XML 的依赖都在,当请求
Accept头为空时,会返回哪个? - 回答:默认只有
HeaderContentNegotiationStrategy,空Accept头会被解析为*/*,此时会使用列表中的第一个能写入的转换器,通常是ByteArrayHttpMessageConverter或StringHttpMessageConverter。若要明确默认返回 JSON,必须配置defaultContentType。
- 追问:如果只用默认配置,同时支持 JSON 和 XML 的依赖都在,当请求
- 标准回答:首先确保 classpath 下有
-
内容协商的策略有哪些?Spring MVC 默认使用什么?
- 标准回答:主要有三种策略:基于请求头、基于参数、基于固定值。Spring MVC 默认使用基于请求头(
HeaderContentNegotiationStrategy)的策略。 - 追问与加分:
- 追问:为什么
PathExtensionContentNegotiationStrategy被废弃? - 回答:因为它依赖于 URL 后缀,与现代 REST URL 设计理念冲突,且容易引发安全风险(如 RFD 反射文件下载攻击)和歧义。
- 追问:如果同时配置了参数和请求头策略,谁的优先级高?
- 回答:取决于它们在
ContentNegotiationManager中strategies列表的顺序。通过ContentNegotiationConfigurer配置时,通常参数策略会被添加在请求头策略之前,使其优先级更高。
- 追问:为什么
- 标准回答:主要有三种策略:基于请求头、基于参数、基于固定值。Spring MVC 默认使用基于请求头(
-
MappingJackson2HttpMessageConverter 是如何将 JSON 转换为 Java 对象的?
- 标准回答:它内部持有一个
ObjectMapper实例。在read方法中,它根据目标类型创建一个ObjectReader,然后直接从HttpInputMessage的输入流中调用readValue进行反序列化。这个过程是流式的,节省内存。 - 追问与加分:
- 追问:如何自定义这个
ObjectMapper? - 回答:可以通过
Jackson2ObjectMapperBuilderCustomizerBean(推荐),或通过WebMvcConfigurer.extendMessageConverters完全替换转换器中的ObjectMapper。 - 追问:
ObjectMapper是线程安全的吗?ObjectReader呢? - 回答:
ObjectMapper是线程安全的,可以全局共享。ObjectReader和ObjectWriter同样是不可变且线程安全的。这个设计保证了在高并发下的性能和安全性。
- 追问:如何自定义这个
- 标准回答:它内部持有一个
-
如何自定义一个 HttpMessageConverter?需要注意什么?
- 标准回答:创建一个类实现
HttpMessageConverter或继承AbstractHttpMessageConverter,实现supports、readInternal、writeInternal方法,并在supports中严格校验类型。然后通过WebMvcConfigurer的extendMessageConverters或configureMessageConverters注册。 - 追问与加分:
- 追问:
extendMessageConverters和configureMessageConverters的区别? - 回答:前者在默认列表基础上添加,不会破坏原有功能。后者会清空默认列表,只使用你提供的。绝大多数场景应使用前者。
- 追问:自定义转换器应放在列表的什么位置?
- 回答:如果希望优先使用,应放在列表头部。特别是当你的转换器处理的媒体类型与默认转换器重叠时,排序至关重要。
- 追问:
- 标准回答:创建一个类实现
-
当 Accept 头包含多个媒体类型时,Spring MVC 如何决定用哪个?
- 标准回答:
HeaderContentNegotiationStrategy会解析Accept头,并根据q值(质量因子)和相对优先级对媒体类型列表进行排序。在writeWithMessageConverters中,Spring 会遍历这个排序后的列表,并尝试找到第一个匹配的转换器。这体现了客户端偏好的优先级。
- 标准回答:
-
415 和 406 错误的根源分别是什么?
- 标准回答:415 是服务端无法解析请求体,根源在于没有
HttpMessageConverter的canRead匹配请求的Content-Type。406 是服务端无法生成客户端可接受的响应体,根源在于没有HttpMessageConverter的canWrite匹配内容协商出的MediaType列表。
- 标准回答:415 是服务端无法解析请求体,根源在于没有
-
为什么通常建议避免使用 PathExtension 内容协商策略?
- 标准回答:主要有三点原因:一是使 URL 不纯净,与现代 RESTful 设计理念相悖;二是无法区分资源层级和格式后缀,如
/users/1.json是格式还是 ID?三是存在安全风险,如反射文件下载(RFD)攻击,可能被恶意构造的 URL 诱骗。 - 追问与加分:
- 追问:如果由于历史原因必须使用,如何配置?
- 回答:可以通过
ContentNegotiationConfigurer启用,但强烈建议禁用ignoreInvalidPathExtensions和useJaf等选项,并显式注册允许的扩展名映射,以避免歧义和安全问题。
- 标准回答:主要有三点原因:一是使 URL 不纯净,与现代 RESTful 设计理念相悖;二是无法区分资源层级和格式后缀,如
-
内置的消息转换器有哪些?它们的排序重要吗?
- 标准回答:重要的有
ByteArrayHttpMessageConverter、StringHttpMessageConverter、ResourceHttpMessageConverter、SourceHttpMessageConverter、FormHttpMessageConverter、MappingJackson2HttpMessageConverter、Jaxb2RootElementHttpMessageConverter。排序至关重要,因为遍历是顺序进行的。例如StringHttpMessageConverter的supports对String返回 true,如果它在 Jackson 之前,并且内容协商出application/json,它的canWrite会失败(因为它不支持application/json),遍历会继续。但如果内容协商返回*/*,StringHttpMessageConverter就会第一个匹配,导致返回的 JSON 被当成普通文本处理,可能被二次编码,产生乱码。 - 追问与加分:
- 追问:如果我想让我自定义的 JSON 转换器优先于默认的,但不想失去
ByteArray等处理能力,如何操作最好? - 回答:使用
extendMessageConverters,先removeIf移除默认的 Jackson 转换器,再add(0, myJacksonConverter)将其放在列表头部。
- 追问:如果我想让我自定义的 JSON 转换器优先于默认的,但不想失去
- 标准回答:重要的有
-
如何在 Spring Boot 中自定义 Jackson 的 ObjectMapper?它会影响消息转换器吗?
- 标准回答:最标准的方式是定义
Jackson2ObjectMapperBuilderCustomizerBean 或直接定义ObjectMapperBean。在 Spring Boot 自动配置下,Context 中的ObjectMapperBean 会自动注入到MappingJackson2HttpMessageConverter中,因此会影响消息转换器的行为。
- 标准回答:最标准的方式是定义
-
(系统设计题)设计一个支持多版本 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 探测动态添加;extend 较 configure 更安全。 |
| 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 头和时间格式配置。 |
延伸阅读
- Spring Framework 官方文档:Web on Servlet Stack -> Message Converters, Content Negotiation.
- 《Spring 实战 (第 5 版)》:第 2 章与第 6 章,提供了大量关于构建 REST API 的可执行示例。
- Jackson 官方文档:特别是
ObjectMapper、ObjectReader、ObjectWriter以及模块(如java-time)的部分,是深入掌握 JSON 处理的必备资料。 - JAXB 规范与教程:用于理解
@XmlRootElement等注解在对象与 XML 映射中扮演的角色。