序列化与反序列化简介
序列化和反序列化是计算机领域中常用的概念,用于将对象或数据结构转换为字节序列(序列化)和将字节序列转换回对象或数据结构(反序列化)。
序列化是指将对象或数据结构转换为字节序列的过程。通过序列化,可以将对象保存到文件、数据库或进行网络传输。在序列化过程中,对象的状态和数据会被转换为一系列的字节,以便能够在不同的环境中进行传输或持久存储。
反序列化则是将字节序列转换回对象或数据结构的过程。通过反序列化,可以重新构造出原始的对象或数据结构,使其可以被程序使用。
序列化和反序列化可以实现跨平台的数据交换。例如,在网络通信中,可以将对象序列化后发送给其他计算机,接收端再进行反序列化,从而实现数据的传输和共享。
常见的序列化格式包括 JSON、XML、Protocol Buffers(protobuf)等。不同的序列化格式有各自的特点和适用场景,选择适合的序列化格式可以提高数据传输效率和方便性。
需要注意的是,在进行序列化和反序列化时,应该确保数据的完整性和安全性。对于敏感数据,可以采用加密等方式进行保护,以防止数据泄露或篡改。此外,在不同语言或不同版本间进行序列化和反序列化时,还需要特别注意对象的兼容性和数据格式的一致性。
序列化目的
序列化的主要目的是将对象或数据结构转换为字节序列,以便在不同环境中进行传输或持久存储。具体来说,序列化的目的有以下几个:
- 数据交换:序列化可以将数据转换为跨平台兼容的格式,使得数据可以在不同的系统、编程语言之间进行交换和共享。
- 持久化存储:将对象序列化后,可以将其保存到磁盘、数据库等介质中,实现数据的持久化存储,防止程序退出或计算机宕机导致数据丢失。
- 网络通信:在网络通信中,可以将对象序列化后通过网络发送到其他计算机,接收端再进行反序列化,从而实现数据的传输和共享。
- 缓存优化:序列化后的数据可以被缓存,当需要时直接从缓存中读取,避免了频繁的数据库查询,提高了性能。
总的来说,序列化的目的是为了方便数据的传输、保存和共享,同时还可以提高程序的性能和响应速度。
序列化框架选择
在选择序列化和反序列化框架的时候,主要从以下两个方面进行考虑:
(1)结果数据大小; 原则上来说,序列化后的数据越小,传输效率越高;
(2)结构复杂程度;结构复杂度会影响序列化和发序列化的效率,结构越复杂,越耗时。
根据以上两点,对于性能要求不是太高的服务器程序,可以选择Json文本格式的序列化框架;对于性能要求比较高的程序程序,则应该选择传输效率更高的二进制序列化框架,建议使用Protobuf。
Json
Json(JavaScript Object Notation JS对象)是一种轻量级的数据交换格式。它是基于ECMAScript 的一个子集。采用完全独立于编程语言的文本格式来存储和表示数据。
Json 协议是一种文本协议,易于阅读和编写,同时也易于机器解析和生成,并能有效地提升网络传输协效率。
JSON格式具有以下特点:
- 可读性高:JSON使用简洁明了的文本格式,易于人类阅读和理解。
- 轻量级:相较于其他数据交换格式如XML,JSON的数据表示更为紧凑,占用更少的存储空间和传输带宽。
- 平台无关性:JSON格式在不同的编程语言和平台之间具有良好的兼容性,可以方便地进行数据交换和共享。
- 支持多种数据类型:JSON支持包括字符串、数字、布尔值、数组、对象和null在内的多种数据类型。这使得JSON能够灵活地表示各种数据结构和复杂对象。
- 易于解析和生成:绝大多数编程语言都提供了JSON的解析和生成库,使得操作JSON数据变得十分方便和高效。
- 可扩展性:JSON格式支持通过嵌套和组合来表示更复杂的数据结构,可以根据具体需求进行扩展和定制。
在JSON中,数据以键值对的方式表示,键是一个字符串,值可以是字符串、数字、布尔值、数组、对象或null。简单的JSON示例如下:
{"name": "张娜","age": 30,"isStudent": false,"hobbies": ["阅读", "游泳"],"address": {"street": "安德里北街12号","city": "北京市"},"score": null
}
Json序列化和反序列化开源库
java处理Json数据有三个比较流行的开源类库:
(1)阿里的FastJson
阿里巴巴的FastJson 是一个高性能的 JSON 库。 FastJson 库采用独创的快速算法,将 JSON 转成 POJO 的速度提升到极致,从性能上说,其反序列化速度超过其他 JSON 开 源库。
FastJson 在复杂类型的 POJO 转换 JSON (序列化)时,可能会 出现一些引用类 型问题而导致 JSON 转换出错,需要进行引用的定制。
(2)谷歌的Gson
Google的 Gson 开源库是一个功能齐全的 JSON 解析库,起源于 Google 公司内部需求而由 Google 自行研发而来,在 2008 年 5 月公开发布第一版之后已被许多公司或用户应用。 Gson 可 以完成复杂类型的 POJO 和 JSON 字符串的相互转换,转换的能力非常强。
(3)开源社区的Jackson
Jackson 是一个简单的、基于java的Json开源库。使用Jackson开源库,可以轻松地将java Pojo对象转换成Json、XML;式串;同样也可以方便地将 JSON 、 XML 字符串转换成 Java POJO 对象。
Jackson 开源库的优点是:所依赖的 Jar 包较少、简单易用、性能也还不错, 另外 Jackson 社区相对比较活跃。
Jackson 开源库的缺点是:对于复杂 POJO 类型、复杂的集合 Map 、 List 的转换结果,不是标准的 JSON 格式,或者会出现一些问题。
在实际生产中,比较主流的策略是Gson+FastJson相互结合的JsonUtil类,在Pojo序列化为JSON字符串的应用场景(序列化场景)使用Gson库;在JSON 字符串反序列化成POJO的应用场景(反序列化)使用FastJson库。JsonUtil类如下:
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import com.crazymaker.springcloud.common.result.RestOut;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;import java.io.UnsupportedEncodingException;
import java.lang.reflect.Type;
import java.util.Map;public class JsonUtil {//谷歌 GsonBuilder 构造器static GsonBuilder gb = new GsonBuilder();private static final Gson gson;static {//不需要html escapegb.disableHtmlEscaping();gson = gb.create();}//序列化:使用谷歌 Gson 将 POJO 转成字符串public static String pojoToJson(Object obj) {String json = gson.toJson(obj);return json;}//反序列化:使用阿里fastJson 将字符串转成 POJO对象public static <T> T jsonToPojo(String json, Class<T> tClass) {T t = JSON.parseObject(json, tClass);return t;}}
在2022年5月的时候,中共电信天翼云发布了FastJson ≤ 1.2.80 具有严重、高危的反序列化远程代码执行漏洞,在这种情况下,就需要通过快速切换json组件来解决子此问题,最佳策略是策略模式+工具类的结合,实现业务应用兼容主要的json组件,根据具体的场景和各种突发事件能够进行快速、灵活的组件切换。
策略模式(Strategy)
策略模式 属于对象的行为模式,主要是针对一组不同的算法,抽象出一组共同的接口或抽象类,然后根据每一个单独的算法封装具体的实现类,从而使得它们可以相互替换。
策略模式的优势是可以在不影响到客户端的情况下,实现具体算法的切换。
策略模式的类图如下:
在这里插入图片描述
使用策略模式(Strategy)实现不同的Json开源组件之间的切换,其具体类图如下:
抽象策略接口 JsonStrategy,定义了一组抽象的方法,如toJson()-序列化、fromJson()-反序列化。
public interface JsonStrategy {...String toJson(Object object);String toJson(Object object, String dateFormatPattern);<T> T fromJson(String json, Class<T> valueType);...
}
具体策略类FastJsonStrategy、GsonJSONStrategy、JackSonStrategy,分别使用FastJson、Gson、JackSon三个主流的开源组件,完成Pojo对象的序列化和反序列化。 这样设计遵循了软件设计的开闭原则,在后续若想新增一个新的序列化/反序列化组件,只需要新增一个JsonStrategy实现类即可。
FastJsonStrategy 类:
public class FastJsonStrategy implements JsonStrategy {public FastJsonStrategy() {}...//序列化@Overridepublic String toJson(Object object) {return JSON.toJSONString(object);}@Overridepublic String toJson(Object object, String dateFormatPattern) {return JSON.toJSONStringWithDateFormat(object, dateFormatPattern, SerializerFeature.WriteDateUseDateFormat);}//反序列化@Overridepublic <T> T fromJson(String json, Class<T> valueType) {return JSON.parseObject(json, valueType);}...
}
GsonStrategy 类:
public class GsonStrategy implements JsonStrategy {public static Gson gson;public static GsonBuilder gsonBuilder;public GsonStrategy() {gsonBuilder = new GsonBuilder();//不需要html escapegsonBuilder.disableHtmlEscaping();// 解决Gson序列化时出现整型变为浮点型的问题gsonBuilder.registerTypeAdapter(new TypeToken<Map<Object, Object>>() { }.getType(),(JsonDeserializer<Map<Object, Object>>) (jsonElement, type, jsonDeserializationContext) -> {Map<Object, Object> map = new LinkedHashMap<>();JsonObject jsonObject = jsonElement.getAsJsonObject();Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();for (Map.Entry<String, JsonElement> entry : entrySet) {Object obj = entry.getValue();if (obj instanceof JsonPrimitive) {map.put(entry.getKey(), ((JsonPrimitive) obj).getAsString());} else {map.put(entry.getKey(), obj);}}return map;});gsonBuilder.registerTypeAdapter(new TypeToken<List<Object>>() { }.getType(),(JsonDeserializer<List<Object>>) (jsonElement, type, jsonDeserializationContext) -> {List<Object> list = new LinkedList<>();JsonArray jsonArray = jsonElement.getAsJsonArray();for (int i = 0; i < jsonArray.size(); i++) {if (jsonArray.get(i).isJsonObject()) {JsonObject jsonObject = jsonArray.get(i).getAsJsonObject();Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();list.addAll(entrySet);} else if (jsonArray.get(i).isJsonPrimitive()) {list.add(jsonArray.get(i));}}return list;});gson = gsonBuilder.create();}...@Overridepublic String toJson(Object object) {return gson.toJson(object);}@Overridepublic String toJson(Object object, String dateFormatPattern) {gson = gsonBuilder.setDateFormat(dateFormatPattern).create();return gson.toJson(object);}@Overridepublic <T> T fromJson(String json, Class<T> valueType) {return gson.fromJson(json, valueType);}
...
}
JacksonJsonStrategy类:
public class JacksonJsonStrategy implements JsonStrategy {public static ObjectMapper objectMapper;public JacksonJsonStrategy() {// 禁止时间格式序列化为时间戳if (objectMapper == null) {objectMapper = new ObjectMapper().findAndRegisterModules().disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);}}...@Overridepublic String toJson(Object object) {try {return objectMapper.writeValueAsString(object);} catch (JsonProcessingException e) {throw new RuntimeException(e);}}@Overridepublic String toJson(Object object, String dateFormatPattern) {SimpleDateFormat dateFormat = new SimpleDateFormat(dateFormatPattern);try {return objectMapper.writer(dateFormat).writeValueAsString(object);} catch (JsonProcessingException e) {throw new RuntimeException(e);}}@Overridepublic <T> T fromJson(String json, Class<T> valueType) {try {return objectMapper.readValue(json, valueType);} catch (IOException e) {throw new RuntimeException(e);}}...
}
环境类JsonContext,此类是模块内部和模块外部之间的纽带,对于模块内部来说,JsonContext类根据配置文件文件中的类型配置,初始化具体的JsonStrategy实现类,并且将其引用保存在内部成员变量中;对于模块外部来说,JsonContext 类为他们提供JsonStrategy引用,供外部Client客户程序使用。
在配置文件system.properties 中配置所使用的json组件,具体配置如下:
json.strategy=Jackson
读取配置文件
@ConfigFileAnno(file = "/system.properties")
public class SystemConfig extends ConfigProperties
{//依照属性,从配置文件中,装载配置项static ConfigProperties singleton= new SystemConfig("/system.properties");private SystemConfig(String fileName){super(fileName);super.loadFromFile();}/*** json的类型: gson/fastjson/Jackson** json.strategy=fastjson*/public static final String JSON_STRATEGY= singleton.getValue("json.strategy");
}
JsonContext类:
@Slf4j
public abstract class JsonContext {private static volatile JsonStrategy strategy;private JsonContext() {}private static final String CLASS_TYPE_JACKSON = "com.fasterxml.jackson.databind.ObjectMapper";private static final String CLASS_TYPE_FASTJSON = "com.alibaba.fastjson.JSON";private static final String CLASS_TYPE_GSON = "com.google.gson.Gson";/*** json的类型: gson/fastjson/Jackson*/private static final String JACKSON = "Jackson";private static final String FASTJSON = "fastjson";private static final String GSON = "gson";private static JsonStrategy loadFromConfig() {String jsonType = SystemConfig.JSON_STRATEGY;switch (jsonType) {case JACKSON:if (isClassPresent(CLASS_TYPE_JACKSON)) {log.info("used jackson");return new JacksonJsonStrategy();} else {log.error("jackson not found");throw new RuntimeException("未找到jackson的依赖");}case FASTJSON:if (isClassPresent(CLASS_TYPE_FASTJSON)) {log.info("used fastjson");return new FastJsonStrategy();} else {log.error("fastjson not found");throw new RuntimeException("未找到fastjson的依赖");}case GSON:if (isClassPresent(CLASS_TYPE_GSON)) {log.info("used gson");return new GsonStrategy();} else {log.error("gson not found");throw new RuntimeException("未找到gson的依赖");}default:log.error("未找到jackson、gson或fastjson的依赖");throw new RuntimeException("未找到jackson、gson或fastjson的依赖");}}public static JsonStrategy getStrategy() {if (strategy == null) {synchronized (JsonContext.class) {if (strategy == null) {strategy = loadFromConfig();}}}return strategy;}private void setStrategy(JsonStrategy strategy) {this.strategy = strategy;}}
外部Client程序只会用到JsonContext类和JsonStrategy引用,不用用到具体的JsonStrategy实现类,实现了client和json开源组件的解耦,达到高内聚低耦合的效果。JsonUtil类需要进行完善一下,具体代码如下:
public class JsonUtil {//序列化: pojo =》 json 字符串//使用策略模式 将 POJO 转成字符串public static String pojoToJson(Object obj) {JsonStrategy strategy = JsonContext.getStrategy();String json = strategy.toJson(obj);return json;}//反序列化:json 字符串 =》 pojo//使用策略模式 将 字符串 转成POJOpublic static <T> T jsonToPojo(String json, Class<T> tClass) {JsonStrategy strategy = JsonContext.getStrategy();T t = strategy.fromJson(json, tClass);return t;}public static <K, V> Map<K, V> jsonToMap(String json, Type type) {JsonStrategy strategy = JsonContext.getStrategy();Map<K, V> t = strategy.toMap(json,type);return t;}}
Protobuf
ProtobufProtocol Buffers 的简称,它是一种由Google开发的跨平台、语言无关、可扩展的序列化二进制数据格式。
与 JSON 类似,Protocol Buffers 也用于在不同系统、不同语言之间进行数据交换和存储。它通过定义消息结构(.proto 文件)来描述数据的类型和格式,并使用专门的编译器(例如protoc)来生成针对特定编程语言的序列化和反序列化代码。
与JSON 、 XML 相比, Protobuf 算是后起之秀,只是 Protobuf 更加适合于高性能、快速响 应的数据传输应用场景。 Protobuf 数据包是一种二进制的格式,相对于文本格式的数据交换 JSON 、 XML )来说,速度要快很多 。由于 Protobuf 优异的性能,使得它更加适用于分布式应用场景下的数据通信或者异构环境下的数据交换。
Protocol Buffers 具有以下特点:
- 高效性:Protocol Buffers 采用二进制编码,相比于文本格式如 JSON,它能够更高效地进行数据压缩和传输,占用更少的存储空间和带宽。
- 可扩展性:Protocol Buffers 提供了向后兼容和字段标签等特性,可以方便地进行数据模型的演进和版本管理。
- 跨平台、跨语言:Protocol Buffers 的定义文件可以在不同的编程语言中使用,生成的序列化和反序列化代码也支持跨平台操作。
- 性能优化:Protocol Buffers 生成的代码通常比手动编写的序列化和反序列化代码更高效,能够提供更快的数据操作速度。
- 代码生成:通过使用特定的编译器(如protoc),根据 .proto 文件可以生成用于不同编程语言的数据结构和序列化代码。
在使用 Protocol Buffers 时,需要定义消息结构的 .proto 文件,并使用对应的编译器生成目标语言的代码。然后,就可以使用生成的代码进行消息的序列化和反序列化操作,实现跨平台、跨语言的数据交换。
proto文件生成
Protobuf使用 proto 文件来预先定义的消息格式。数据包是按照 proto 文件所定义的消息格式完成二进制码流的编码和解码。
proto 文件就是一个消息的协议文件,这个协议文件的后缀文件名为“.proto ”。其目录结构如下:
Msg.proto文件具体内容如下:
// [开始声明]
syntax = "proto3";//定义protobuf的包名称空间
package com.th.protocol;
// [结束声明]// [开始 java 选项配置]
//作用:在生成 proto ”文件中消息的POJO 类和 Builder (构造者)的 Java 代码时,将生成的 Java 代码放入该选项所指定的 package类路径中。
option java_package = "com.th.protocol";
//作用:在生成 proto件所对应 Java代码时,生产的 Java 外部类使用配置的名称。
option java_outer_classname = "MsgProtos";
// [结束 java 选项配置]// [开始 消息定义]
//message 关键字来定义消息的结构体。
//每一个消息结构体可以有多个字段。定义一个字段的格式为“类型名称= 编号”。
message Msg {uint32 id = 1; // Unique ID number for this person.string content = 2;
}
message Msg2 {uint32 id = 1; // Unique ID number for this person.string content = 2;
}
message Msg3 {uint32 id = 1; // Unique ID number for this person.string content = 2;
}
// [结束 消息定义]
在每一个“.proto ”文件中,可以声明多个 message 。大部分情况下会把存在依赖关 系或者包含关系的 message 消息结构体写入一个 .proto 文件。将那些没有关系、相互独立的 message 消息结构体,分别写入不同的文件,这样便于管理。
完成 “.proto ”文件定义后,下一步就是生成消息的 POJO 类和 Builder (构造者)类。有两种方式生成 Java 类:
(1)通过控制台命令的方式;
(2)使用 Maven 插件的方式(推荐使用)。
使用maven插件
首先从“https://github.com/protocolbuffers/protobuf/releases ”下载 Protobuf 的安装包,可以选择不同的版本,这里下载的是 3.6.1 的 Java 版本。
使用protobuf maven plugin 插件,可以非常方便地生成消息的 POJO 类和 Builder 类的 Java 代码。在 Maven 的 pom 文件中增加此 plugin 插件的配置项,具体如下:
<plugin><groupId>org.xolstice.maven.plugins</groupId><artifactId>protobuf-maven-plugin</artifactId><version>0.6.5</version><extensions>true</extensions><configuration><!--proto文件路径--><protoSourceRoot>${project.basedir}/protobuf</protoSourceRoot><!--目标路径--><outputDirectory>${project.build.sourceDirectory}</outputDirectory><!--设置是否在生成java文件之前清空outputDirectory的文件--><clearOutputDirectory>false</clearOutputDirectory><!--临时目录--><temporaryProtoFileDirectory>${project.build.directory}/protoc-temp</temporaryProtoFileDirectory><!--protoc 可执行文件路径--><protocExecutable>${project.basedir}/protobuf/protoc3.6.1.exe</protocExecutable></configuration><executions><execution><goals><goal>compile</goal><goal>test-compile</goal></goals></execution></executions></plugin>
protobuf-maven-plugin 插件的配置项,具体介绍如下:
- protoSourceRoot proto” 消息结构体所在文件的路径;
- outputDirectory :生成的 POJO 类和 Builder 类的目标路径;
- protocExecutable :protobuf 的 Java 代码生成工具的 protoc3.6.1.exe 可执行文件的 路径。
配置好之后,执行插件的compile 命令, Java 代码就利索生成了。
演示示例
添加maven依赖:
<dependency><groupId>com.google.protobuf</groupId><artifactId>protobuf-java</artifactId><version>3.6.1</version>
</dependency>
使用Builder 构造POJO消息对象
public static MsgProtos.Msg buildMsg(){MsgProtos.Msg.Builder personBuilder = MsgProtos.Msg.newBuilder();personBuilder.setId(1000);personBuilder.setContent("protoBuf适用于分布式系统");MsgProtos.Msg message = personBuilder.build();return message;}
获得消息POJO 的实例之后,可以通过多种方法将 POJO 对象序列化成二进制字节,或者反序列化。
方式一:调用 Protobuf POJO 对象的 toByteArray() 方法将 POJO 对象序列化成字节数组,具体的代码如下:
//第1种方式:序列化 serialization & 反序列化 Deserialization@Testpublic void serAndDesr1() throws IOException{MsgProtos.Msg message = buildMsg(1,"protoBuf适用于高并发、高性能的分布式系统");//将Protobuf对象,序列化成二进制字节数组byte[] data = message.toByteArray();//可以用于网络传输,保存到内存或外存ByteArrayOutputStream outputStream = new ByteArrayOutputStream();outputStream.write(data);data = outputStream.toByteArray();//二进制字节数组,反序列化成Protobuf 对象MsgProtos.Msg inMsg = MsgProtos.Msg.parseFrom(data);Logger.info("devId:=" + inMsg.getId());Logger.info("content:=" + inMsg.getContent());}
这种方式类似于普通Java 对象的序列化,适用于很多将 Protobuf 的 POJO 序列化到内存或 者外存(如物理硬盘)的应用场景。
方式二:通过调用Protobuf 生成的 POJO 对象的 writeTo (OutputStream )方法将 POJO 对象 的二进制字节写出到输出流。通过调用 Protobuf 生成的 POJO 对象的 parseFrom( InputStream) 方法, Protobuf 从输入流中读取二进制码然后反序列化,得到 POJO 新的实例。具体的代码如下:
//第2种方式:序列化 serialization & 反序列化 Deserialization@Testpublic void serAndDesr2() throws IOException{MsgProtos.Msg message = buildMsg();//序列化到二进制流ByteArrayOutputStream outputStream = new ByteArrayOutputStream();message.writeTo(outputStream);ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());//从二进流,反序列化成Protobuf 对象MsgProtos.Msg inMsg = MsgProtos.Msg.parseFrom(inputStream);Logger.info("devId:=" + inMsg.getId());Logger.info("content:=" + inMsg.getContent());}
在阻塞式的二进制码流传输应用场景中,这种序列化和反序列化的方式是没有问题的。 例如,可以将二进制码流写入阻塞式的 Java OIO 套接字或者输出到文件。但是,这种方式在 异步操作的 NIO 应用场景中,存在粘包半包的问题。
方式三:通过调用Protobuf 生成的 POJO 对象的 writeDelimitedTo( OutputStream )方法在序列化的字节码之前添加了字节数组的长度。
//第3种方式:序列化 serialization & 反序列化 Deserialization//带字节长度:[字节长度][字节数据],解决粘包问题@Testpublic void serAndDesr3() throws IOException{MsgProtos.Msg message = buildMsg();//序列化到二进制流ByteArrayOutputStream outputStream = new ByteArrayOutputStream();message.writeDelimitedTo(outputStream);ByteArrayInputStream inputStream = new ByteArrayInputStream(outputStream.toByteArray());//从二进流,反序列化成Protobuf 对象MsgProtos.Msg inMsg = MsgProtos.Msg.parseDelimitedFrom(inputStream);Logger.info("devId:=" + inMsg.getId());Logger.info("content:=" + inMsg.getContent());}
这种方式可以用于异步操作的NIO 应用场景中,解决了粘包半包的问题。