当前位置: 首页 > 工具软件 > Kryo > 使用案例 >

序列化-Kryo的使用详解

贾沛
2023-12-01

1. 简单使用

引入maven依赖:

<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo</artifactId>
    <version>4.0.2</version>
</dependency>

需要注意的是,由于kryo使用了较高版本的asm,可能会与业务现有依赖的asm产生冲突,这是一个比较常见的问题。只需将依赖改成:

<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo-shaded</artifactId>
    <version>4.0.2</version>
</dependency>

序列化:

public static byte[] serialize(UserDto dto) {
		Kryo kryo = new Kryo();
		kryo.register(UserDto.class);
		ByteArrayOutputStream bos = new ByteArrayOutputStream();
		Output output = new Output(bos);
		kryo.writeObject(output,dto);//写入null时会报错
		output.close();
		return bos.toByteArray();
	}

反序列化:

 public static UserDto deserialize(byte[] bytes) {
		Kryo kryo = new Kryo();
		kryo.register(UserDto.class);
		ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
		Input input = new Input(bis);
		UserDto userDto = kryo.readObject(input, UserDto.class);//读出null时会报错
		input.close();
		return userDto;
	}

正常情况下, 序列化类必须包含无参构造


2. 两种读写方式

根据是否写入class类型分为两种方式, 这里特别指出这里的的class指的是读写对象的class, 如果读写的是有嵌套类型对象,则不管采用哪种方式, 子类型class都会序列化.

2.1 只写实例信息

  • 知道class且对象不为null
kryo.writeObject(output, someObject);
    // ...
SomeClass someObject = kryo.readObject(input, SomeClass.class);
  • 知道class且对象可能为null
kryo.writeObjectOrNull(output, someObject);
    // ...
SomeClass someObject = kryo.readObjectOrNull(input, SomeClass.class);

2.2 同时写入class类型和实例信息

class未知且对象可能为null, 但这种场景, 会多占用空间. 这种方式是我们在RPC中应当使用的方式

kryo.writeClassAndObject(output, object);
    // ...
    Object object = kryo.readClassAndObject(input);
    if (object instanceof SomeClass) {
       // ...
    }

3 相关配置参数

  • 类注册

kryo支持通过类注册, 注册会给每一个class一个int类型的Id相关联,这显然比类名称高效,但同时要求反序列化的时候的Id必须与序列化过程中一致。这意味着注册的顺序非常重要。

kryo.register(SomeClassA.class);
kryo.register(SomeClassB.class);

但是由于现实原因,同样的代码,同样的Class在不同的机器上注册编号任然不能保证一致,所以多机器部署时候反序列化可能会出现问题。所以kryo默认会开启类注册(version:5.0.2),可以通过kryo.setRegistrationRequired(false)关闭, 关闭后Kryo会根据类型去loadClass关联

kryo.setRegistrationRequired(false);//一般设置为false

  • 循环引用检测

对循环引用的支持,可以有效防止栈内存溢出,kryo默认会打开这个属性。当你确定不会有循环引用发生的时候,可以通过kryo.setReferences(false); 关闭循环引用检测,从而提高一些性能。

kryo.setRegistrationRequired(true);//大多数情况下,请保持kryo.setReferences(true)

  • 实例化器
kryo.setInstantiatorStrategy(new Kryo.DefaultInstantiatorStrategy(
                    new StdInstantiatorStrategy()));

上面这句话显式指定了实例化器。

在一些依赖了kryo的开源软件中,可能由于实例化器指定的问题而抛出空指针异常。例如hive的某些版本中,默认指定了StdInstantiatorStrategy。

public static ThreadLocal<Kryo> runtimeSerializationKryo = new ThreadLocal<Kryo>() {
    @Override
    protected synchronized Kryo initialValue() {
      Kryo kryo = new Kryo();
      kryo.setClassLoader(Thread.currentThread().getContextClassLoader());
      kryo.register(java.sql.Date.class, new SqlDateSerializer());
      kryo.register(java.sql.Timestamp.class, new TimestampSerializer());
      kryo.register(Path.class, new PathSerializer());
      kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
      ......
      return kryo;
    };
  };

而StdInstantiatorStrategy在是依据JVM version信息及JVM vendor信息创建对象的,可以不调用对象的任何构造方法创建对象。那么例如碰到ArrayList这样的对象时候,就会出问题。观察一下ArrayList的源码:

public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

既然没有调用构造器,那么这里elementData会是NULL,那么在调用类似ensureCapacity方法时,就会抛出一个异常。

 public void ensureCapacity(int minCapacity) {
        if (minCapacity > elementData.length
            && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
                 && minCapacity <= DEFAULT_CAPACITY)) {
            modCount++;
            grow(minCapacity);
        }
    }

解决方案很简单,就如框架中代码写的一样,显示指定实例化器,首先使用默认无参构造策略DefaultInstantiatorStrategy,若创建对象失败再采用StdInstantiatorStrategy。


4. 解决线程不安全

由于Kryo线程不安全, 意味着每次序列化和反序列化时都需要实例化一次, 或借助ThreadLocal来维护以保证其线程安全。

private static final ThreadLocal<Kryo> kryos = new ThreadLocal<Kryo>() {
    protected Kryo initialValue() {
        Kryo kryo = new Kryo();
        // configure kryo instance, customize settings
        return kryo;
    };
};
// Somewhere else, use Kryo
Kryo k = kryos.get();
...

或者使用kryo提供的pool:

public KryoPool newKryoPool() {
        return new KryoPool.Builder(() -> {
            final Kryo kryo = new Kryo();
            kryo.setInstantiatorStrategy(new Kryo.DefaultInstantiatorStrategy(
                    new StdInstantiatorStrategy()));
            return kryo;
        }).softReferences().build();
}

5. 解决增删字段

kryo默认不支持Bean中增删字段

在实际开发中,class增删字段是很常见的事情,但对于kryo来说,确是不支持的,而如果恰好需要(集群)缓存数据,那么这个问题会被放得更大。例如一个对象使用kryo序列化后,数据放入了缓存中,而这时候如果这个对象增删了一个属性,那么缓存中反序列化的时候就会报错。所以频繁使用缓存的场景,可以尽量避免kryo。

不过现在的Kryo提供了兼容性的支持,使用CompatibleFieldSerializer.class,在kryo.writeClassAndObject时候写入的信息如下:

class name|field length|field1 name|field2 name|field1 value| filed2 value

而在读入kryo.readClassAndObject时,会先读入field names,然后匹配当前反序列化类的field和顺序再构造结果。对两种读写方式均有效. 当然如果在做好缓存隔离的情况下,这一切都不用在意。

kryo.setDefaultSerializer(new SerializerFactory.CompatibleFieldSerializerFactory());

也可以换成支持增减字段的其他框架, 如protoBuff.


6. 使用模版类

public class KryoSerializer {
    private static final ThreadLocal<Kryo> kryoLocal = ThreadLocal.withInitial(() -> {
		Kryo kryo = new Kryo();
		kryo.setReferences(true);//检测循环依赖,默认值为true,避免版本变化显式设置
		kryo.setRegistrationRequired(false);//默认值为true,避免版本变化显式设置
		((DefaultInstantiatorStrategy) kryo.getInstantiatorStrategy())
			.setFallbackInstantiatorStrategy(new StdInstantiatorStrategy());//设定默认的实例化器
		return kryo;
	});

	public byte[] serialize(Object obj) {
		Kryo kryo = getKryo();
		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
		Output output = new Output(byteArrayOutputStream);
		kryo.writeClassAndObject(output, obj);
		output.close();
		return byteArrayOutputStream.toByteArray();
	}

	public <T> T deserialize(byte[] bytes) {
		Kryo kryo = getKryo();
		Input input = new Input(new ByteArrayInputStream(bytes));
		return (T) kryo.readClassAndObject(input);
	}

	private Kryo getKryo() {
		return kryoLocal.get();
	}
}

 类似资料: