消息转换

Spring MVC 通过 HttpMessageConverter 将 HTTP 请求体/响应体与 Java 对象互转,@RequestBody@ResponseBodyRestTemplateWebClient 均依赖该机制。


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/canWritetrue 的转换器。


内置转换器

转换器MediaType说明
ByteArrayHttpMessageConverter*/*字节数组
StringHttpMessageConvertertext/plain, */*字符串,默认 ISO-8859-1
ResourceHttpMessageConverter*/*Resource 文件下载
FormHttpMessageConverterapplication/x-www-form-urlencoded, multipart/form-data表单数据
MappingJackson2HttpMessageConverterapplication/jsonJSON ↔ Java 对象(需 jackson-databind)
MappingJackson2XmlHttpMessageConverterapplication/xmlXML ↔ Java 对象(需 jackson-dataformat-xml)
Jaxb2RootElementHttpMessageConverterapplication/xmlJAXB XML
ProtobufHttpMessageConverterapplication/x-protobufProtobuf

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

  1. URL 后缀(已废弃):/user.json
  2. 请求参数/user?format=json
  3. 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);
    }
}

统一响应格式定义参见 统一响应格式


相关链接