泛型新认知

18 阅读5分钟

概要

想要成为一个写好java代码的Coder,泛型一定是不可少的。大家都听说过java的泛型是伪泛型,是编译期进行校验使用的,而不是在运行期生效。 怎么理解? 就是泛型里的类信息在实际运行的时候,java对象本身是获取不到的,需要从java对象的类信息才能获取到。

注:本文是通过protostuff框架,实现了protobuf的序列化/反序列化功能

protostuff框架地址

一个小demo

序列化

我们今天通过一个序列化/反序列化的例子来验证说明一下。我们的demo是基于kafka的序列化,反序列化接口来进行实现。只保留需要我们实现的接口方法

public interface Serializer<T> extends Closeable {

    /**
     * Convert {@code data} into a byte array.
     *
     * @param topic topic associated with data
     * @param data typed data
     * @return serialized bytes
     */
    byte[] serialize(String topic, T data);

}

此时我们有个PO对象类:Company

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Company {
    private String name;
    private String address;
}

然后我们很快写出来了Company的序列化类

public class ProtoCompanySerializer implements Serializer<Company> {
    
    @Override
    public byte[] serialize(String topic, Company data) {
        if (data == null) {
            return null;
        }
        Schema<Company> schema = RuntimeSchema.getSchema(Company.class);
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        return ProtostuffIOUtil.toByteArray(data, schema, buffer);
    }
}

看着很简单,是吧?不过如果我们有多个PO类呢?譬如还有一个User类,再写一个

public class ProtoUserSerializer implements Serializer<User> {

    @Override
    public byte[] serialize(String topic, User data) {
        if (data == null) {
            return null;
        }
        Schema<User> schema = RuntimeSchema.getSchema(User.class);
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        return ProtostuffIOUtil.toByteArray(data, schema, buffer);
    }
}

这么写下去,确实是很清晰,但是我们很快就能发现这两个类的实现,除了入参类型不同、Schema泛型不同,其他都是一样的。那写个通用的不就行了?

public class ProtostuffSerializer implements Serializer {

    @Override
    public byte[] serialize(String topic, Object data) {
        if (data == null) {
            return null;
        }
        Schema schema = RuntimeSchema.getSchema(data.getClass());
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        return ProtostuffIOUtil.toByteArray(data, schema, buffer);
    }
}

Object类型的对象作为入参,看着很别扭,是吧?但是实际运行没有任何问题,当然我们可以通过泛型,让代码看起来稍微优雅一些

public class ProtoCommonSerializer<T> implements Serializer<T> {

    @Override
    public byte[] serialize(String topic, T data) {
        if (data == null) {
            return null;
        }
        Schema<T> schema = (Schema<T>) RuntimeSchema.getSchema(data.getClass());
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        return ProtostuffIOUtil.toByteArray(data, schema, buffer);
    }
}

但是这样的优雅其实并没有什么大用,因为为了支持一个序列化工具对象序列化多种类型的对象,实际我们在实例化ProtoCommonSerializer时,根本没法指定泛型 CommonSerializer的使用情况

反序列化

public interface Deserializer<T> extends Closeable {

    /**
     * Deserialize a record value from a byte array into a value or object.
     * @param topic topic associated with the data
     * @param data serialized bytes; may be null; implementations are recommended to handle null by returning a value or null rather than throwing an exception.
     * @return deserialized typed data; may be null
     */
    T deserialize(String topic, byte[] data);

    

有了上面的先验认知,我们当然想直接写通用的反序列化工具类,然后我们开始动笔,然后很快就发现一个问题,入参没有对象,我们没法判断入参的data这个字节数组要转化为哪个Class的对象 那换成上面后来写的泛型那种呢?

public class ProtoCommonDeserializer<T> implements Deserializer<T> {

    @SneakyThrows
    @Override
    public T deserialize(String topic, byte[] data) {
        if (data == null) {
            return null;
        }
        Class<T> tClazz = (Class<T>) GenericTypeResolver.resolveTypeArgument(this.getClass(), ProtoCommonDeserializer.class);
        Schema<T> schema = RuntimeSchema.getSchema(tClazz);
        T t = tClazz.newInstance();
        ProtostuffIOUtil.mergeFrom(data, t, schema);
        return t;
    }
}

然后美滋滋写个单测

@Test
public void testDeserialize1() {
    String topic = "abc";
    Company company = new Company("wangsansan", "not same place");
    ProtostuffSerializer protostuffSerializer = new ProtostuffSerializer();
    byte[] data = protostuffSerializer.serialize(topic, company);
    ProtoCommonDeserializer<Company> protoCommonDeserializer = new ProtoCommonDeserializer<Company>();
    Company company2 = protoCommonDeserializer.deserialize(topic, data);
    assert "wangsansan".equals(company2.getName());
}

一跑,哗擦,空指针。看了下异常栈,Schema<T> schema = RuntimeSchema.getSchema(tClazz);这一行空指针,也就是Class<T> tClazz = (Class<T>) GenericTypeResolver.resolveTypeArgument(this.getClass(), ProtoCommonDeserializer.class);没拿到Class信息,问了下豆包,豆包说出了那句我们都知道的八股文:

Java的泛型在运行时是擦除的,只有编译时有限制作用

而Spring的工具类GenericTypeResolver拿到的是运行时类信息,但是因为protoCommonDeserializer对象的类信息是ProtoCommonDeserializer,所以根本拿不到泛型参数,豆包给的解法是修改为 ProtoCommonDeserializer<Company> protoCommonDeserializer = new ProtoCommonDeserializer<Company>(){};注意,跟之前比,多了个花括号,也就是protoCommonDeserializer变成了一个匿名类的对象,而不再是ProtoCommonDeserializer的对象了,这个匿名类是ProtoCommonDeserializer<Company>的子类,且被load进jvm里了,所以该匿名类能查到的类的泛型参数 但是!!!

这样的话,protoCommonDeserializer就只能反序列化某一种类型的对象了

怎么办呢

如何实现一个反序列化对象可以通用,反序列化任何类型的对象呢?

参考ProtostuffSerializer写个ProtostuffDeserializer

public class ProtostuffDeserializer implements Deserializer {

    @Override
    public Object deserialize(String topic, byte[] data) {
        if (data == null) {
            return null;
        }
        Schema schema = RuntimeSchema.getSchema(Object.class);
        Company ans = new Company();
        ProtostuffIOUtil.mergeFrom(data, ans, schema);
        return ans;
    }
}

跑个单测试试

@Test
public void testDeserialize1() {
    String topic = "abc";
    Company company = new Company("wangsansan", "not same place");
    ProtostuffSerializer protostuffSerializer = new ProtostuffSerializer();
    byte[] data = protostuffSerializer.serialize(topic, company);
    ProtostuffDeserializer protostuffDeserializer = new ProtostuffDeserializer();
    Company company1 = (Company) protostuffDeserializer.deserialize(topic, data);
    assert "wangsansan".equals(company1.getName());
}

这次又是空指针,不过是protostuff框架报的了,其实是因为protostuff最外层的类信息是没有序列化到字节数组里,需要程序员指定? 何如?软件学的精髓是啥?封装,抽象呗,于是,我们再加一层

@NoArgsConstructor
@AllArgsConstructor
@Data
public class DomainWrapper<T> {

    private T domain;

    public static <T> DomainWrapper<T> of(T obj) {
        return new DomainWrapper<>(obj);
    }

}

写出了这样的通用反序列化类

public class ProtoWrapperDeserializer implements Deserializer<DomainWrapper> {


    @Override
    public DomainWrapper deserialize(String topic, byte[] data) {
        if (data == null) {
            return null;
        }
        Schema<DomainWrapper> schema = RuntimeSchema.getSchema(DomainWrapper.class);
        DomainWrapper ans = new DomainWrapper();
        ProtostuffIOUtil.mergeFrom(data, ans, schema);
        return ans;
    }
}

写个单测试试

@Test
public void testWrapperDeserialize() {
    String topic = "abc";
    Company company = new Company("wangsansan", "not same place");
    User user = new User("wangsansan", 25);
    ProtostuffSerializer protostuffSerializer = new ProtostuffSerializer();
    byte[] data1 = protostuffSerializer.serialize(topic, DomainWrapper.of(company));
    byte[] data2 = protostuffSerializer.serialize(topic, DomainWrapper.of(user));
    ProtoWrapperDeserializer protoWrapperDeserializer = new ProtoWrapperDeserializer();
    DomainWrapper<Company> obj1 = protoWrapperDeserializer.deserialize(topic, data1);
    DomainWrapper<User> obj2 = protoWrapperDeserializer.deserialize(topic, data2);
    assert obj1.getDomain().getName().equals("wangsansan");
    assert obj2.getDomain().getAge() == 25;
}

执行是没问题的,因为protostuffSerializer对象的类信息是有泛型的->DomainWrapper,ProtoWrapperDeserializer类本身是不支持设置泛型的。

问题

虽然都是BeanWrapper,但是反序列化时,ProtostuffIOUtil.mergeFrom(data, ans, schema);是怎么知道里面的domain是什么类型,然后反序列化成功的呢? 这个问题,我们下次探讨一下。