资讯专栏INFORMATION COLUMN

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

stackfing / 1807人阅读

摘要:序列化反序列化主要体现在程序这个过程中,包括网络和磁盘。如果是开发应用,一般这两个注解对应的就是序列化和反序列化的操作。协议的处理过程,字节流内部对象,就涉及这两种序列化。进行第二步操作,也就是序列化和反序列化的核心是。

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

对象的序列化/反序列化大家应该都比较熟悉:序列化就是将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的一种实现。

在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 {
    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 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> 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> 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/stud...
https://github.com/page12/stu...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68126.html

相关文章

  • SpringMVC HttpMessageConverter 匹配规则

    摘要:例如,服务端不支持这种,应该返回。而当使用或者其他等价方式进行配置时,会把添加在最前面,优先级最高。好了,到此就基本上说完了整个的匹配规则或者叫选择过程。自己能力不是最大,却大包大揽承担最大责任,处理不了还返回,是甩锅客户端的行为。 以下内容,如有问题,烦请指出,谢谢! SpringMVC启动时会自动配置一些HttpMessageConverter,接收到http请求时,从这些Con...

    Imfan 评论0 收藏0
  • Spring Boot 参考指南(开发Web应用程序)

    摘要:开发应用程序非常适合应用程序开发,通过使用嵌入式或,你可以创建一个自包含的服务器。如果你还没有开发过一个应用程序,你可以按照入门部分中的示例进行操作。自动配置为大多数应用程序提供了良好的自动配置。 27. 开发Web应用程序 Spring Boot非常适合web应用程序开发,通过使用嵌入式Tomcat、Jetty、Undertow或Netty,你可以创建一个自包含的HTTP服务器。大多...

    roadtogeek 评论0 收藏0
  • 《 Kotlin + Spring Boot : 下一代 Java 服务端开发 》

    摘要:下一代服务端开发下一代服务端开发第部门快速开始第章快速开始环境准备,,快速上手实现一个第章企业级服务开发从到语言的缺点发展历程的缺点为什么是产生的背景解决了哪些问题为什么是的发展历程容器的配置地狱是什么从到下一代企业级服务开发在移动开发领域 《 Kotlin + Spring Boot : 下一代 Java 服务端开发 》 Kotlin + Spring Boot : 下一代 Java...

    springDevBird 评论0 收藏0
  • PHP反列化漏洞系列之–PHP列化和反列化原理

    0.前言 本文为笃行日常学习记录,web安全php漏洞系列。 对象的序列化和反序列化作用就不再赘述,php中序列化的结果是一个php自定义的字符串格式,有点类似json. 我们在任何语言中设计对象的序列化和反序列化都需要解决几个问题 把某个对象序列化之后,序列化的结果有自描述的功能(从序列化的结果中知道这个对象的具体类型,知道类型还不够,当然还需要知道这个类型所对应具体的值). 序列化时的权...

    caozhijian 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<