我正在参与掘金创作者训练营第6期, 点击了解活动详情
-
🍬 微信公众号: 后端时光
-
🎨 所有文章第一时间都会在公众号发布,感兴趣的小伙伴快来关注吧~~
-
⏰ 本篇文章共计
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
}
两个均请求成功
通过http网关调用
请求方法一:
curl –location –request POST ‘java.test.com/serializabl…‘
请求方法二:
curl –location –request POST ‘java.test.com/serializabl…‘
两个均请求成功
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”
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:完成了生产者端的泛化功能,本次重点关注这个
以下是项目调用流程图:
无法复制加载中的内容
可以先分析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);
判断是否是序列化
第二点:
通过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 方法
返回了一个httpResponse对象,并设置了 content-type=application/json; charset=utf-8
所以http网关返回值是字符串
对象序列化是什意思?
- 普通的Java对象的生命周期是仅限于一个JVM中的,只要JVM停止,这个对象也就不存在了,下次JVM启动我们还想使用这个对象怎么办呢?
- 或者我们想要把一个对象传递给另外一个JVM的时候,应该怎么做呢?
这两个问题的答案就是将该对象进行序列化,然后保存在文件中或者进行网络传输到另一个JVM,由另外一个JVM反序列化成一个对象,然后供JVM使用。
序列化:Java
中的序列化机制能够将一个实例对象信息写入到一个字节流中,序列化后的对象可用于网络传输,或者持久化到数据库、磁盘中。
常见的RPC 架构图
Dubbo 序列化
Java dubbo 默认使用序列化的协议是 hessian2,也就是传输对象序列化,它是二进制的RPC协议
@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接口。否则同样会报错。如下图所示:
什么是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。
生成serialVersionUID算法链接:docs.oracle.com/javase/6/do…
生成serialVersionUID
点击idea左上角File -> Settings -> Editor -> Inspections -> 搜索 Serialization issues ,找到 Serializable class without ‘serialVersionUID’ ->打上勾,再点击Apply->OK
只需要鼠标点击类名,点击 Add ‘serialVersionUID‘ field 就可以一键生成serialVersionUID
总结
经过上面这么多的讲解、案例和对知识的思考,相信大家已经初步掌握了Serializable接口的使用方法和细节, 如果你觉得本文对你有一定的启发,引起了你的思考。 点赞、转发、收藏,下次你就能很快的找到我喽!
今天的文章很遗憾,你可能真的不知道为什么需要Serializable分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/22232.html