HttpMessageConverter(消息转换器),POJO转JSON之原理解析、内容协商原理(1)都有涉及到,本篇将稍作总结,并实现一个自定义的消息转换器。
HttpMessageConverter的定义
public interface HttpMessageConverter<T> {
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
List<MediaType> getSupportedMediaTypes();
default List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
return (canRead(clazz, null) || canWrite(clazz, null) ?
getSupportedMediaTypes() : Collections.emptyList());
}
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;
}
HttpMessageConverter的实现类
HttpMessageConverter的实现类挺多的,如
- MappingJackson2HttpMessageConverter,可以将POJO转换为JSON,作为响应返回给浏览器。
- MappingJackson2XmlHttpMessageConverter,可以将POJO转换为XML,作为响应返回给浏览器。
自定义消息转换器
创建基础项目
- 新建一个spring项目:helloworld。
Group:com.example
Artifact:helloworld
Package name:com.example.boot
添加依赖:Lombok、Spring Web和Spring Configuration Processor。其中,spring-boot-starter-web>spring-boot-starter-jackson里的jackson jar能够返回JSON。
继续添加依赖:jackson-dataformat-xml,它可以返回XML。
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.13.1</version>
</dependency>
</dependencies>
- com.example.boot下新建类:bean.Person。
package com.example.boot.bean;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.Date;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Person {
private String name;
private Integer age;
private Date birth;
}
- com.example.boot下新建控制器:controller.HelloController。
package com.example.boot.controller;
import com.example.boot.bean.Person;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.util.Date;
@Controller
public class HelloController {
@GetMapping("/person")
@ResponseBody
public Person person(){
Person person = new Person("Tom", 1, new Date());
return person;
}
}
- helloworld根目录下新建目录:http,http目录下新建文件:helloworld.http。
GET localhost:8080/person
Accept: application/json
###
GET localhost:8080/person
Accept: application/xml
###
- 启动应用,访问接口localhost:8080/person。
Accept: application/json 时,接口返回的信息如下。
GET http://localhost:8080/person
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 07 Jan 2022 07:23:02 GMT
Keep-Alive: timeout=60
Connection: keep-alive
{
"name": "Tom",
"age": 1,
"birth": "2022-01-07T07:23:02.900+00:00"
}
Accept: application/xml 时,接口返回的信息如下。
GET http://localhost:8080/person
HTTP/1.1 200
Content-Type: application/xml;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 07 Jan 2022 07:26:42 GMT
Keep-Alive: timeout=60
Connection: keep-alive
<Person>
<name>Tom</name>
<age>1</age>
<birth>2022-01-07T07:26:42.502+00:00</birth>
</Person>
自定义消息转换器
- com.example.boot下新建消息转换器:converter.HelloMessageConverter。
package com.example.boot.converter;
import com.example.boot.bean.Person;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List;
public class HelloMessageConverter implements HttpMessageConverter<Person> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return clazz.isAssignableFrom(Person.class);
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-helloworld");
}
@Override
public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
@Override
public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
String data = person.getName()+";"+person.getAge()+";"+person.getBirth();
OutputStream body = outputMessage.getBody();
body.write(data.getBytes());
}
}
- com.example.boot下新建配置类:config.MyConfig。
package com.example.boot.config;
import com.example.boot.converter.HelloMessageConverter;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new HelloMessageConverter());
}
}
- helloworld.http中新增接口且Accept字段值为自定义类型:application/x-helloworld
GET localhost:8080/person
Accept: application/json
###
GET localhost:8080/person
Accept: application/xml
###
GET localhost:8080/person
Accept: application/x-helloworld
###
- 启动应用,访问接口localhost:8080/person。
Accept: application/x-helloworld 时,接口返回的信息如下。
GET http://localhost:8080/person
HTTP/1.1 200
Content-Length: 34
Date: Fri, 07 Jan 2022 07:50:36 GMT
Keep-Alive: timeout=60
Connection: keep-alive
Tom;1;Fri Jan 07 15:50:36 CST 2022
想了解SpringMVC底层所有的消息转换器是怎么配置的,可以查看或调试以下源码。
- WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#configureMessageConverters
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
this.messageConvertersProvider
.ifAvailable((customConverters) -> converters.addAll(customConverters.getConverters()));
}
- HttpMessageConverters#getConverters和HttpMessageConverters#getDefaultConverters
public HttpMessageConverters(boolean addDefaultConverters, Collection<HttpMessageConverter<?>> converters) {
List<HttpMessageConverter<?>> combined = getCombinedConverters(converters,
addDefaultConverters ? getDefaultConverters() : Collections.emptyList());
//...
}
public List<HttpMessageConverter<?>> getConverters() {
return this.converters;
}
private List<HttpMessageConverter<?>> getDefaultConverters() {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
if (ClassUtils.isPresent("org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport",
null)) {
converters.addAll(new WebMvcConfigurationSupport() {
public List<HttpMessageConverter<?>> defaultMessageConverters() {
return super.getMessageConverters();
}
}.defaultMessageConverters());
}
//...
}
- WebMvcConfigurationSupport#getMessageConverters
protected final List<HttpMessageConverter<?>> getMessageConverters() {
if (this.messageConverters == null) {
this.messageConverters = new ArrayList<>();
configureMessageConverters(this.messageConverters);
if (this.messageConverters.isEmpty()) {
addDefaultHttpMessageConverters(this.messageConverters);
}
extendMessageConverters(this.messageConverters);
}
return this.messageConverters;
}
- WebMvcConfigurationSupport#addDefaultHttpMessageConverters
//WebMvcConfigurationSupport
public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
private static final boolean jackson2Present;
private static final boolean jackson2XmlPresent;
static {
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
}
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcValidator") Validator validator) {
if (jackson2XmlPresent) {
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
if (jackson2Present) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json();
messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build()));
}
}