likes
comments
collection
share

很遗憾,你可能真的不知道为什么需要Serializable

作者站长头像
站长
· 阅读数 22
  • 🍬 微信公众号: 后端时光

  • 🎨 所有文章第一时间都会在公众号发布,感兴趣的小伙伴快来关注吧~~

  • ⏰ 本篇文章共计 1969字 ,预计阅读用时6分钟

Serializable接口

在java rpc项目中经常见到这样的代码

@Data

public class PostCardInfoResp implements Serializable {

    private static final long serialVersionUID = 4106980050364098429L;

    

    /**

     * postId

     */

    private Long postId;

}

Serializable接口是什么,为什么要实现它,serialVersionUID是干什么的,这么长的数字是随便写的吗?它的作用是什么?

Serializable 名词在Java中解释为对象序列化,什么是对象序列化稍后再说。

查看 Serializable 源代码,里面却是一个空的接口:

package java.io;

/*

* @see java.io.ObjectOutputStream

* @see java.io.ObjectInputStream

* @see java.io.ObjectOutput

* @see java.io.ObjectInput

* @see java.io.Externalizable

/

public interface Serializable {

}

是空接口好像不需要实现什么方法,不实现Serializable会怎么样?既然不知道为啥继承Serializable,可以先去掉试下看看有什么影响

去除Serializable接口会怎样

通过提供以下两个rpc方法进行实验:

方法一:请求参数无,返回值对应的类没有实现Serializable接口

//返回值对象

@Data

public class UnSerializableResp {

    String msg;

}



//无序列化rpc方法

public UnSerializableResp serializableDemo1() {

    UnSerializableResp res = new UnSerializableResp();

    res.setMsg("demo1");

    return res;

}

方法二:请求参数无,返回值对应的类, 部分属性没有实现Serializable接口

//返回值对象

@Data

public class SerializableResp implements Serializable {

    String msg;

    private TestField testField;

    

    //静态内部类

    //没有实现Serializable接口

    @Data

    public static class TestField {

        String tips;

    }

}





//部分序列化rpc方法

@Log

@Override

public SerializableResp serializableDemo2() {

    SerializableResp res = new SerializableResp();

    res.setMsg("demo2");

    SerializableResp.TestField testFiled = new SerializableResp.TestField();

    testFiled.setTips("tips");

    res.setTestField(testFiled);

    return res;

}

go项目通过dubbo-go调用

//请求方法1

func SerializableDemo1(ctx context.Context) (data interface{}, err error) {

   request := base.NewRequest(ServiceName, "serializableDemo1")


   result, err := request.Call(ctx)

   if err != nil {

      err = errors.New(fmt.Sprintf("SerializableDemo request failed,err:%+v", err))

      return nil, err

   }

   sResult := cast.ToString(result)

   return sResult, nil

}



func SerializableDemo2(ctx context.Context) (data interface{}, err error) {

   request := base.NewRequest(ServiceName, "serializableDemo2")

   result, err := request.Call(ctx)

   if err != nil {

      err = errors.New(fmt.Sprintf("SerializableDemo request failed,err:%+v", err))

      return nil, err

   }

   sResult := cast.ToString(result)

   //fmt.Println("SerializableDemo2, sResult====", sResult)

   return sResult, nil

}

两个均请求成功

很遗憾,你可能真的不知道为什么需要Serializable

通过http网关调用

请求方法一:

curl --location --request POST 'java.test.com/serializabl…'

很遗憾,你可能真的不知道为什么需要Serializable

请求方法二:

curl --location --request POST 'java.test.com/serializabl…'

很遗憾,你可能真的不知道为什么需要Serializable

两个均请求成功

java项目通过dubbo调用

<dependency>

    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo</artifactId>
    <version>3.0.8</version>

</dependency>
/**

 * 测试

 */

public void testSerial() {

    UnSerializableResp res = productApi.serializableDemo1();

    log.info("serializableDemo1 success, res:{}", res);



    SerializableResp res2 = productApi.serializableDemo2();

    log.info("serializableDemo2 success, res2:{}", res2);

}

请求失败,提示:“Serialized class com.xxxx.SerializableResp$TestField must implement java.io.Serializable”

很遗憾,你可能真的不知道为什么需要Serializable

dubbo-go和http平台为什么会成功?

细节观察他们之间有所差异和相同

第一点:

dubbo-go和http平台都不是通过mvn引入jar包去调用的rpc接口(也不可能通过mvn引入jar包),java项目引入的方式是mvn。原来dubbo为了解决,不同编程语言之间调用rpc或消费者不依赖生产者jar包问题,提供了泛化调用能力。

dubbo的泛化调用

泛接口调用方式主要用于客户端没有API接口及模型类元的情况,参数及返回值中的所有POJO均用Map表示,通常用于框架集成,比如:实现一个通用的服务测试框架,可通过GenericService调用所有服务实现。

简单说:对于泛化调用,指不需要依赖服务的JAR包, 但还须要晓得服务提供方提供了哪些接口,只不过是程序不晓得而已,此时在程序中应用泛化调用,显示填入须要调用的接口名称,dubbo会进行匹配并进行调用后返回。消费者必须手动指定要调用的接口名、方法名、参数列表、版本号,分组等信息。

GenericService这个接口和java的反射调用非常像, 只需提供调用的方法名称, 参数的类型以及参数的值就可以直接调用对应方法了.

接口的实现如下:

package com.alibaba.dubbo.rpc.service;



/**

 * 通用服务接口

 */

public interface GenericService {



    /**

     * 泛化调用

     * 

     * @param method 方法名

     * @param parameterTypes 参数类型

     * @param args 参数列表

     * @return 返回值

     * @throws Throwable 方法抛出的异常

     */

    Object $invoke(String method, String[] parameterTypes, Object[] args) throws GenericException;

}

Dubbo 泛化调用和泛化实现依赖于下面两个过滤器 来完成。如下图:

  • GenericImplFilter:完成了消费者端的泛化功能。会将参数信息按照指定的序列化方式进行序列化后进行泛化调用

  • GenericFilter:完成了生产者端的泛化功能,本次重点关注这个

很遗憾,你可能真的不知道为什么需要Serializable

以下是项目调用流程图:

无法复制加载中的内容

可以先分析http网关实现逻辑, 熟悉了http网关,基本上也就了解了dubbo-go的原理

消费者逻辑 http 项目 DubboClient.java

//标准的泛化调用

ReferenceConfig<GenericService> reference = new ReferenceConfig<>();

//放入app config

reference.setApplication(applicationConfig);

//放入注册中心 config

reference.setRegistry(registryConfig);

//设置服务名,例如: 

reference.setInterface(serviceName);

//设置分组

reference.setGroup(group);

//是否使用dubbo原生协议

// RouteType.Native_Dubbo(4, "Native_Dubbo")

boolean nativeProto = apiInfo.getRouteType() == RouteType.Native_Dubbo.type();

if (nativeProto) {

    //设置为泛化调用

    //Constants.GENERIC_SERIALIZATION_DEFAULT = true

    reference.setGeneric(Constants.GENERIC_SERIALIZATION_DEFAULT);

} else {

    // Constants.GENERIC_SERIALIZATION_JSON = "json"
    reference.setGeneric(Constants.GENERIC_SERIALIZATION_JSON);

}

//默认v1

if (protocolVersion.equals("v1")) {

    //使用自己的系列化模式(性能好一些)

    // Constants.PROTOCOL_VERSION= "protocol_version"

    RpcContext.getContext().getAttachments().put(Constants.PROTOCOL_VERSION, "v1");

} else if (protocolVersion.equals("v2")) {
    RpcContext.getContext().getAttachments().put(Constants.PROTOCOL_VERSION, "v2");
}

//设置dubbo 版本

reference.setVersion(version);

//超时设置

reference.setTimeout(timeOut);



ReferenceConfigCache cache = ReferenceConfigCache.getCache();

genericService = cache.get(reference);

//方法名,参数类型,参数值

Object response = genericService.$invoke(methodName, typeAndValue.getLeft(), typeAndValue.getRight());

生产者逻辑 dubbo项目 GenericFilter.java

//Constants.GENERIC_KEY = "generic"

String generic = inv.getAttachment(Constants.GENERIC_KEY);

//true 或false

isJson = ProtocolUtils.isGenericSerialization(generic);

判断是否是序列化

很遗憾,你可能真的不知道为什么需要Serializable

第二点:

通过dubbo-go和http平台调用,返回结果都是字符串,rpc服务怎么知道应该返回字符串还是java对象?rpc 服务代码好像没有做什么逻辑处理

继续查看 dubbo项目 genericfilter.java

dubbo-go返回值为字符串的秘密:
//请求rpc接口

Result result = invoker.invoke(new RpcInvocation(method, args, inv.getAttachments()));

//是否是json序列化

if (isJson) {

     // Constants.PROTOCOL_VERSION= "protocol_version"

    if (inv.getAttachment(Constants.PROTOCOL_VERSION, "v1").equals("v1")) {

        //这个就是dubbo-go调用Java rpc服务,返回值为字符串的原因

        if (inv.getAttachment("raw_return", "").equals("true")) {

            return new RpcResult(gson.toJson(result.getValue()));

        }

        return new RpcResult(PojoUtils.generalize(result.getValue(), false));

    } else {

        //适用v2协议

        String filterField = inv.getAttachment(Constants.FILTER_FIELD, "");

        Gson gson = createGson(filterField);

        return new RpcResult(gson.toJson(result.getValue()));

    }

}
http网关返回值为字符串的秘密:

继续查看 http 项目 DubboClient.java

把返回对象就行了字符串转换

public FullHttpResponse call(FilterContext ctx, final ApiInfo apiInfo, final FullHttpRequest request) {

    //转换为字符串

    if (protocolVersion.equals("v1")) {

        GsonBuilder builder = new GsonBuilder();

        if (ctx.switchIsAllow(SwitchFlag.SWITCH_GSON_DISABLE_HTML_ESCAPING)) {

            builder.disableHtmlEscaping();

        }

        Gson gson = builder.create();

        if (ctx.switchIsAllow(SwitchFlag.SWITCH_DIRECT_TO_STRING)) {

            data = response.toString();

        } else {

            data = gson.toJson(response);

        }

    } else if (protocolVersion.equals("v2")) {

        data = response.toString();

    }

    return HttpResponseUtils.create(ByteBufUtils.createBuf(ctx, data, configService.isAllowDirectBuf()));

}

查看 HttpResponseUtils.create 方法

很遗憾,你可能真的不知道为什么需要Serializable

返回了一个httpResponse对象,并设置了 content-type=application/json; charset=utf-8

所以http网关返回值是字符串

对象序列化是什意思?

  1. 普通的Java对象的生命周期是仅限于一个JVM中的,只要JVM停止,这个对象也就不存在了,下次JVM启动我们还想使用这个对象怎么办呢?
  2. 或者我们想要把一个对象传递给另外一个JVM的时候,应该怎么做呢?

这两个问题的答案就是将该对象进行序列化,然后保存在文件中或者进行网络传输到另一个JVM,由另外一个JVM反序列化成一个对象,然后供JVM使用。

序列化Java中的序列化机制能够将一个实例对象信息写入到一个字节流中,序列化后的对象可用于网络传输,或者持久化到数据库、磁盘中。

常见的RPC 架构图

很遗憾,你可能真的不知道为什么需要Serializable

Dubbo 序列化

Java dubbo 默认使用序列化的协议是 hessian2,也就是传输对象序列化,它是二进制的RPC协议

常见的几种 dubbo 序列化协议

@SPI("hessian2")

public interface Serialization {

    byte getContentTypeId();

    

    String getContentType();



    @Adaptive

    ObjectOutput serialize(URL url, OutputStream output) throws IOException;



    @Adaptive

    ObjectInput deserialize(URL url, InputStream input) throws IOException;

}

Dubbo rpc 方法类都要实现Serializable接口的原因

dubbo在使用hessian2协议序列化方式的时候,对象的序列化使用的是JavaSerializer

com.alibaba.com.caucho.hessian.io.SerializerFactory#getDefaultSerializer

com.alibaba.com.caucho.hessian.io.SerializerFactory#getSerializer

com.alibaba.com.caucho.hessian.io.Hessian2Output#writeObject

获取默认的序列化方式的时候,会判断该参数是否实现了Serializable接口

protected Serializer getDefaultSerializer(Class cl) {

    if (_defaultSerializer != null)

        return _defaultSerializer;



    // 判断是否实现了Serializable接口

    if (!Serializable.class.isAssignableFrom(cl)

        && !_isAllowNonSerializable) {

        throw new IllegalStateException("Serialized class " + cl.getName() + " must implement java.io.Serializable");

    }



    return new JavaSerializer(cl, _loader);

}

如果没有实现Serializable接口的话就抛出异常。

所以说当对外提供的rpc方法,调用方是通过Java dubbo调用方式的话,Java 类对象都要实现Serializable接口,并且需要注意的是,如果类有静态内部类则也需要实现Serializable接口。否则同样会报错。如下图所示:

很遗憾,你可能真的不知道为什么需要Serializable

什么是serialVersionUID

serialVersionUID是Java原生序列化时候的一个关键属性,但是在不使用Java原生序列化的时候,这个属性是没有被用到的,比如基于hessian2协议实现的序列化方式中没有用到这个属性。

这里说的Java原生序列化是指使用下面的序列化方式和反序列化方式

java.io.ObjectOutputStream

java.io.ObjectInputStream

java.io.ObjectOutput

java.io.ObjectInput

java.io.Externalizable

在使用Java原生序列化的时候,serialVersionUID起到了一个类似版本号的作用,在反序列化的时候判断serialVersionUID如果不相同,会抛出InvalidClassException。

如果在使用原生序列化方式的时候官方是强烈建议指定一个serialVersionUID的,如果没有指定,在序列化过程中,jvm会自动计算出一个值作为serialVersionUID,由于这种运行时计算serialVersionUID的方式依赖于jvm的实现方式,如果序列化和反序列化的jvm实现方式不一样可能会导致抛出异常InvalidClassException,所以强烈建议指定serialVersionUID。

参考链接:mkyong.com/java-best-p…

生成serialVersionUID算法链接:docs.oracle.com/javase/6/do…

生成serialVersionUID

点击idea左上角File -> Settings -> Editor -> Inspections -> 搜索 Serialization issues ,找到 Serializable class without ‘serialVersionUID’ ->打上勾,再点击Apply->OK

很遗憾,你可能真的不知道为什么需要Serializable

只需要鼠标点击类名,点击 Add 'serialVersionUID' field 就可以一键生成serialVersionUID

很遗憾,你可能真的不知道为什么需要Serializable

总结

很遗憾,你可能真的不知道为什么需要Serializable

经过上面这么多的讲解、案例和对知识的思考,相信大家已经初步掌握了Serializable接口的使用方法和细节,  如果你觉得本文对你有一定的启发,引起了你的思考。  点赞、转发、收藏,下次你就能很快的找到我喽!