熟悉SpringMVC的同学都清楚接口的请求(request)与响应(response)涉及序列化与反序列化操作,如果我们想根据项目需求做点定制化操作,保险起见我们需要了解下HttpMessageConverter
接口工作流程及一些注意事项(仅针对HTTP)。
Message Conversion SpringMVC的请求与响应涉及HTTP协议,而HTTP请求和响应的传输是字节流,所以用Java编写的服务器必然涉及解析转换字节流的过程,当然这个过程Spring已经帮我们屏蔽了,我们接收请求时加一个注解@RequestBody
,响应时我们直接返回实体,十分方便。这个过程需要HttpMessageConverter
参与,HttpMessageConverter
是一个接口,定义了几个方法,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public interface HttpMessageConverter <T > { boolean canRead (Class<?> clazz, @Nullable MediaType mediaType) ; boolean canWrite (Class<?> clazz, @Nullable MediaType mediaType) ; List<MediaType> getSupportedMediaTypes () ; T read (Class<? extends T> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException ; void write (T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException ;}
熟悉IO编程的同学对read/write
模式应该非常熟悉,这个也正是对应将数据从流中读取出来和将数据写入到流中,MediaType
在HTTP中对应Content-Type
,是从HTTP的header里面取的,这个也无需多说,详情可以参考RFC文档:https://tools.ietf.org/html/rfc7231#section-3.1.1.1。
关于HttpMessageConverter
的实现,Spring为我们提供了超级多的实现,具体实现类参考:HttpMessageConverter Implementations ,比较常用的比如:StringHttpMessageConverter
,MappingJackson2HttpMessageConverter
等,同时,RestTemplate
里面也是用到这些转换器的,这里提一下。我们来关注一下AbstractHttpMessageConverter
抽象类,其他的具体实现类是继承了这个类的。
AbstractHttpMessageConverter
提供了一个Map类型的变量supportedMediaTypes
,用来表示HTTP请求与响应支持的Content-Type
类型,所以在自定义HttpMessageConverter
时,需要注意这个变量的赋值情况,否则会出现'Content-Type' cannot contain wildcard type '*'
这种错误,此时需要正确赋值,这个报错信息在如下代码中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public void setContentType (@Nullable MediaType mediaType) { if (mediaType != null ) { Assert.isTrue(!mediaType.isWildcardType(), "'Content-Type' cannot contain wildcard type '*'" ); Assert.isTrue(!mediaType.isWildcardSubtype(), "'Content-Type' cannot contain wildcard subtype '*'" ); set(CONTENT_TYPE, mediaType.toString()); } else { set(CONTENT_TYPE, null ); } }
注意如果响应时没有指定Content-Type
,会取supportedMediaTypes
中的第一个值,所以当程序初始化后,supportedMediaTypes
里面的值是*/*
的话,就会报错,这一点还是要注意的。具体赋值方式,可参考源码。
那么read
和 write
究竟是如何实现的呢?这个对于接收数据的不同类型,用的转换器不一样,实现也不一样。假设现在接收的是一个String
类型的参数,我们来看下StringHttpMessageConverter
如何处理。
StringHttpMessageConverter
继承自AbstractHttpMessageConverter
,所以只需重写readInternal
方法就可以了,代码如下:
1 2 3 4 5 @Override protected String readInternal (Class<? extends String> clazz, HttpInputMessage inputMessage) throws IOException { Charset charset = getContentTypeCharset(inputMessage.getHeaders().getContentType()); return StreamUtils.copyToString(inputMessage.getBody(), charset); }
代码也是十分简单,首先去Content-Type
拿到字符集,如果没有,默认是StandardCharsets.ISO_8859_1
,这个字符集大家估计都很熟悉了。接着将body里面的数据根据字符集转为字符串,返回即可。
同样的道理,如果返回的是字符串,流程刚好和接收时相反,代码如下,就不说了。
1 2 3 4 5 6 7 8 @Override protected void writeInternal (String str, HttpOutputMessage outputMessage) throws IOException { if (this .writeAcceptCharset) { outputMessage.getHeaders().setAcceptCharset(getAcceptedCharsets()); } Charset charset = getContentTypeCharset(outputMessage.getHeaders().getContentType()); StreamUtils.copy(str, charset, outputMessage.getBody()); }
另一个需要关注的实现类是MappingJackson2HttpMessageConverter
,我们知道Spring默认使用Jackson来做序列化与反序列化的,比如我们想格式化响应实体的时间格式,以及忽略值为空的字段等,我们可以利用MappingJackson2HttpMessageConverter
来完成我们自定义需求,下面是一个示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Bean public MappingJackson2HttpMessageConverter getMappingJackson2HttpMessageConverter () { MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter(); ObjectMapper objectMapper = new ObjectMapper(); SimpleDateFormat smt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss" ); objectMapper.setDateFormat(smt); objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false ); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY); mappingJackson2HttpMessageConverter.setObjectMapper(objectMapper); return mappingJackson2HttpMessageConverter; }
References: