使用Spring框架的Http消息转换器

2023/05/12

1. 概述

在本教程中,我们将学习如何在Spring中配置HttpMessageConverters

简而言之,我们可以使用消息转换器通过HTTP将Java对象编组到JSON和XML以及从中解组。

延伸阅读

Spring MVC内容协商

在Spring MVC应用程序中配置内容协商以及启用和禁用各种可用策略的指南。

阅读更多

使用Spring MVC返回图像/媒体数据

本文展示了使用Spring MVC返回图像(或其他媒体)的备选方案,并讨论了每种方法的优缺点。

阅读更多

Spring REST API中的二进制数据格式

在本文中,我们探讨了如何配置Spring REST机制以利用我们用Kryo说明的二进制数据格式。此外,我们还展示了如何使用Google Protocol Buffers支持多种数据格式。

阅读更多

2. 基础知识

2.1 启用Web MVC

首先,需要为Web应用程序配置Spring MVC支持。执行此操作的一种方便且非常可自定义的方法是使用@EnableWebMvc注解:

@EnableWebMvc
@Configuration
@ComponentScan({ "cn.tuyucheng.taketoday.web" })
public class WebConfig implements WebMvcConfigurer {
    // ...
}

请注意,此类实现了WebMvcConfigurer,这将允许我们使用自己的Http转换器更改默认列表。

2.2 默认消息转换器

默认情况下,以下HttpMessageConverters实例处于预启用状态:

  • ByteArrayHttpMessageConverter:转换字节数组
  • StringHttpMessageConverter:转换字符串
  • ResourceHttpMessageConverter:将org.springframework.core.io.Resource转换为任何类型的八位字节流
  • SourceHttpMessageConverter:转换javax.xml.transform.Source
  • FormHttpMessageConverter:将表单数据与MultiValueMap<String, String>相互转换
  • Jaxb2RootElementHttpMessageConverter:将Java对象与XML相互转换(仅当类路径中存在JAXB 2时才添加)
  • MappingJackson2HttpMessageConverter:转换JSON(仅当类路径中存在Jackson 2时才添加)
  • MappingJacksonHttpMessageConverter:转换JSON(仅当类路径中存在Jackson时才添加)
  • AtomFeedHttpMessageConverter:转换Atom提要(仅当类路径中存在Rome时才添加)
  • RssChannelHttpMessageConverter:转换RSS提要(仅当类路径中存在Rome时才添加)

3. 客户端服务器通信-仅JSON

3.1 高级内容协商

每个HttpMessageConverter实现都有一个或多个关联的MIME类型。

当收到新请求时,Spring将使用“Accept”标头来确定它需要响应的媒体类型

然后它将尝试找到能够处理该特定媒体类型的已注册转换器。最后,它将使用找到的转换器来转换实体并发回响应。

该过程类似于接收包含JSON信息的请求。框架将使用“Content-Type”标头来确定请求正文的媒体类型

然后它将搜索可以将客户端发送的正文转换为Java对象的HttpMessageConverter。

让我们用一个简单的例子来阐明这一点:

  • 客户端向/foos发送GET请求,Accept头设置为application/json,以JSON形式获取所有Foo资源。
  • 命中Foo Spring Controller,并返回相应的Foo Java实体。
  • 然后Spring使用Jackson消息转换器之一将实体编组为JSON。

现在让我们看一下它是如何工作的,以及我们如何利用@ResponseBody和@RequestBody注解。

3.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"
}

3.3 @RequestBody

我们可以在控制器方法的参数上使用@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/1p://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 2022 11:18:54 GMT

4. 自定义转换器配置

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

@EnableWebMvc
@Configuration
@ComponentScan({ "cn.tuyucheng.taketoday.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支持对其进行配置。这提供了很大的灵活性,因为我们正在使用底层编组框架的低级API,在本例中为XStream,我们可以根据需要对其进行配置。

请注意,此示例需要将XStream库添加到类路径中。

另请注意,通过扩展此支持类,我们将丢失先前预注册的默认消息转换器

当然,我们现在可以通过定义我们自己的MappingJackson2HttpMessageConverter来为Jackson做同样的事情。我们可以在此转换器上设置自定义ObjectMapper,并根据需要对其进行配置。

在这种情况下,XStream是选定的编组器/解组器实现,但也可以使用其他实现,如JibxMarshaller。

此时,在后端启用XML的情况下,我们可以将API与XML表示形式一起使用:

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

4.1 Spring Boot支持

如果我们使用Spring Boot,我们可以避免像上面那样实现WebMvcConfigurer并手动添加所有消息转换器。

我们可以在上下文中定义不同的HttpMessageConverter bean,Spring Boot会自动将它们添加到它创建的自动配置中

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

    // ...

    return xmlConverter;
}

5. 将Spring的RestTemplate与HTTP消息转换器结合使用

与服务器端一样,可以在Spring RestTemplate的客户端配置HTTP消息转换。

我们将在适当的时候使用“Accept”和“Content-Type”标头配置模板。然后我们将尝试通过Foo资源的完整编组和解组来使用REST API,包括JSON和XML。

5.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());
}

5.2 使用application/xml Accept标头检索资源

现在让我们以XML表示形式显式检索资源。我们将定义一组转换器并在RestTemplate上设置它们。

因为我们使用的是XML,所以我们将使用与以前相同的XStream编组器:

@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;
}

5.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;
}

5.4 使用XML内容类型更新资源

最后,我们将JSON数据发送到REST API,并通过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转换机制的真正强大之处。

6. 总结

在本文中,我们了解了Spring MVC如何允许我们指定和完全自定义Http消息转换器以自动将Java实体编组/解组到XML或JSON。当然,这是一个简单的定义,消息转换机制可以做的还有很多,正如我们从上一个测试示例中看到的那样。

我们还研究了如何利用与RestTemplate客户端相同的强大机制,从而以一种完全类型安全的方式使用API。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章