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类型。当收到新请求时:
- Spring会根据"Accept"头确定响应的媒体类型
- 查找能处理该媒体类型的已注册转换器
- 使用该转换器序列化实体并返回响应
处理包含JSON的请求时流程类似:框架会根据"Content-Type"头确定请求体的媒体类型,然后查找能将客户端发送的请求体转换为Java对象的HttpMessageConverter
。
举个实际例子:
- 客户端发送GET请求到
/foos
,Accept
头设为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仓库中获取。