2. 概述

本文将深入探讨如何在Spring中配置HttpMessageConverters

简单来说,消息转换器让我们能在HTTP通信中轻松实现Java对象与JSON/XML之间的序列化和反序列化。

3. 基础知识

3.1. 启用Web MVC

首先,Web应用需要配置Spring MVC支持。最便捷且高度可定制的方式是使用@EnableWebMvc注解:

@EnableWebMvc
@Configuration
@ComponentScan({ "com.baeldung.web" })
public class WebConfig implements WebMvcConfigurer {
    
    // ...
    
}

⚠️ 注意:该类实现了WebMvcConfigurer接口,这让我们能用自定义转换器覆盖默认的Http转换器列表。

3.2. 默认的消息转换器

Spring默认预置了以下HttpMessageConverter实现:

  • ByteArrayHttpMessageConverter – 处理字节数组
  • StringHttpMessageConverter – 处理字符串
  • ResourceHttpMessageConverter – 处理org.springframework.core.io.Resource类型的八位流
  • SourceHttpMessageConverter – 处理javax.xml.transform.Source
  • FormHttpMessageConverter – 处理表单数据与MultiValueMap<String, String>的转换
  • Jaxb2RootElementHttpMessageConverter – 处理Java对象与XML的转换(仅当JAXB2在类路径中时可用)
  • MappingJackson2HttpMessageConverter – 处理JSON(仅当Jackson 2在类路径中时可用)
  • MappingJacksonHttpMessageConverter – 处理JSON(仅当Jackson在类路径中时可用)
  • AtomFeedHttpMessageConverter – 处理Atom订阅(仅当Rome在类路径中时可用)
  • RssChannelHttpMessageConverter – 处理RSS订阅(仅当Rome在类路径中时可用)

4. 客户端-服务器通信——仅JSON模式

4.1. 高级内容协商

每个HttpMessageConverter实现都关联一个或多个MIME类型。当收到新请求时:

  1. Spring会根据"Accept"头确定响应的媒体类型
  2. 查找能处理该媒体类型的已注册转换器
  3. 使用该转换器序列化实体并返回响应

处理包含JSON的请求时流程类似:框架会根据"Content-Type"头确定请求体的媒体类型,然后查找能将客户端发送的请求体转换为Java对象的HttpMessageConverter

举个实际例子:

  • 客户端发送GET请求到/foosAccept头设为application/json,要求获取所有Foo资源的JSON表示
  • Foo控制器被触发,返回对应的Foo Java实体
  • Spring使用Jackson消息转换器将实体序列化为JSON

4.2. @ResponseBody注解

控制器方法上的@ResponseBody注解告诉Spring:方法返回值应直接序列化到HTTP响应体中。如前所述,客户端指定的"Accept"头将决定使用哪个Http转换器进行序列化:

@GetMapping("/{id}")
public @ResponseBody Foo findById(@PathVariable long id) {
    return fooService.findById(id);
}

客户端在请求中指定"Accept"头为application/json(例如使用curl命令):

curl --header "Accept: application/json" 
  http://localhost:8080/spring-boot-rest/foos/1

Foo类定义:

public class Foo {
    private long id;
    private String name;
}

HTTP响应体:

{
    "id": 1,
    "name": "Paul",
}

我们可以在控制器方法的参数上使用@RequestBody注解,表示HTTP请求体应反序列化为特定的Java实体。Spring将使用客户端请求中的"Content-Type"头确定合适的转换器:

@PutMapping("/{id}")
public @ResponseBody void update(@RequestBody Foo foo, @PathVariable String id) {
    fooService.update(foo);
}

现在用JSON对象调用该接口,指定"Content-Type"为application/json

curl -i -X PUT -H "Content-Type: application/json"  
-d '{"id":"83","name":"klik"}' http://localhost:8080/spring-boot-rest/foos/1

将收到成功的200 OK响应:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Length: 0
Date: Fri, 10 Jan 2014 11:18:54 GMT

5. 自定义转换器配置

我们可以通过实现WebMvcConfigurer接口并覆盖configureMessageConverters方法来自定义消息转换器:

@EnableWebMvc
@Configuration
@ComponentScan({ "com.baeldung.web" })
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> messageConverters) {
        messageConverters.add(createXmlHttpMessageConverter());
        messageConverters.add(new MappingJackson2HttpMessageConverter());
    }

    private HttpMessageConverter<Object> createXmlHttpMessageConverter() {
        MarshallingHttpMessageConverter xmlConverter = new MarshallingHttpMessageConverter();

        XStreamMarshaller xstreamMarshaller = new XStreamMarshaller();
        xmlConverter.setMarshaller(xstreamMarshaller);
        xmlConverter.setUnmarshaller(xstreamMarshaller);

        return xmlConverter;
    } 
}

这里我们创建了新的转换器MarshallingHttpMessageConverter,并使用Spring XStream支持进行配置。这种方式提供了极大的灵活性,因为我们直接操作底层编组框架(这里是XStream)的低级API,可以按需自由配置。

⚠️ 注意:此示例需要将XStream库添加到类路径中。另外,通过扩展这个支持类,我们会丢失之前预注册的默认消息转换器

当然,我们也可以用同样的方式配置Jackson,只需定义自己的MappingJackson2HttpMessageConverter,并在其上设置自定义的ObjectMapper

本例中XStream是选定的编组/解组实现,但也可以使用其他实现如JibxMarshaller

现在后端已启用XML支持,我们可以用XML表示调用API:

curl --header "Accept: application/xml" 
  http://localhost:8080/spring-boot-rest/foos/1

5.1. Spring Boot支持

如果使用Spring Boot,可以避免手动实现WebMvcConfigurer和添加所有消息转换器。只需在上下文中定义不同的HttpMessageConverter Bean,Spring Boot会自动将其添加到自动配置中

@Bean
public HttpMessageConverter<Object> createXmlHttpMessageConverter() {
    MarshallingHttpMessageConverter xmlConverter = new MarshallingHttpMessageConverter();

    // ...

    return xmlConverter;
}

6. 使用Spring的RestTemplate与HTTP消息转换器

除了服务端,HTTP消息转换器也可以在Spring的RestTemplate客户端配置。我们将根据需要为模板设置"Accept"和"Content-Type"头,然后通过完整的Foo资源编组/解组来消费REST API(支持JSON和XML)。

6.1. 不带Accept头获取资源

@Test
public void whenRetrievingAFoo_thenCorrect() {
    String URI = BASE_URI + "foos/{id}";

    RestTemplate restTemplate = new RestTemplate();
    Foo resource = restTemplate.getForObject(URI, Foo.class, "1");

    assertThat(resource, notNullValue());
}

6.2. 使用application/xml Accept头获取资源

现在明确以XML表示获取资源。我们定义一组转换器并设置到RestTemplate中:

@Test
public void givenConsumingXml_whenReadingTheFoo_thenCorrect() {
    String URI = BASE_URI + "foos/{id}";

    RestTemplate restTemplate = new RestTemplate();
    restTemplate.setMessageConverters(getXmlMessageConverters());

    HttpHeaders headers = new HttpHeaders();
    headers.setAccept(Collections.singletonList(MediaType.APPLICATION_XML));
    HttpEntity<String> entity = new HttpEntity<>(headers);

    ResponseEntity<Foo> response = 
      restTemplate.exchange(URI, HttpMethod.GET, entity, Foo.class, "1");
    Foo resource = response.getBody();

    assertThat(resource, notNullValue());
}

private List<HttpMessageConverter<?>> getXmlMessageConverters() {
    XStreamMarshaller marshaller = new XStreamMarshaller();
    marshaller.setAnnotatedClasses(Foo.class);
    MarshallingHttpMessageConverter marshallingConverter = 
      new MarshallingHttpMessageConverter(marshaller);

    List<HttpMessageConverter<?>> converters = new ArrayList<>();
    converters.add(marshallingConverter);
    return converters;
}

6.3. 使用application/json Accept头获取资源

类似地,现在通过请求JSON来消费REST API:

@Test
public void givenConsumingJson_whenReadingTheFoo_thenCorrect() {
    String URI = BASE_URI + "foos/{id}";

    RestTemplate restTemplate = new RestTemplate();
    restTemplate.setMessageConverters(getJsonMessageConverters());

    HttpHeaders headers = new HttpHeaders();
    headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
    HttpEntity<String> entity = new HttpEntity<String>(headers);

    ResponseEntity<Foo> response = 
      restTemplate.exchange(URI, HttpMethod.GET, entity, Foo.class, "1");
    Foo resource = response.getBody();

    assertThat(resource, notNullValue());
}

private List<HttpMessageConverter<?>> getJsonMessageConverters() {
    List<HttpMessageConverter<?>> converters = new ArrayList<>();
    converters.add(new MappingJackson2HttpMessageConverter());
    return converters;
}

6.4. 使用XML Content-Type更新资源

最后,我们向REST API发送JSON数据,并通过Content-Type头指定数据媒体类型:

@Test
public void givenConsumingXml_whenWritingTheFoo_thenCorrect() {
    String URI = BASE_URI + "foos";
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.setMessageConverters(getJsonAndXmlMessageConverters());

    Foo resource = new Foo("jason");
    HttpHeaders headers = new HttpHeaders();
    headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON));
    headers.setContentType((MediaType.APPLICATION_XML));
    HttpEntity<Foo> entity = new HttpEntity<>(resource, headers);

    ResponseEntity<Foo> response = 
      restTemplate.exchange(URI, HttpMethod.POST, entity, Foo.class);
    Foo fooResponse = response.getBody();

    assertThat(fooResponse, notNullValue());
    assertEquals(resource.getName(), fooResponse.getName());
}

private List<HttpMessageConverter<?>> getJsonAndXmlMessageConverters() {
    List<HttpMessageConverter<?>> converters = getJsonMessageConverters();
    converters.addAll(getXmlMessageConverters());
    return converters;
}

有趣的是,这里我们混合使用了媒体类型:发送XML数据但要求服务器返回JSON数据。这充分展示了Spring转换机制的强大能力。

7. 总结

本文我们学习了Spring MVC如何让我们指定并完全自定义Http消息转换器,实现Java实体与XML/JSON的自动编组和解组。当然这只是基础定义,消息转换机制能做的远不止这些(如最后一个测试示例所示)。

我们还探讨了如何在RestTemplate客户端利用同样的强大机制,实现完全类型安全的API消费方式。

本文所有代码示例可在GitHub仓库中获取。


原始标题:Http Message Converters with the Spring Framework