springboot学习(三)——使用HttpMessageConverter进行http序列化和反序列化

以下内容,如有问题,烦请指出,谢谢!


对象的序列化/反序列化大家应该都比较熟悉:序列化就是将object转化为可以传输的二进制,反序列化就是将二进制转化为程序内部的对象。序列化/反序列化主要体现在程序I/O这个过程中,包括网络I/O和磁盘I/O。

那么什么是http序列化和反序列化呢?

在使用springmvc时,我们经常会这样写:

@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    private UserService userService;

    @GetMapping("/{id}")
    public User getUserById(@PathVariable long id) {
        return userService.getUserById(id);
    }

    @PostMapping
    public User createUser(@RequestBody User user) {
        System.err.println("create an user: " + user);
        return user;
    }
}

@RestController中有@ResponseBody,可以帮我们把User序列化到resp.body中。@RequestBody可以帮我们把req.body的内容转化为User对象。如果是开发Web应用,一般这两个注解对应的就是Json序列化和反序列化的操作。这里实际上已经体现了Http序列化/反序列化这个过程,只不过和普通的对象序列化有些不一样,Http序列化/反序列化的层次更高,属于一种Object2Object之间的转换。

有过Netty使用经验的对这个应该比较了解,Netty中的Decoder和Encoder就有两种基本层次,层次低的一种是Byte <---> Message,二进制与程序内部消息对象之间的转换,就是常见的序列化/反序列化;另外一种是 Message <---> Message,程序内部对象之间的转换,比较高层次的序列化/反序列化。

Http协议的处理过程,TCP字节流 <---> HttpRequest/HttpResponse <---> 内部对象,就涉及这两种序列化。在springmvc中第一步已经由Servlet容器(tomcat等等)帮我们处理了,第二步则主要由框架帮我们处理。上面所说的Http序列化/反序列化就是指的这第二个步骤,它是controller层框架的核心功能之一,有了这个功能,就能大大减少代码量,让controller的逻辑更简洁清晰,就像上面示意的代码那样,方法中只有一行代码。

spirngmvc进行第二步操作,也就是Http序列化和反序列化的核心是HttpMessageConverter。用过老版本springmvc的可能有些印象,那时候需要在xml配置文件中注入MappingJackson2HttpMessageConverter这个类型的bean,告诉springmvc我们需要进行Json格式的转换,它就是HttpMessageConverter的一种实现。

springmvc消息转换过程

在Web开发中我们经常使用Json相关框架来进行第二步操作,这是因为Web应用中主要开发语言是js,对Json支持非常好。但是Json也有很大的缺点,大多数Json框架对循环引用支持不够好,并且Json报文体积通常比较大,相比一些二进制序列化更耗费流量。很多移动应用也使用Http进行通信,因为这是在手机app中,Json格式报文并没有什么特别的优势。这种情况下我们可能会需要一些性能更好,体积更小的序列化框架,比如Protobuf等等。

当前的SpringMVC 4.3版本已经集成了Protobuf的Converter,org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter,使用这个类可以进行Protobuf中的Message类和http报文之间的转换。使用方式很简单,先依赖Protobuf相关的jar,代码中直接@Bean就行,像下面这样,springboot会自动注入并添加这种Converter。

    @Bean
    public ProtobufHttpMessageConverter protobufHttpMessageConverter() {
        return new ProtobufHttpMessageConverter();
    }

这里就不演示protobuf相关的内容了。

另外有很重要的一点需要说明一下,springmvc可以同时配置多个Converter,根据一定的规则(主要是Content-Type、Accept、controller方法的consumes/produces、Converter.mediaType以及Converter的排列顺序这四个属性)来选择到底是使用哪一个,这使得springmvc能够一个接口支持多种报文格式。这个规则的具体内容,下一篇再详细说明。


下面重点说下如何自定义一个HttpMessageConverter,就用Java原生序列化为例,叫作JavaSerializationConverter,基本仿照ProtobufHttpMessageConverter来写。

首先继承AbstractHttpMessageConverter,泛型类这里有几种方式可以选择:

  • 最广的可以选择Object,不过Object并不都是可以序列化的,但是可以在覆盖的supports方法中进一步控制,因此选择Object是可以的
  • 最符合的是Serializable,既完美满足泛型定义,本身也是个Java序列化/反序列化的充要条件
  • 自定义的基类Bean,有些技术规范要求自己代码中的所有bean都继承自同一个自定义的基类BaseBean,这样可以在Serializable的基础上再进一步控制,满足自己的业务要求

这里选择Serializable作为泛型基类。

其次是选择一个MediaType,使得springmvc能够根据Accept和Content-Type唯一确定是要使用JavaSerializationConverter,所以这个MediaType不能是通用的text/plain、application/json、*/*这种,得特殊一点,这里就用application/x-java-serialization;charset=UTF-8。因为Java序列化是二进制数据,charset不是必须的,但是MediaType的构造方法中需要指定一个charset,这里就用UTF-8。

最后,二进制在电脑上不是可以直接拷贝的内容,为了方便测试,使用Base64再处理一遍,这样就显示成正常文本了,便于测试。

整个代码如下:

package pr.study.springboot.configure.mvc.converter;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.Base64;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.util.StreamUtils;

public class JavaSerializationConverter extends AbstractHttpMessageConverter<Serializable> {
    private Logger LOGGER = LoggerFactory.getLogger(JavaSerializationConverter.class);

    public JavaSerializationConverter() {
        // 构造方法中指明consumes(req)和produces(resp)的类型,指明这个类型才会使用这个converter
        super(new MediaType("application", "x-java-serialization", Charset.forName("UTF-8")));
    }

    @Override
    protected boolean supports(Class<?> clazz) {
        // 使用Serializable,这里可以直接返回true
        // 使用object,这里还要加上Serializable接口实现类判断
        // 根据自己的业务需求加上其他判断
        return true;
    }

    @Override
    protected Serializable readInternal(Class<? extends Serializable> clazz, HttpInputMessage inputMessage)
            throws IOException, HttpMessageNotReadableException {
        byte[] bytes = StreamUtils.copyToByteArray(inputMessage.getBody());
        // base64使得二进制数据可视化,便于测试
        ByteArrayInputStream bytesInput = new ByteArrayInputStream(Base64.getDecoder().decode(bytes));
        ObjectInputStream objectInput = new ObjectInputStream(bytesInput);
        try {
            return (Serializable) objectInput.readObject();
        } catch (ClassNotFoundException e) {
            LOGGER.error("exception when java deserialize, the input is:{}", new String(bytes, "UTF-8"), e);
            return null;
        }
    }

    @Override
    protected void writeInternal(Serializable t, HttpOutputMessage outputMessage)
            throws IOException, HttpMessageNotWritableException {
        ByteArrayOutputStream bytesOutput = new ByteArrayOutputStream();
        ObjectOutputStream objectOutput = new ObjectOutputStream(bytesOutput);
        objectOutput.writeObject(t);
        // base64使得二进制数据可视化,便于测试
        outputMessage.getBody().write(Base64.getEncoder().encode(bytesOutput.toByteArray()));
    }

}

添加一个converter的方式有三种,代码以及说明如下:

    // 添加converter的第一种方式,代码很简单,也是推荐的方式
    // 这样做springboot会把我们自定义的converter放在顺序上的最高优先级(List的头部)
    // 即有多个converter都满足Accpet/ContentType/MediaType的规则时,优先使用我们这个
    @Bean
    public JavaSerializationConverter javaSerializationConverter() {
        return new JavaSerializationConverter();
    }

    // 添加converter的第二种方式
    // 通常在只有一个自定义WebMvcConfigurerAdapter时,会把这个方法里面添加的converter(s)依次放在最高优先级(List的头部)
    // 虽然第一种方式的代码先执行,但是bean的添加比这种方式晚,所以方式二的优先级 大于 方式一
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        // add方法可以指定顺序,有多个自定义的WebMvcConfigurerAdapter时,可以改变相互之间的顺序
        // 但是都在springmvc内置的converter前面
        converters.add(new JavaSerializationConverter());
    }

    // 添加converter的第三种方式
    // 同一个WebMvcConfigurerAdapter中的configureMessageConverters方法先于extendMessageConverters方法执行
    // 可以理解为是三种方式中最后执行的一种,不过这里可以通过add指定顺序来调整优先级,也可以使用remove/clear来删除converter,功能强大
    // 使用converters.add(xxx)会放在最低优先级(List的尾部)
    // 使用converters.add(0,xxx)会放在最高优先级(List的头部)
    @Override
    public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(new JavaSerializationConverter());
    }

使用下面的数据演示:

// java序列化
rO0ABXNyAB1wci5zdHVkeS5zcHJpbmdib290LmJlYW4uVXNlcrt1879rvWjlAgAESgACaWRMAApjcmVhdGVUaW1ldAAQTGphdmEvdXRpbC9EYXRlO0wABWVtYWlsdAASTGphdmEvbGFuZy9TdHJpbmc7TAAEbmFtZXEAfgACeHIAIXByLnN0dWR5LnNwcmluZ2Jvb3QuYmVhbi5CYXNlQmVhbklx6Fsr8RKpAgAAeHAAAAAAAAAAe3NyAA5qYXZhLnV0aWwuRGF0ZWhqgQFLWXQZAwAAeHB3CAAAAWCs8ufxeHQAEGhlbGxvd29ybGRAZy5jb210AApoZWxsb3dvcmxk

// json
{"id":123,"name":"helloworld","email":"helloworld@g.com","createTime":"2017-12-31 22:21:28"}

// 对应的user.toString()
User[id=123, name=helloworld, email=helloworld@g.com, createTime=Sun Dec 31 22:21:28 CST 2017]

演示结果如下,包含了一个接口多种报文格式支持的演示:

1、请求是 GET + Accept: application/x-java-serialization,返回的是 Content-Type: application/x-java-serialization;charset=UTF-8 的Java序列化格式的报文

2、请求是 GET + Accept: application/json,返回的是 Content-Type: application/json;charset=UTF-8 的json格式报文

3、请求是 POST + Accept: application/x-java-serialization + Content-Type: application/x-java-serialization,返回的是 Content-Type: application/x-java-serialization;charset=UTF-8的Java序列化格式的报文

4、请求是 POST + Accept: application/json + Content-Type: application/x-java-serialization,返回的是 Content-Type: application/json;charset=UTF-8 的json格式报文

5、请求是 POST + Accept: application/json + Content-Type: application/json,返回的是 Content-Type: application/json;charset=UTF-8 的json格式报文

6、请求是 POST + Accept: application/x-java-serialization + Content-Type: application/json,返回的是 Content-Type: application/x-java-serialization;charset=UTF-8的Java序列化格式的报文


下面再说些其他的有关Http序列化/反序列化的内容.
1、jackson配置
使用Jackson时,一般我们都会配置下ObjectMapper,常见的两个是时间序列化格式,以及是否序列化null值。使用springboot时,因为Jackson是内置加载的,那么如何配置我们想要的的Jackson属性呢?最贱的的方式,那就是自己注入一个ObjectMapper实例,这样spring内所有通过依赖注入使用ObjectMapper的地方,都会优先使用我们自己注入的那个,JacksonConverter也不例外。

/**
 * jackson的核心是ObjectMapper,在这里配置ObjectMapper来控制springboot使用的jackson的某些功能
 */
@Configuration
public class MyObjectMpper {

    @Bean
    public ObjectMapper getObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setSerializationInclusion(Include.NON_NULL); // 不序列化null的属性
        mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); // 默认的时间序列化格式
        return mapper;
    }
}

2、控制Json中某些属性的序列化方式
官方文档中说了个Custom JSON Serializers and Deserializers,我也没想到怎么用这个,后来网上发现了个比较好的例子,说的是rgb颜色的序列化。web页面需要的是css格式的rgb颜色,服务的提供的可能是三个独立的byte型数字,这时候就需要改变颜色属性的json序列化/反序列化方式。具体可以看看这里

3、FastJson配置
可能某些时候需要使用FastJson,这时候该如何配置呢?基本上和springmvc xml配置差不多,注入一个FastJsonHttpMessageConverter就行了。最简单的就是上面的配置JavaSerializationConverter的方式一,方式二和方式三也都行。
不过会有奇怪的问题出现(使用@JSONField(serialize=false, deserialize=false)注解createTime,用以区分FastJson和Jackson):
假如你把FastJson配置为优先级最高的,并且同时配置上JavaSerializationConverter,你会发现JavaSerializationConverter不管用了,请求是 GET + Accept: application/x-java-serialization,返回是 Content-Type: application/x-java-serialization;charset=UTF-8;,但是实际内容是json格式的,如下。

假如你把FastJson配置为优先级最低的,别的不管,你以为得到会是Jackson序列化后的结果。但实际上,你用浏览器直接敲得到的是FastJson的,用上面的 GET 的 fiddler结果是jackson的;

详细原因在下一篇讲解converter匹配规则时说。

这里说下原因中重要且值得吐槽的一点,那就是FastJsonHttpMessageConverter默认注册的MediaType的 */*,然后就有了上面的 请求是 GET + Accept: application/x-java-serialization,返回是 Content-Type: application/x-java-serialization;charset=UTF-8;,但是实际内容是json格式的,这种挂羊头卖狗肉的行为,明着违反HTTP协议的规范。

这个代码设计真是差,json框架就该只管json,这样霸道,什么格式都要管,为哪般!?


相关代码:
https://gitee.com/page12/study-springboot/tree/springboot-3
https://github.com/page12/study-springboot/tree/springboot-3

posted @ 2018-01-01 13:33  十二页  阅读(58170)  评论(2编辑  收藏  举报