消息转换
Spring MVC 通过 HttpMessageConverter 将 HTTP 请求体/响应体与 Java 对象互转,@RequestBody、@ResponseBody、RestTemplate、WebClient 均依赖该机制。
HttpMessageConverter 接口
public interface HttpMessageConverter<T> {
// 判断能否读取(请求体 → Java 对象)
boolean canRead(Class<?> clazz, MediaType mediaType);
// 判断能否写入(Java 对象 → 响应体)
boolean canWrite(Class<?> clazz, MediaType mediaType);
// 支持的 MediaType 列表
List<MediaType> getSupportedMediaTypes();
T read(Class<? extends T> clazz, HttpInputMessage inputMessage);
void write(T t, MediaType contentType, HttpOutputMessage outputMessage);
}Spring Boot 按照注册顺序依次匹配,找到第一个 canRead/canWrite 为 true 的转换器。
内置转换器
| 转换器 | MediaType | 说明 |
|---|---|---|
ByteArrayHttpMessageConverter | */* | 字节数组 |
StringHttpMessageConverter | text/plain, */* | 字符串,默认 ISO-8859-1 |
ResourceHttpMessageConverter | */* | Resource 文件下载 |
FormHttpMessageConverter | application/x-www-form-urlencoded, multipart/form-data | 表单数据 |
MappingJackson2HttpMessageConverter | application/json | JSON ↔ Java 对象(需 jackson-databind) |
MappingJackson2XmlHttpMessageConverter | application/xml | XML ↔ Java 对象(需 jackson-dataformat-xml) |
Jaxb2RootElementHttpMessageConverter | application/xml | JAXB XML |
ProtobufHttpMessageConverter | application/x-protobuf | Protobuf |
Jackson 配置(最常用)
全局 ObjectMapper
spring:
jackson:
# 日期序列化为时间戳(false 则输出 ISO-8601 字符串)
serialization:
write-dates-as-timestamps: false
# 遇到未知字段不报错
deserialization:
fail-on-unknown-properties: false
# null 字段不序列化
default-property-inclusion: non_null
# 时区
time-zone: Asia/Shanghai
date-format: "yyyy-MM-dd HH:mm:ss"自定义 ObjectMapper Bean
@Configuration
public class JacksonConfig {
@Bean
@Primary
public ObjectMapper objectMapper() {
return Jackson2ObjectMapperBuilder.json()
// 驼峰 → 下划线
.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE)
// 枚举序列化为 name 字符串而非序号
.featuresToEnable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING)
// 忽略未知属性
.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
// Java 8 时间支持
.modules(new JavaTimeModule())
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
}
}字段级注解
@Data
public class UserDTO {
@JsonProperty("user_id") // 序列化/反序列化时使用 user_id
private Long id;
@JsonIgnore // 不序列化此字段
private String password;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
private LocalDateTime createdAt;
@JsonInclude(JsonInclude.Include.NON_NULL) // 此字段为 null 时不输出
private String remark;
@JsonAlias({"nick", "nickname"}) // 反序列化时接受多个别名
private String nickName;
}自定义序列化/反序列化
// 自定义序列化:Long → String(防止前端 JS 精度丢失)
public class LongToStringSerializer extends JsonSerializer<Long> {
@Override
public void serialize(Long value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
gen.writeString(value == null ? null : value.toString());
}
}
// 自定义反序列化
public class StringToLongDeserializer extends JsonDeserializer<Long> {
@Override
public Long deserialize(JsonParser p, DeserializationContext ctx)
throws IOException {
String text = p.getText();
return text == null || text.isEmpty() ? null : Long.parseLong(text);
}
}
// 使用
@JsonSerialize(using = LongToStringSerializer.class)
@JsonDeserialize(using = StringToLongDeserializer.class)
private Long orderId;
// 或全局注册
objectMapperBuilder.serializerByType(Long.class, new LongToStringSerializer());自定义 HttpMessageConverter
以注册一个 CSV 转换器为例:
public class CsvHttpMessageConverter extends AbstractHttpMessageConverter<List<String[]>> {
public CsvHttpMessageConverter() {
super(new MediaType("text", "csv"));
}
@Override
protected boolean supports(Class<?> clazz) {
return List.class.isAssignableFrom(clazz);
}
@Override
protected List<String[]> readInternal(Class<? extends List<String[]>> clazz,
HttpInputMessage inputMessage) throws IOException {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(inputMessage.getBody()))) {
return reader.lines()
.map(line -> line.split(","))
.collect(Collectors.toList());
}
}
@Override
protected void writeInternal(List<String[]> data,
HttpOutputMessage outputMessage) throws IOException {
try (PrintWriter writer = new PrintWriter(outputMessage.getBody())) {
data.forEach(row -> writer.println(String.join(",", row)));
}
}
}
// 注册
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new CsvHttpMessageConverter());
}
// extendMessageConverters 可在不替换默认列表的前提下追加
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 调整 Jackson 转换器的顺序(移至首位)
converters.removeIf(c -> c instanceof MappingJackson2HttpMessageConverter);
converters.add(0, new MappingJackson2HttpMessageConverter(customObjectMapper()));
}
}内容协商(Content Negotiation)
Spring MVC 根据以下策略确定响应的 MediaType:
- URL 后缀(已废弃):
/user.json - 请求参数:
/user?format=json - Accept 请求头(默认):
Accept: application/json
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer
.favorParameter(true) // 启用 ?format=xxx 参数协商
.parameterName("format")
.ignoreAcceptHeader(false) // 仍支持 Accept 头
.defaultContentType(MediaType.APPLICATION_JSON)
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML)
.mediaType("csv", new MediaType("text", "csv"));
}
}控制器返回同一对象,框架根据客户端请求的 MediaType 选择转换器:
@GetMapping(value = "/users/{id}",
produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
public UserDTO getUser(@PathVariable Long id) {
return userService.getById(id);
}RestTemplate 中的消息转换
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
// 查看默认注册的转换器
restTemplate.getMessageConverters().forEach(c ->
log.info("Converter: {}", c.getClass().getSimpleName()));
// 替换 Jackson 转换器(使用自定义 ObjectMapper)
restTemplate.getMessageConverters().removeIf(
c -> c instanceof MappingJackson2HttpMessageConverter);
restTemplate.getMessageConverters().add(
new MappingJackson2HttpMessageConverter(customObjectMapper()));
return restTemplate;
}RestTemplate 详细用法参见 RestTemplate,响应式客户端参见 WebClient。
WebClient 中的消息转换
@Bean
public WebClient webClient() {
// 自定义 CodecConfigurer 来配置编解码器
return WebClient.builder()
.codecs(configurer -> {
configurer.defaultCodecs()
.jackson2JsonEncoder(new Jackson2JsonEncoder(customObjectMapper()));
configurer.defaultCodecs()
.jackson2JsonDecoder(new Jackson2JsonDecoder(customObjectMapper()));
// 调大 buffer 限制(默认 256KB,大响应体时需调整)
configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024);
})
.build();
}统一响应格式与消息转换
配合 @ControllerAdvice + ResponseBodyAdvice 在转换前统一包装响应体,无需每个接口手动包装:
@RestControllerAdvice
public class ResponseWrapAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
// 已经是 ApiResponse 的跳过,防止二次包装
return !returnType.getParameterType().isAssignableFrom(ApiResponse.class);
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
return ApiResponse.success(body);
}
}统一响应格式定义参见 统一响应格式。