从内存到网络:深入理解对象序列化
1、概述
前面一篇文章我们实现了一个自定义协议Cheese协议,文章的末尾提到了序列化和反序列化的问题,今天我们就深入的分析一下什么是序列化、为什么要序列化以及序列化的最佳技术选型。最后我们将理论落地成一款实用的序列化框架,可以通过配置选择不同的序列化方式并且集成到Cheese 中。
2、什么是序列化
首先我们的编写的代码最终都会变成计算机能够执行的机器码指令,编译型的语言一步到位生成机器码,解释型的语言通过解释器或者虚拟机来翻译成机器码。那么我们的程序处理的数据是怎么从内存里进入到网络呢
毫无疑问,数据进入网络或者磁盘肯定是通过IO操作实现的,比如Java中的InputStream和 OutputStream以及C++中的ifstream和ofstream函数 等等 。
数据在内存中的组织结构千奇百怪,有各种简单类型的数据,也有复杂类型的对象以及各种奇奇怪怪的数据结构。因此我们需要一种通用的算法,将这些千奇百怪的数据按照统一的格式进行编排,这一类的算法就叫序列化算法,这个步骤叫做序列化。
3、常见的序列化解决方案
3.1、序列化需要干什么
首先我们需要明确序列化需要干什么,换而言之就是序列化需要提供什么功能,放到代码层面上讲的话 我们的序列化方法 入参是什么 出参是什么,逻辑怎么实现。我们通过上一章节的介绍至少可以粗糙的知道序列化是“将对象转换成字节数组”,反序列化是将“字节数组转换成数据对象”,具体实现有多种办法,这里我们先定义一个接口,后面一次为大家介绍常见的实现方案
/**
* @Description
* @Author wcan
* @Date 2025/1/21 上午 10:18
* @Version 1.0
*/
public interface SerializeStrategy<T> {
public byte[] serialize(T object);
public T deserialize(byte[] bytes);
}
3.2、JDK原生序列化
在Java中,JDK自带了一个序列化的接口 Serializable。API如下:
java.io.Serializable
我们将需要序列化的类实现这个接口,然后就可以使用 ObjectOutputStream 序列化
public interface Externalizable extends java.io.Serializable {
/**
* The object implements the writeExternal method to save its contents
* by calling the methods of DataOutput for its primitive values or
* calling the writeObject method of ObjectOutput for objects, strings,
* and arrays.
*
* @serialData Overriding methods should use this tag to describe
* the data layout of this Externalizable object.
* List the sequence of element types and, if possible,
* relate the element to a public/protected field and/or
* method of this Externalizable class.
*
* @param out the stream to write the object to
* @throws IOException Includes any I/O exceptions that may occur
*/
void writeExternal(ObjectOutput out) throws IOException;
/**
* The object implements the readExternal method to restore its
* contents by calling the methods of DataInput for primitive
* types and readObject for objects, strings and arrays. The
* readExternal method must read the values in the same sequence
* and with the same types as were written by writeExternal.
*
* @param in the stream to read data from in order to restore the object
* @throws IOException if I/O errors occur
* @throws ClassNotFoundException If the class for an object being
* restored cannot be found.
*/
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
同样的这个接口还有一个子接口 Externalizable,我么也可以实现 Externalizable 这个接口,和 Serializable 接口不同的是我们需要实现 writeExternal 和 readExternal 这两个抽象方法,也就是序列化和反序列化的实现。例如下面这段代码
public class Jerry implements Externalizable {
private String name;
private int age;
// getter and setter ...
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}
@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}
}
Externalizable 接口国内使用的并不多,下面我们使用原生的API来编写一个完整的案例,相关代码比较简单,如下所示
/**
* @Description Java 原生的序列化方式
* @Author wcan
* @Date 2025/1/21 上午 10:15
* @Version 1.0
*/
public class SimpleSerialize<T> implements SerializeStrategy<T> {
public SimpleSerialize() {
}
public SimpleSerialize(Class<T> objectClass) {
}
@Override
public byte[] serialize(T object) {
if (object == null) {
throw new IllegalArgumentException("Object 不能为空 ");
}
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream)) {
objectOutputStream.writeObject(object);
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
// 使用日志记录异常信息
throw new RuntimeException("serialize failed: ", e);
}
}
@Override
public T deserialize(byte[] bytes) {
if (bytes == null || bytes.length == 0) {
throw new IllegalArgumentException("bytes 不能为空 ");
}
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream)) {
@SuppressWarnings("unchecked")
T obj = (T) objectInputStream.readObject();
return obj;
} catch (IOException | ClassNotFoundException e) {
// 使用日志记录异常信息
throw new RuntimeException("deserialize failed: ", e);
}
}
}
这种方案就是 使用 ObjectOutputStream将数据对象转换成字节数组,使用 ObjectInputStream 将字节数据转成换数据对象。
3.3、JSON 序列化
这种序列化方式也很常见,我们在之前的Cheese中 使用的就是Json序列化的方式,也就是将对象转换成Json字符串,然后再转成字节数组。之前的cheese中使用的时SpringBoot自带的jackson,这里给大家再介绍一种更好用的Json库,谷歌开源的 gson
首先我们先导入坐标,
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.8</version>
</dependency>
接着就可以编写代码了,相关代码如下
package org.wcan.serialize.strategy;
import com.google.gson.Gson;
import org.wcan.serialize.SerializeStrategy;
import java.nio.charset.Charset;
/**
* @Description
* @Author wcan
* @Date 2025/1/21 上午 10:30
* @Version 1.0
*/
public class JsonSerialize<T> implements SerializeStrategy<T> {
private Gson gson = new Gson();
private Class<T> type;
public JsonSerialize(Class<T> type) {
this.type = type;
}
@Override
public byte[] serialize(T object) {
String json = gson.toJson(object);
return json.getBytes(Charset.defaultCharset());
}
@Override
public T deserialize(byte[] bytes){
String string = new String(bytes, Charset.defaultCharset());
return gson.fromJson(string, type);
}
}
3.4、Hessian 序列化
JSON序列化的方式很直观,但是效率并不高,尤其是再处理大对象的时候。相比较而言 Hessian序列化效率更高,他提供了一种二进制格式,并且优化了对象的序列化过程,某些场景下比Java原生的序列化方式更高效。下面是代码示例
引入依赖
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>
package org.wcan.serialize.strategy;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;
import org.wcan.serialize.SerializeStrategy;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* @Description
* @Author wcan
* @Date 2025/1/21 上午 11:13
* @Version 1.0
*/
public class HessianSerialize<T> implements SerializeStrategy<T> {
public HessianSerialize() {
}
public HessianSerialize(Class<T> objectClass) {
}
@Override
public byte[] serialize(T object) {
try(ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
HessianOutput hessianOutput = new HessianOutput(outputStream);
hessianOutput.writeObject(object);
return outputStream.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Override
public T deserialize(byte[] bytes) {
try(ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes)) {
HessianInput hessianInput = new HessianInput(inputStream);
return (T)hessianInput.readObject();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
3.5、Kryo序列化
Kryo也是一种高效的序列化方式,数据也是以二进制格式编码的。得益于其高效的实现方式,序列化后的数据相比Hessian更小,所以在网络中传输的时候更有优势,下面再给大家提供一段简单的代码案例
依赖
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.6.2</version>
</dependency>
示例代码
package org.wcan.serialize.strategy;
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.wcan.serialize.SerializeStrategy;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
/**
* @ClassName KryoSerialize
* @Description TODO
* @Author wcan
* @Date 2025/2/8 下午 15:03
* @Version 1.0
*/
public class KryoSerialize<T> implements SerializeStrategy<T> {
private final Kryo kryo = new Kryo();
private final Class<T> clazz;
public KryoSerialize(Class<T> clazz) {
this.clazz = clazz;
kryo.register(clazz);
kryo.register(Class[].class);
kryo.register(Class.class);
kryo.register(Object[].class);
// kryo.setReferences(true);
}
@Override
public byte[] serialize(T object) {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Output output = new Output(byteArrayOutputStream)) {
kryo.writeObject(output, object);
output.flush();
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
throw new RuntimeException("Serialization failed", e);
}
}
@Override
public T deserialize(byte[] bytes) {
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Input input = new Input(byteArrayInputStream)) {
return kryo.readObject(input, clazz);
} catch (IOException e) {
throw new RuntimeException("Deserialization failed", e);
}
}
}
3.6、Protobuf 序列化
在Java中 Kryo的性能已经很优秀了,当然也有更优秀的存在 那就是 Protobuf了。
Protobuf 是由 Google 开发的一个高效的、平台中立的序列化协议。它允许你定义数据结构,并提供了一种机制,通过将这些结构序列化为紧凑的二进制格式,从而实现高效的数据存储和传输。我们再使用的时候 需要定义proto文件,在该文件中定义好对象的格式。然后使用 protoc 编译器 将这个文件编译成pojo类。 例如:
新建一个文件 Person.proto
// Person.proto
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
string email = 3;
}
使用下面的命令编译这个文件
protoc --java_out=./tmp Person.proto
这个命令执行完成后会在当前目录的/tmp 下生成一个Persion.java 的文件,就可以放到项目里面去使用了 ,例如将对象写入文件中
import com.example.protobuf.PersonOuterClass.Person;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ProtobufExample {
public static void main(String[] args) {
// 创建一个 Person 对象并设置字段
Person person = Person.newBuilder()
.setName("Tom")
.setAge(10)
.setEmail("tom@qq.com")
.build();
// 序列化:将 person 对象写入文件
try (FileOutputStream output = new FileOutputStream("person.txt")) {
person.writeTo(output);
System.out.println("Person object serialized to person.txt");
} catch (IOException e) {
e.printStackTrace();
}
// 反序列化:从文件中读取 person 对象
try (FileInputStream input = new FileInputStream("person.txt")) {
Person deserializedPerson = Person.parseFrom(input);
System.out.println("Deserialized Person: ");
System.out.println("Name: " + deserializedPerson.getName());
System.out.println("Age: " + deserializedPerson.getAge());
System.out.println("Email: " + deserializedPerson.getEmail());
} catch (IOException e) {
e.printStackTrace();
}
}
}
protobuf 这种序列化方式很高效,但是也存在局限性,因为pojo类 都是固定的,所以不能灵活的集成到项目中使用,只有在特定的场景下使用
3.7、Protostuff序列化
前面我们知道了 Protobuf 性能虽然优秀,但是不够灵活,所以在他的基础上衍生出了 Protostuff。protostuff 拥有了等同于 protobuf 的性能,并且 不需要使用 protoc编译器,缺点就是仅仅适用于Java。下面我们给出相关的代码案例
依赖
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.8.0</version>
</dependency>
代码
package org.wcan.serialize.strategy;
import io.protostuff.LinkedBuffer;
import io.protostuff.ProtostuffIOUtil;
import io.protostuff.Schema;
import io.protostuff.runtime.RuntimeSchema;
import org.wcan.serialize.SerializeStrategy;
/**
* @Description
* @Author wcan
* @Date 2025/1/21 上午 11:13
* @Version 1.0
*/
public class ProtostuffSerialize<T> implements SerializeStrategy<T> {
private Class<T> inputClass;
private Schema<T> schema ;
public ProtostuffSerialize(Class<T> inputClass) {
this.inputClass = inputClass;
this.schema = RuntimeSchema.getSchema(inputClass);
}
@Override
public byte[] serialize(T object) {
LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
byte[] byteArray = ProtostuffIOUtil.toByteArray(object, schema, buffer);
return byteArray;
}
@Override
public T deserialize(byte[] bytes) {
T object = schema.newMessage();
ProtostuffIOUtil.mergeFrom(bytes, object, schema);
return object;
}
}
4、技术选型最佳实践
本章节我们讨论一下常见的序列化解决方案的技术选型,首先我们排除掉JDK原生的方式,你想想 如果原生的方式足够的好,那么还会出来这么多第三方的解决方案吗。
这里先来给出一张表格。从性能的方面罗列一下各种序列化方案的优缺点
序列化方式 | 序列化速度 | 反序列化速度 | 序列化数据大小 | 优缺点总结 |
---|---|---|---|---|
JDK序列化 | 中等 | 中等 | 较大 | 易用但性能差,不适合大规模系统 |
JSON | 较慢 | 较慢 | 较大 | 跨平台支持好,但性能差 |
Hessian | 较快 | 较快 | 较小 | 跨语言支持,但性能不如Kryo和Protobuf |
Kryo | 非常快 | 非常快 | 较小 | 高效,但跨语言支持差 |
Protobuf | 非常快 | 非常快 | 很小 | 性能最好,跨语言支持优秀 |
Protostuff | 非常快 | 非常快 | 更小 | 性能最优,但社区较小 |
假如 你对性能有很高的要求 那么肯定建议使用 protostuff 或者 Protobuf 。如果你的业务系统都是基于Java技术栈的,那么最佳技术选型就是 protostuff 了。如果你的业务系统技术栈比较庞杂那就可以选择Kroy或者 Protobuf。当然如果你的业务系统涉及到多种类型的平台那就建议选择JSON了。
关于其他方面,比如 WebApi 用于前后端数据交换,尤其是 RESTful API 最佳的技术选型肯定就是JSON了。
在大数据领域Kroy则使用的比较多,Kroy 适用于需要高效序列化的高性能应用,如大数据计算框架(例如Apache Spark)
至于hessian 在一下RPC框架中使用的比较多 例如Dubbo和SpringCloud
最后总结一下,没有完美的技术,所以也就没有最优的技术选型,引入一种解决方案 同时也会引入该方案存在的问题,我们最佳实践就是根据自己业务系统中不同的场景 权衡利弊,选择合适的方案就行了 利大于弊 就是好的选型。
5、应用集成
前面我们分析了Java中一些常用的序列化方案,各有各的优缺点,我们下面就将上述的代码示例落地成可以与应用集成的工程化项目,让用户可以用过配置指定不同的序列化方案。
5.1、搭建工程
首先新建一个项目,serialize-store 添加依赖如下
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.wcan.serialize</groupId>
<artifactId>serialize-store</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.8</version>
</dependency>
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.63</version>
</dependency>
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.6.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.protostuff/protostuff-core -->
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.8.0</version>
</dependency>
</dependencies>
</project>
将前面的代码加入到项目中,项目结构如下
5.2、设计一个优雅的工厂
我们前面已经有5种策略了,所以我们需要编写一个工厂类来实现具体策略对象的创建。简单、高效、简洁、明了。我认为这就是最好的代码了。
public class SerializeFactory {
public static <T> SerializeStrategy<T> getSerialize(String strategy, Class<T> clazz) {
if ("json".equals(strategy))
return new JsonSerialize<>(clazz);
if ("java".equals(strategy))
return new SimpleSerialize<>();
if ("hessian".equals(strategy))
return new HessianSerialize<>();
if ("kry".equals(strategy))
return new KryoSerialize<>(clazz);
if ("proto".equals(strategy))
return new ProtostuffSerialize<>(clazz);
return null;
}
}
虽然我觉得这样写挺好的。但是 有些 大聪明 总觉的这样写很low, 哪有在工厂里直接new的,而且还写这么多 if 。好吧,那我们再改改。我们使用 函数式接口和方法引用 来实现
相关代码如下
package org.wcan.serialize;
import org.wcan.serialize.strategy.*;
import java.util.HashMap;
import java.util.Map;
public class SerializeFactory {
private static final Map<String, SerializeStrategyFactory<?>> strategyMap = new HashMap<>();
static {
strategyMap.put("json", JsonSerialize::new);
strategyMap.put("java", SimpleSerialize::new);
strategyMap.put("hessian", HessianSerialize::new);
strategyMap.put("kry", KryoSerialize::new);
strategyMap.put("proto", ProtostuffSerialize::new);
}
public static <T> SerializeStrategy<T> getSerialize(String strategy, Class<T> clazz) {
SerializeStrategyFactory<T> factory = (SerializeStrategyFactory<T>) strategyMap.get(strategy);
if (factory != null) {
return factory.create(clazz);
}
return null;
}
@FunctionalInterface
private interface SerializeStrategyFactory<T> {
SerializeStrategy<T> create(Class<T> clazz);
}
}
为了让某些大聪明和领导感觉高大上, 本来几行代码的事我们生生的改成了几十行,有效的提高的自己的工作量,无形之中就增加了自己的产出。工厂有了 我们还需要一个通用的方法,方便集成到项目中使用类名加方法名直接进行序列化和反序列化
下面是对外暴露的静态方法
package org.wcan.serialize;
/**
* @Description
* @Author wcan
* @Date 2025/1/22 上午 10:30
* @Version 1.0
*/
public class SerializePlus {
private SerializePlus() {
}
private static <T> SerializeStrategy<T> getSerializeStrategy(String strategy, Class<T> clazz) {
return SerializeFactory.getSerialize(strategy, clazz);
}
public static <T> byte[] serializer(String strategy, T object, Class<T> clazz) {
return getSerializeStrategy(strategy, clazz).serialize(object);
}
public static <T> T deserialize(String strategy, byte[] bytes, Class<T> clazz) {
return getSerializeStrategy(strategy, clazz).deserialize(bytes);
}
}
5.3、引入Pojo测试
前面我们已经搭建好了一个通用的序列化框架了,下面我们再来引入一组Pojo对象进行测试,我们直接把cheese 里面请求和响应对象搬过来吧
package org.wcan.cheese;
import java.io.Serializable;
/**
* @Description
* @Author wcan
* @Date 2025/1/16 下午 23:37
* @Version 1.0
*/
public interface CheeseProtocol extends Serializable {
public void setResponseCode(Integer responseCode);
public void setServerIp(String serverIp);
}
package org.wcan.cheese;
/**
* @Description
* @Author wcan
* @Date 2025/1/17 下午 17:12
* @Version 1.0
*/
public class AbstractCheeseProtocol implements CheeseProtocol {
public Integer responseCode = 200;
public String serverIp = null;
public Integer getResponseCode() {
return responseCode;
}
public String getServerIp() {
return serverIp;
}
@Override
public void setResponseCode(Integer responseCode) {
this.responseCode= responseCode;
}
@Override
public void setServerIp(String serverIp) {
this.serverIp = serverIp;
}
}
package org.wcan.cheese;
/**
* @Description Cheese 请求对象
* @Author wcan
* @Date 2025/1/16 下午 23:15
* @Version 1.0
*/
public class CheeseResponse extends AbstractCheeseProtocol {
private Object returnValue;
private Exception exceptionValue;
public CheeseResponse() {
}
public CheeseResponse(Object returnValue, Exception exceptionValue) {
this.returnValue = returnValue;
this.exceptionValue = exceptionValue;
}
public Object getReturnValue() {
return returnValue;
}
public void setReturnValue(Object returnValue) {
this.returnValue = returnValue;
}
public Exception getExceptionValue() {
return exceptionValue;
}
public void setExceptionValue(Exception exceptionValue) {
this.exceptionValue = exceptionValue;
}
@Override
public String toString() {
return "CheeseResponse{" +
"returnValue=" + returnValue +
", responseCode=" + responseCode +
", exceptionValue=" + exceptionValue +
'}';
}
}
package org.wcan.cheese;
import java.util.Arrays;
import java.util.Objects;
/**
* @Description Cheese 请求对象
* @Author wcan
* @Date 2025/1/16 下午 23:15
* @Version 1.0
*/
public class CheeseRequest extends AbstractCheeseProtocol{
public String className;
public String methodName;
private Class<?> returnValueType;
private Class[] parameterTypes;
private Object[] parameterValue;
public CheeseRequest() {
}
public CheeseRequest(String className, String methodName, Class<?> returnValueType, Class[] parameterTypes, Object[] parameterValue) {
this.className = className;
this.methodName = methodName;
this.returnValueType = returnValueType;
this.parameterTypes = parameterTypes;
this.parameterValue = parameterValue;
}
public String getClassName() {
return className;
}
public void setClassName(String className) {
this.className = className;
}
public String getMethodName() {
return methodName;
}
public void setMethodName(String methodName) {
this.methodName = methodName;
}
public Class<?> getReturnValueType() {
return returnValueType;
}
public void setReturnValueType(Class<?> returnValueType) {
this.returnValueType = returnValueType;
}
public Class[] getParameterTypes() {
return parameterTypes;
}
public void setParameterTypes(Class[] parameterTypes) {
this.parameterTypes = parameterTypes;
}
public Object[] getParameterValue() {
return parameterValue;
}
public void setParameterValue(Object[] parameterValue) {
this.parameterValue = parameterValue;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CheeseRequest that = (CheeseRequest) o;
return Objects.equals(className, that.className) && Objects.equals(methodName, that.methodName) && Objects.equals(returnValueType, that.returnValueType) && Arrays.equals(parameterTypes, that.parameterTypes) && Arrays.equals(parameterValue, that.parameterValue);
}
@Override
public int hashCode() {
int result = Objects.hash(className, methodName, returnValueType);
result = 31 * result + Arrays.hashCode(parameterTypes);
result = 31 * result + Arrays.hashCode(parameterValue);
return result;
}
@Override
public String toString() {
return "CheeseRequest{" +
"className='" + className + '\'' +
", methodName='" + methodName + '\'' +
", returnValueType=" + returnValueType +
", parameterTypes=" + Arrays.toString(parameterTypes) +
", parameterValue=" + Arrays.toString(parameterValue) +
'}';
}
}
添加后工程结构如下
最后我们编写测试方法 就能看到效果了
public class Startup {
@Test
public void serializePlusTest() throws InterruptedException {
CheeseRequest cheeseRequest = new CheeseRequest("org.wcan.test.TestDemo", "demoTest", String.class,
new Class[]{String.class, Integer.class}, new Object[]{"乔峰", 35});
CheeseResponse cheeseResponse = new CheeseResponse();
cheeseResponse.setReturnValue("哈哈哈");
cheeseResponse.setResponseCode(200);
// byte[] bytes = SerializePlus.serializer("java", cheeseRequest, CheeseRequest.class);
// CheeseRequest deserialize = SerializePlus.deserialize("java", bytes, CheeseRequest.class);
byte[] javas = SerializePlus.serializer("java", cheeseResponse, CheeseResponse.class);
CheeseResponse deserialize = SerializePlus.deserialize("java", javas, CheeseResponse.class);
System.out.println(deserialize);
}
}
5.4、引入Socket测试
5.4.1、流程分析
下面我们来编写一个Socket传输的案例,这个过程就是一个简单的RPC调用
1、客户端将对象序列化后发送给服务端
2、服务端接收到对象后进行反序列化解析出对象的数据
3、服务端进行反射调用目标方法,获取到执行结果
4、将执行结果封装成响应对象,经过序列化后发送给客户端
5、客户端通过反序列化解析出响应的数据
5.4.2、服务端实现
服务端代码如下
public class TestDemo {
public String demoTest(String userName,Integer age){
return "hi "+ userName +" 你的 age is : "+age;
}
}
package org.wcan.server;
import org.wcan.cheese.CheeseRequest;
import org.wcan.cheese.CheeseResponse;
import java.lang.reflect.Method;
/**
* @Description
* @Author wcan
* @Date 2025/1/16 下午 23:26
* @Version 1.0
*/
public class ReflectionExecute {
public static CheeseResponse execute(CheeseRequest cheeseRequest) {
CheeseResponse cheeseResponse = new CheeseResponse();
String className = cheeseRequest.getClassName();
String methodName = cheeseRequest.getMethodName();
try {
Class<?> aClass = Class.forName(className);
Method method = aClass.getMethod(methodName, cheeseRequest.getParameterTypes());
Object invoke = method.invoke(aClass.newInstance(), cheeseRequest.getParameterValue());
cheeseResponse.setReturnValue(invoke);
} catch (Exception e) {
cheeseResponse.setExceptionValue(e);
}
return cheeseResponse;
}
}
package org.wcan.server;
import org.wcan.cheese.CheeseRequest;
import org.wcan.cheese.CheeseResponse;
import org.wcan.serialize.SerializePlus;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
/**
* @ClassName ServerEndpoint
* @Description TODO
* @Author wcan
* @Date 2025/2/11 上午 10:35
* @Version 1.0
*/
public class ServerEndpoint {
private int cheesePort = 8000; // 监听端口
public void ServerStart(String strategy) {
try {
// 打开服务器端的 ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new java.net.InetSocketAddress(cheesePort));
serverSocketChannel.configureBlocking(false);
// 打开 Selector
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服务器正在运行,监听端口 " + cheesePort);
//TODO 注册端口
while (true) {
// 阻塞,等待 I/O 事件发生
selector.select();
// 获取所有发生的事件
Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
while (selectedKeys.hasNext()) {
SelectionKey key = selectedKeys.next();
selectedKeys.remove();
if (key.isAcceptable()) {
// 接受连接请求
handleAccept(serverSocketChannel, selector);
} else if (key.isReadable()) {
// 处理读取请求
handleResponse(key,strategy);
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
private static void handleAccept(ServerSocketChannel serverSocketChannel, Selector selector) throws IOException {
// 接受客户端连接
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
// 注册到 Selector,监听读事件
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("新连接接入:" + socketChannel.getRemoteAddress());
}
private void handleResponse(SelectionKey key,String strategy) throws IOException {
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
if (bytesRead == -1) {
socketChannel.close();
System.out.println("连接关闭");
return;
}
buffer.flip();
byte[] request = null;
if (buffer.remaining() > 0) {
request = new byte[buffer.remaining()];
buffer.get(request);
} else {
System.out.println("没有数据可读");
return;
}
//反序列化
CheeseRequest cheeseRequest = SerializePlus.deserialize(strategy, request, CheeseRequest.class);
CheeseResponse cheeseResponse = ReflectionExecute.execute(cheeseRequest);
//序列化
byte[] serialize = SerializePlus.serializer(strategy, cheeseResponse, CheeseResponse.class);
buffer.clear();
buffer.put(serialize);
buffer.flip();
// 发送响应数据
while (buffer.hasRemaining()) {
socketChannel.write(buffer);
}
socketChannel.close();
System.out.println("响应已发送");
}
}
5.4.3、客户端实现
package org.wcan.client;
import org.wcan.cheese.CheeseRequest;
import org.wcan.cheese.CheeseResponse;
import org.wcan.serialize.SerializePlus;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
/**
* @Description
* @Author wcan
* @Date 2025/1/17 下午 15:13
* @Version 1.0
*/
public class ClientEndpoint {
private final static int cheesePort = 8000; // 监听端口
public CheeseResponse doRequest(CheeseRequest cheeseRequest,String strategy) {
CheeseResponse cheeseResponse = null;
try (SocketChannel client = SocketChannel.open(new InetSocketAddress(cheeseRequest.getServerIp(), cheesePort))) {
client.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
byte[] serialize = SerializePlus.serializer(strategy, cheeseRequest, CheeseRequest.class);
buffer.put(serialize);
buffer.flip();
while (buffer.hasRemaining()) {
client.write(buffer);
}
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int bytesRead = 0;
while (bytesRead == 0) {
bytesRead = client.read(readBuffer);
}
if (bytesRead > 0) {
readBuffer.flip();
byte[] dst = new byte[readBuffer.limit()];
readBuffer.get(dst);
cheeseResponse = SerializePlus.deserialize(strategy, dst, CheeseResponse.class);
System.out.println("Response from server: " + cheeseResponse);
}
} catch (IOException e) {
throw new RuntimeException("IO Exception occurred during request processing", e);
}
return cheeseResponse;
}
}
5.4.4、功能测试
@Before
public void Startup() {
new Thread(() -> {
new ServerEndpoint().ServerStart("hessian");
}).start();
}
@Test
public void serializeTest() throws InterruptedException {
CheeseRequest cheeseRequest = new CheeseRequest("org.wcan.test.TestDemo", "demoTest", String.class,
new Class[]{String.class, Integer.class}, new Object[]{"乔峰", 35});
cheeseRequest.setServerIp("127.0.0.1");
Thread.sleep(1000);
CheeseResponse cheeseResponse = new ClientEndpoint().doRequest(cheeseRequest, "hessian");
System.out.println(cheeseResponse.getReturnValue());
}
这里我们可以根据 doRequest 方法的 strategy 参数 动态的调整序列化的实现方式,需要注意的是 ServerStart 也要保持一致。
5、总结
本篇文章给大家介绍了序列化相关的内容以及常用的序列化方案,并且都提供了对应的代码案例,最后我们编写了一个支持多种序列化方式的通用组件,完成了一个基本的RPC调用的案例,将序列化和Socket串起来了。相信现在的你肯定知道了序列化的作用了,也意识到一个高效的序列化算法 对网络传输是的重要性了。
相关代码仓库:cheese-store