数据结构或者对象本身是无法在网络中进行传输的。当我们需要将数据结构或者对象在网络中进行传输,或者将其存储到文件系统,数据库中时,需要进行序列化。
在Java编程中存在一个问题:有的时候我们并没有进行序列化,却依然可以持久化到数据库。其实这是基本数据类型已经帮我们实现了Serializable接口(Java自带的序列化接口,将在后面介绍序列化方式时详细介绍)。我们可以看看我们的实体类中常用的数据类型,例如Date、String等等,它们本身已经实现了序列化接口,而一些基本类型,数据库里面有与之对应的数据结构。虽然从我们的类声明及使用来看,我们并没有进行序列化操作,但其实在声明的各个不同变量的时候,具体的数据类型帮助我们实现了序列化操作。
实现序列化的方式有很多,下面对一些比较常见的序列化方式做一个介绍。
根据大标题可知,通过这种方式实现的序列化不支持跨语言。且这个接口性能较差,序列化后所得的二进制串(Java中体现为字节数组)所占空间大,影响传输效率,所以实际使用不多。
查看Serializable接口源码,我们会发现,这个接口中没有任何方法,它仅仅是一个标志接口。我们只需在类声明中实现这个接口并不需要实现任何方法,JVM在加载类时看到这个标志会自动帮我们进行序列化。但是在实现该接口时,Java官方强烈建议我们提供一个serialVersionID,以便于后续反序列化的进行。
反序列化的过程中需要使用
serialVersionUID
来确定由哪个类来加载这个对象。在反序列化的过程中,如果接收方为对象加载了一个类,如果该对象的serialVersionUID
与对应持久化时的类不同,那么反序列化的过程中将会导致InvalidClassException
异常。如果我们在序列化中没有显示地声明
serialVersionUID
,则序列化运行时将会根据该类的各个方面计算该类默认的serialVersionUID
值。但是,Java官方强烈建议所有要序列化的类都显示地声明serialVersionUID
字段,因为如果高度依赖于JVM默认生成serialVersionUID
,可能会导致其与编译器的实现细节耦合,这样可能会导致在反序列化的过程中发生意外的InvalidClassException
异常。因此,为了保证跨不同Java编译器实现的serialVersionUID
值的一致,实现Serializable
接口的必须显示地声明serialVersionUID
字段。此外
serialVersionUID
字段地声明要尽可能使用private关键字修饰,这是因为该字段的声明只适用于声明的类,该字段作为成员变量被子类继承是没有用处的!有个特殊的地方需要注意的是,数组类是不能显示地声明serialVersionUID
的,因为它们始终具有默认计算的值,不过数组类反序列化过程中也是放弃了匹配serialVersionUID
值的要求。
setting→Editor→Inspections→搜索框中搜索UID→勾选Serializable class without ‘serialVersionUID‘
将光标移动到实现了Serializable接口的类上,添加serialVersionID。
有一些对象类不具有可持久化性,因为其数据的特性决定了它会经常变化,其状态只是瞬时的,这样的对象是无法保存其状态的,如Thread对象或流对象。对于这样的成员变量,必须用 transient 关键字标明,否则编译器将报错。任何用 transient 关键字标明的成员变量,都不会被保存。
hessian是一种跨语言的高效二进制序列化方式,顾名思义,hessian2是hessian的新版本。最近几年,新的高效序列化方式层出不穷(后文将介绍的Kryo便是其中的一种),且性能大多显著优于hessian2,但是dubbo默认的序列化方式仍然是阿里做过一定改进的hessian2。
下示Demo中还会将jdk自带的序列化接口与hessian2进行对比。
<dependency>
<groupId>com.caucho</groupId>
<artifactId>hessian</artifactId>
<version>4.0.65</version>
</dependency>
public class SerializeUtils {
public static byte[] hessianSerialize(Object o) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
hessian2Output.writeObject(o);
hessian2Output.flush();//强制输出缓冲区数据
byte[] bytes = byteArrayOutputStream.toByteArray();
byteArrayOutputStream.close();
hessian2Output.close();
return bytes;
}
public static Object hessianDeSerialize(byte[] bytes) throws IOException, ClassNotFoundException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
Object o = hessian2Input.readObject();
byteArrayInputStream.close();
hessian2Input.close();
return o;
}
public static byte[] jdkSerialize(Object o) throws IOException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(o);
objectOutputStream.flush();
byte[] bytes = byteArrayOutputStream.toByteArray();
byteArrayOutputStream.close();
objectOutputStream.close();
return bytes;
}
public static Object jdkDeSerialize(byte[] bytes) throws IOException, ClassNotFoundException {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
Object o = objectInputStream.readObject();
byteArrayInputStream.close();
objectInputStream.close();
return o;
}
}
public class User implements Serializable {
private static final long serialVersionUID = 3047430431797599333L;
private String id;
private String name;
private int age;
private boolean sex;
public User(String id, String name, int age, boolean sex) {
this.id = id;
this.name = name;
this.age = age;
this.sex = sex;
}
}
public class hessianVSjdk {
public static void main(String[] args) throws IOException, ClassNotFoundException {
User user = new User("1", "zhangsan",18,false);
long starttime = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
SerializeUtils.jdkDeSerialize(SerializeUtils.jdkSerialize(user));
}
long endtime = System.currentTimeMillis();
long starttimeH = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
SerializeUtils.hessianDeSerialize(SerializeUtils.hessianSerialize(user));
}
long endtimeH = System.currentTimeMillis();
System.out.println("jdk runtime:"+(endtime-starttime));
System.out.println("hessian2 runtime:"+(endtimeH-starttimeH));
System.out.println("jdk size:"+SerializeUtils.jdkSerialize(user).length);
System.out.println("hessian2 size:"+SerializeUtils.hessianSerialize(user).length);
}
}
Kryo是一种非常成熟的序列化实现,已经在Twitter、Groupon、Yahoo以及多个著名开源项目(如Hive、Storm)中广泛的使用。
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.0.3</version>
</dependency>
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
public class KryoSerializeUtil {
private static ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(()->{
Kryo kryo = new Kryo();
//关闭注册,默认值为true
kryo.setRegistrationRequired(false);
return kryo;
});
public static byte[] serialize(Object obj) throws Exception {
try (Output output = new Output(4096,4096)){
Kryo kryo = kryoThreadLocal.get();
kryo.writeObject(output,obj);
output.flush();
kryoThreadLocal.remove();
return output.toBytes();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static Object deserialize(byte[] bytes,Class<?> clazz) throws Exception {
try (Input input = new Input(bytes)){
Kryo kryo = kryoThreadLocal.get();
Object o = kryo.readObject(input, clazz);
kryoThreadLocal.remove();
return clazz.cast(o);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
Kryo不是线程安全的,所以我们使用ThradLocal存放kryo对象。
ThradLocal.withInitial()用于初始化ThreadLocal中的元素。这里使用withInitial()初始化,后面在serialize()和deserialize()才可以使用ThreadLocal.get()直接获取kryo对象。
kryo.setRegistrationRequired(false)
关闭注册。
默认情况下,Kryo即将读取或写入的类都必须实现注册,语法为kryo.register(xxx.class)
。注册会提供一个int类型的ID(可以类比serialVersionID),用于该类的序列化程序、反序列化以及创建该类实例的实例化器。需要注意的是,注册行为需要为每一个类进行注册。例如使用者注册了A类型,但是A类型内部引用了B类型,那么使用者必须手动注册B类型,且jdk自带的类型例如ArrayList等也是需要手动注册的。当需要注册的类数量比较大时,将会十分麻烦。
类别ID -1和-2保留。默认情况下,类ID 0-8用于原始类型和String,尽管这些ID可重新使用。ID最好从零开始,因此当它们是小的正整数时,效率最高,且负ID无法有效地序列化。需要注意,Kryo 并不保证同一个 Class 每一次的注册的号码都相同。也就是说,同样的代码、同一个 Class ,在两台机器上的注册编号可能不一致;那么,一台机器序列化之后的结果,可能就无法在另一台机器上反序列化。因此,对于多机器部署的情况,建议关闭注册。
关闭注册后,无需提供一个类别ID。在未注册类第一次进行序列化时,将会在其序列化信息中写入其全限定类名,以后在对这个类做序列化及反序列化操作时,都会使用其全限定类名作为标志。因此,可以考虑使用简短的包名称来减小序列化的大小。
(上面的说明来自Kryo README.md,水平一般能力有限,有疑问的可以参考一下原文。Instead of writing a varint class ID (often 1-2 bytes), the fully qualified class name is written the first time an unregistered class appears in the object graph. Subsequent appearances of that class within the same object graph are written using a varint. Short package names could be considered to reduce the serialized size.)
Output类提供了多种构造方法,其中包括将OutputStream作为参数的构造方法。查看Output的源码会发现,使用OutputStream作为参数的构造方法,实际调用的是public Output(int bufferSize, int maxBufferSize)
,且其中bufferSize=maxBufferSize=4096。Kryo官方文档写道,写入OutputStream时,输出会缓冲字节,因此flush
或close
必须在写入完成后调用,以使缓冲的字节被写入OutputStream。如果未向Output提供OutputStream,则无需调用flush
或close
。所以这里我不选择直接使用OutputStream作为参数的构造函数。
由于ThreadLocal是借用Thread中的threadLocals来实现的,所以在Threadlocal中,每个元素使用完之后,最好使用remove移出数据,以防影响下一次调用。
Output类中维护着一个byte[]类型的buffer,用于存储序列化生成的二进制串。
output.getBuffer()与toBytes()的区别:getBuffer()直接返回整个buffer,不论buffer中后面数据是否为空。而toBytes()会返回有数据的buffer(做法是通过position定位当前存入数据位置,通过System.arraycopy复制0-position的buffer)
public class TestClass{
private String name;
private int age;
private boolean sex;
public TestClass() {
}
public TestClass(String name, int age, boolean sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
@Override
public String toString() {
return "TestClass{" +
"name=‘" + name + ‘\‘‘ +
", age=" + age +
", sex=" + sex +
‘}‘;
}
}
public class KryoTest {
public static void main(String[] args) throws Exception {
TestClass testClass = new TestClass("zhangsan",18,false);
byte[] serialize = KryoSerializeUtil.serialize(testClass);
TestClass deserialize = (TestClass) KryoSerializeUtil.deserialize(serialize, TestClass.class);
System.out.println(deserialize.toString());
}
}
原文:https://www.cnblogs.com/snail-coderyu/p/14494946.html