在项目中使用Kryo来做序列化,首先我们需要引入相关的依赖,如下:
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.2</version>
</dependency>
主要注意的是,Kryo作为一个快速序列化/反序列化工具,其使用了字节码生成机制(底层依赖了 ASM 库),因此具有比较好的运行速度。但是一般我们可能会在Spring框架中使用,而Spring底层也用了这个库 。但是如果Kryo 使用的版本比较高;而 Spring 用的版本较低; 如果 pom 里的 Kryo 和 Spring 的顺序不对的话,Kryo 就会读到低版本的 ASM ,就会出错。
这里加了个 shaded 就能解决了,如下:
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo-shaded</artifactId>
<version>4.0.2</version>
</dependency>
然后我们就来看一看Kryo最最基础的使用方法,其实大致和我们使用JDK自带的序列化步骤差不多,在 对象流——ObjectOutputStream和ObjectInputSteam 中,我们就介绍了相关的步骤,这里我们就来看看使用Kryo如何实现。
这里我们还是用之前的Dog对象来进行测试,该Bean的内容如下,这里就不是必须要求实现我们的Serializable 接口啦
public class Dog {
private String name;
private int age;
public Dog(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
然后我们来看一看,如何利用Kryo来将对象写入文件之中
public class KryoTest {
public static void main(String[] args) throws FileNotFoundException {
Dog dog = new Dog("金毛", 2);
File file = new File("F:\\demo\\dog.bin");
try (Output output = new Output(new FileOutputStream(file))) {
Kryo kryo = new Kryo();
kryo.writeObject(output, dog);
}
}
}
是不是很简单,那么我们如何将刚刚写入文件中的对象在读取出来呢?这里就需要利用与Output对于的Input了,如下:
public class KryoTest {
public static void main(String[] args) throws FileNotFoundException {
File file = new File("F:\\demo\\dog.bin");
try(Input input = new Input(new FileInputStream(file))){
Kryo kryo = new Kryo();
Dog dog = kryo.readObject(input, Dog.class);
System.out.println(dog.toString());
}
}
}
其实Kryo大体有三种序列化方法,每种方式都有其优势和劣势。上述就是第一种,也是最为简单的一种,就是writeObject/readObject ,这种方法只会序列化对象实例,而不会记录对象所属类的任何信息。优势是最节省空间,劣势是在反序列化时,需要提供类作为模板才能顺利反序列。如上述,我们在反序列化时,就提供了Dog.class进行反序列化。
第二种就是使用 writeClassAndObject ,这种方法会先将对象所属类的全限定名序列化,然后再依次序列化对象实例的成员。优势是完全动态序列化,整个Kryo周期都不需要提供类信息。反序列化时使用 readClassAndObject 。这算是kryo的一个特点,可以把对象信息直接写到序列化数据里,反序列化的时候可以精确地找到原始类信息,不会出错,这意味着在写readClassAndObject 方法时,无需传入Class或Type类信息。
第三种也是使用 writeClassAndObject ,但是我们在这之前会先进行注册,事先将需要序列化的类注册给 Kryo(此时类和唯一id绑定),之后使用 writeClassAndObject 序列化时,只会序列化注册id,而不会序列化类的全限定名了,这样大大节省了空间(通常只比writeObject多一个字节)。反序列化时使用readClassAndObject 。但是特别需要注意序列化和反序列的Kryo的注册信息应当保持一致。
从上述三种方式来看的话,肯定是第三种采用类注册的writeClassAndObject 的方法最为理想,但是对类进行注册。注册行为会给每一个 Class 编一个号码,从 0 开始,然后注意序列化和反序列的Kryo的注册信息应当保持一致。而有时 Kryo 并不保证同一个 Class 每一次的注册的号码都相同(比如重启 JVM 后,用户访问资源的顺序不同,就会导致类注册的先后顺序不同);还有的情况就是同样的Class在不同的机器上注册编号任然不能保证一致,所以多机器部署时候反序列化可能会出现问题。所以kryo默认会禁止类注册,当然如果想要打开这个属性,可以通过 kryo.setRegistrationRequired(true);
打开。
还有需要注意的就是 Kryo 对循环引用的支持。References即引用,对A对象序列化时,默认情况下 Kryo会在每个成员对象第一次序列化时写入一个数字,该数字逻辑上就代表了对该成员对象的引用,如果后续有引用指向该成员对象,则直接序列化之前存入的数字即可,而不需要再次序列化对象本身。
而 “循环引用” 是指,假设有一个 JavaBean,假设是一个销售订单(SalesOrder),这个订单下面有很多子订单,比如 List<SalesOrderLine> ,而销售子订单中又有其中一个包括一个销售订单,那么这就构成了"循环引用"。Kryo 默认是支持循环引用的,当你确定不会有循环引用发生的时候,可以通过 kryo.setReferences(false);
关闭循环引用检测,从而提高一些性能。关闭后虽然序列化速度更快,但是遇到循环引用,就会报 “栈内存溢出” 错误。
如我们采用类注册的方式,我们可以注册一些常用的类,然后可以作为一个Kryo的工厂类可以用来直接使用,如下:
public class KryoFactory {
public static Kryo createKryo() {
Kryo kryo = new Kryo();
kryo.setRegistrationRequired(false);
kryo.register(Arrays.asList("").getClass(), new ArraysAsListSerializer());
kryo.register(GregorianCalendar.class, new GregorianCalendarSerializer());
kryo.register(InvocationHandler.class, new JdkProxySerializer());
kryo.register(BigDecimal.class, new DefaultSerializers.BigDecimalSerializer());
kryo.register(BigInteger.class, new DefaultSerializers.BigIntegerSerializer());
kryo.register(Pattern.class, new RegexSerializer());
kryo.register(BitSet.class, new BitSetSerializer());
kryo.register(URI.class, new URISerializer());
kryo.register(UUID.class, new UUIDSerializer());
UnmodifiableCollectionsSerializer.registerSerializers(kryo);
SynchronizedCollectionsSerializer.registerSerializers(kryo);
kryo.register(HashMap.class);
kryo.register(ArrayList.class);
kryo.register(LinkedList.class);
kryo.register(HashSet.class);
kryo.register(TreeSet.class);
kryo.register(Hashtable.class);
kryo.register(Date.class);
kryo.register(Calendar.class);
kryo.register(ConcurrentHashMap.class);
kryo.register(SimpleDateFormat.class);
kryo.register(GregorianCalendar.class);
kryo.register(Vector.class);
kryo.register(BitSet.class);
kryo.register(StringBuffer.class);
kryo.register(StringBuilder.class);
kryo.register(Object.class);
kryo.register(Object[].class);
kryo.register(String[].class);
kryo.register(byte[].class);
kryo.register(char[].class);
kryo.register(int[].class);
kryo.register(float[].class);
kryo.register(double[].class);
return kryo;
}
}
上述我们使用了UnmodifiableCollectionsSerializer 和 SynchronizedCollectionsSerializer
这里是因为 Kryo 在序列化时如果是不可修改的类默认是有问题的,以及默认的序列化器不支持java.util.Collections$SynchronizedRandomAccessList 这种没有无参构造函数的类,可能会抛出异常。但是使用上述的方法,我们需要引入下面的依赖
<dependency>
<groupId>de.javakaffee</groupId>
<artifactId>kryo-serializers</artifactId>
<version>0.42</version>
</dependency>
然后我们在使用Kryo做序列化的时候,就可以直接使用上述的工厂类获取Kryo实例进行处理的
public class KryoSerializer {
private static Kryo kryo = KryoFactory.createKryo();
public static void serialize() throws IOException {
Dog dog = new Dog("金毛", 2);
File file = new File("F:\\demo\\dog.bin");
try(Output output = new Output(new FileOutputStream(file))){
kryo.writeClassAndObject(output, dog);
}
}
public static void deserialize() throws FileNotFoundException {
File file = new File("F:\\demo\\dog.bin");
try(Input input = new Input(new FileInputStream(file))){
Object object = kryo.readClassAndObject(input);
Dog dog = (Dog) object;
System.out.println(dog.toString());
}
}
public static void main(String[] args) throws IOException {
serialize();
deserialize();
}
}
上述我们只是简单的演示写入、读取文件,一般在项目中,我们可以使用 ByteArrayOutputStream 来处理,不过需要注意的是,无论是哪种方式,我们需要明确一点,就是Output对象默认的缓存字节数并不大,实际对象超出大小的时候,系列化的时候并不会报告错误,但是系列化结果已经不完整,从而就会导致反系列化的时候失败,所以我们有时需要特意的指定更大的bufferSize,如下:
public class KryoSerializer {
private static Kryo kryo = KryoFactory.createKryo();
public static byte[] serialize(Object object) throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
Output output = new Output(baos, 8192)) {
kryo.writeClassAndObject(output, object);
output.flush();
byte[] bytes = baos.toByteArray();
baos.flush();
return bytes;
}
}
public static Object deserialize(byte[] bytes) throws IOException {
try (ByteArrayInputStream bios = new ByteArrayInputStream(bytes);
Input input = new Input(bios)) {
return kryo.readClassAndObject(input);
}
}
}
上述我们说了这么久的Kryo,其实还有一个比较重要的问题,就是Kryo其本身是线程不安全的,所以我们在并发编程时,需要特别的注意这一点,一般我们有两种较为常用的方法,其中一种就是使用 Kryo 为我们提供的 Pool,其用法如下:
public class KryoTest {
public KryoPool newKryoPool() {
return new KryoPool.Builder(() -> {
final Kryo kryo = new Kryo();
kryo.setInstantiatorStrategy(new Kryo.DefaultInstantiatorStrategy(new StdInstantiatorStrategy()));
return kryo;
}).softReferences().build();
}
public static void main(String[] args) {
KryoTest kryoTest = new KryoTest();
KryoPool pool = kryoTest.newKryoPool();
Kryo kryo = pool.borrow();
//相关序列化与反序列化操作
...
pool.release(kryo);
}
}
上述指定的默认 StdInstantiatorStrategy ,是为了解决可能由于实例化器指定的问题而抛出空指针异常。
InstantiatorStrategy
即初始化策略,默认kryo在反序列化对象时需要对象的类有一个零参数构造器,该构造器可以是private的,Kryo通过反射调用该构造器来实例化对象。如果没有这样一个构造器,就需要使用kryo.setInstantiatorStrategy(new StdInstantiatorStrategy())
了,该策略通过JVM API创建对象,这会创建一个完全空的对象(即不执行任何代码中的初始化工作),如果对象在实例化时需要一些初始化操作(比如在构造代码块中执行一些计算逻辑),这种策略就不可行了。
比较好的策略是kryo.setInstantiatorStrategy(new Kryo.DefaultInstantiatorStrategy(new StdInstantiatorStrategy()));
即先尝试通过零参数构造实例化对象,如果类中没有零参数构造器,则会使用StdInstantiatorStrategy策略。
StdInstantiatorStrategy
在创建对象时是依据JVM version信息及JVM vendor信息进行的,而不是依据Class的具体实现,其可以不调用对象的任何构造方法创建对象。
还有另一种方式就是通过我们的 ThreadLocal 来实现,有关ThreadLocal在并发编程中也进行介绍过
在我们实际的使用中,我们有时不仅要求将其序列化为二进制数据,可能有时我们希望得到的结果是字符串,那么我们只需要在Kryo序列化后,在使用Base64进行编码即可;在反序列化的时候,同样如此,首先先试用Base64,然后在使用Kryo进行反序列化,使用Base64我们需要引入相关的依赖,如下:
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.13</version>
</dependency>
利用上述我们所介绍的,下面就是一个我们常见的Kryo应用的工具类的,如下:
public class KryoUtil {
private static final String DEFAULT_ENCODING = "UTF-8";
//每个线程Kryo的实例副本
private static final ThreadLocal<Kryo> kryoLocal = new ThreadLocal<Kryo>() {
@Override
protected Kryo initialValue() {
Kryo kryo = new Kryo();
//支持对象循环引用(否则会栈溢出),其默认值为true
kryo.setReferences(true);
//默认值就是false
kryo.setRegistrationRequired(false);
//Fix the NPE bug when deserializing Collections.
((Kryo.DefaultInstantiatorStrategy) kryo.getInstantiatorStrategy())
.setFallbackInstantiatorStrategy(new StdInstantiatorStrategy());
return kryo;
}
};
/**
* 获得当前线程的Kryo实例副本
*/
public static Kryo getInstance() {
return kryoLocal.get();
}
//-----------------------------------------------
// 序列化/反序列化对象,及类型信息
// 序列化的结果里,包含类型的信息
// 反序列化时不再需要提供类型
//-----------------------------------------------
/**
* 将对象【及类型】序列化为字节数组
*
* @param obj 任意对象
* @param <T> 对象的类型
* @return 序列化后的字节数组
*/
public static <T> byte[] writeToByteArray(T obj) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Output output = new Output(byteArrayOutputStream);
Kryo kryo = getInstance();
kryo.writeClassAndObject(output, obj);
output.flush();
return byteArrayOutputStream.toByteArray();
}
/**
* 将对象【及类型】序列化为 String
* 利用了 Base64 编码
*
* @param obj 任意对象
* @param <T> 对象的类型
* @return 序列化后的字符串
*/
public static <T> String writeToString(T obj) {
try {
return new String(Base64.encodeBase64(writeToByteArray(obj)), DEFAULT_ENCODING);
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
/**
* 将字节数组反序列化为原对象
*
* @param byteArray writeToByteArray 方法序列化后的字节数组
* @param <T> 原对象的类型
* @return 原对象
*/
@SuppressWarnings("unchecked")
public static <T> T readFromByteArray(byte[] byteArray) {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArray);
Input input = new Input(byteArrayInputStream);
Kryo kryo = getInstance();
return (T) kryo.readClassAndObject(input);
}
/**
* 将 String 反序列化为原对象
* 利用了 Base64 编码
*
* @param str writeToString 方法序列化后的字符串
* @param <T> 原对象的类型
* @return 原对象
*/
public static <T> T readFromString(String str) {
try {
return readFromByteArray(Base64.decodeBase64(str.getBytes(DEFAULT_ENCODING)));
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
//-----------------------------------------------
// 只序列化/反序列化对象
// 序列化的结果里,不包含类型的信息
//-----------------------------------------------
/**
* 将对象序列化为字节数组
*
* @param obj 任意对象
* @param <T> 对象的类型
* @return 序列化后的字节数组
*/
public static <T> byte[] writeObjectToByteArray(T obj) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Output output = new Output(byteArrayOutputStream);
Kryo kryo = getInstance();
kryo.writeObject(output, obj);
output.flush();
return byteArrayOutputStream.toByteArray();
}
/**
* 将对象序列化为 String
* 利用了 Base64 编码
*
* @param obj 任意对象
* @param <T> 对象的类型
* @return 序列化后的字符串
*/
public static <T> String writeObjectToString(T obj) {
try {
return new String(Base64.encodeBase64(writeObjectToByteArray(obj)), DEFAULT_ENCODING);
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
/**
* 将字节数组反序列化为原对象
*
* @param byteArray writeToByteArray 方法序列化后的字节数组
* @param clazz 原对象的 Class
* @param <T> 原对象的类型
* @return 原对象
*/
@SuppressWarnings("unchecked")
public static <T> T readObjectFromByteArray(byte[] byteArray, Class<T> clazz) {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArray);
Input input = new Input(byteArrayInputStream);
Kryo kryo = getInstance();
return kryo.readObject(input, clazz);
}
/**
* 将 String 反序列化为原对象
* 利用了 Base64 编码
*
* @param str writeToString 方法序列化后的字符串
* @param clazz 原对象的 Class
* @param <T> 原对象的类型
* @return 原对象
*/
public static <T> T readObjectFromString(String str, Class<T> clazz) {
try {
return readObjectFromByteArray(Base64.decodeBase64(str.getBytes(DEFAULT_ENCODING)), clazz);
} catch (UnsupportedEncodingException e) {
throw new IllegalStateException(e);
}
}
}