前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >很遗憾,你可能真的不知道为什么需要Serializable

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

作者头像
后端时光
发布2022-12-19 21:48:55
4300
发布2022-12-19 21:48:55
举报
文章被收录于专栏:后端时光后端时光

Serializable接口

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

代码语言:javascript
复制
@Data

public class PostCardInfoResp implements Serializable {

    private static final long serialVersionUID = 4106980050364098429L;

    

    /**

     * postId

     */

    private Long postId;

}

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

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

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

代码语言:javascript
复制
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接口

代码语言:javascript
复制
//返回值对象

@Data

public class UnSerializableResp {

    String msg;

}



//无序列化rpc方法

public UnSerializableResp serializableDemo1() {

    UnSerializableResp res = new UnSerializableResp();

    res.setMsg("demo1");

    return res;

}

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

代码语言:javascript
复制
//返回值对象

@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调用

代码语言:javascript
复制
//请求方法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

}

两个均请求成功

image-20220831174553845

通过http网关调用

请求方法一:

curl --location --request POST 'http://java.test.com/serializable_default'

image-20220831174629909

请求方法二:

curl --location --request POST 'http://java.test.com/serializable_demo2'

image-20220831174653317

两个均请求成功

java项目通过dubbo调用

代码语言:javascript
复制
<dependency>

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

</dependency>
代码语言:javascript
复制
/**

 * 测试

 */

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”

image-20220831174741031

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的反射调用非常像, 只需提供调用的方法名称, 参数的类型以及参数的值就可以直接调用对应方法了.

接口的实现如下:

代码语言:javascript
复制
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:完成了生产者端的泛化功能,本次重点关注这个

img

以下是项目调用流程图:

无法复制加载中的内容

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

消费者逻辑 http 项目 DubboClient.java
代码语言:javascript
复制
//标准的泛化调用

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
代码语言:javascript
复制
//Constants.GENERIC_KEY = "generic"

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

//true 或false

isJson = ProtocolUtils.isGenericSerialization(generic);

判断是否是序列化

image-20220831175414418

第二点:

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

继续查看 dubbo项目 genericfilter.java

dubbo-go返回值为字符串的秘密:
代码语言:javascript
复制
//请求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

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

代码语言:javascript
复制
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 方法

image-20220831175011141

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

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

对象序列化是什意思?

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

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

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

常见的RPC 架构图

img

Dubbo 序列化

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

常见的几种 dubbo 序列化协议

代码语言:javascript
复制
@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

代码语言:javascript
复制
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接口

代码语言:javascript
复制
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接口。否则同样会报错。如下图所示:

image-20220831175302056

什么是serialVersionUID

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

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

代码语言:javascript
复制
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。

参考链接:https://mkyong.com/java-best-practices/understand-the-serialversionuid/

生成serialVersionUID算法链接:https://docs.oracle.com/javase/6/docs/platform/serialization/spec/class.html#4100

生成serialVersionUID

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

img

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

img

总结

image-20220831175731210

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

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-09-01,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 后端时光 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Serializable接口
  • 去除Serializable接口会怎样
    • go项目通过dubbo-go调用
      • 通过http网关调用
        • java项目通过dubbo调用
        • dubbo-go和http平台为什么会成功?
          • 第一点:
            • dubbo的泛化调用
            • 消费者逻辑 http 项目 DubboClient.java
            • 生产者逻辑 dubbo项目 GenericFilter.java
          • 第二点:
          • 对象序列化是什意思?
          • 常见的RPC 架构图
          • Dubbo 序列化
          • Dubbo rpc 方法类都要实现Serializable接口的原因
          • 什么是serialVersionUID
          • 生成serialVersionUID
          • 总结
          相关产品与服务
          文件存储
          文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
          http://www.vxiaotou.com