在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,可以先去掉试下看看有什么影响
通过提供以下两个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;
}
//请求方法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
请求方法一:
curl --location --request POST 'http://java.test.com/serializable_default'
image-20220831174629909
请求方法二:
curl --location --request POST 'http://java.test.com/serializable_demo2'
image-20220831174653317
两个均请求成功
<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”
image-20220831174741031
细节观察他们之间有所差异和相同
dubbo-go和http平台都不是通过mvn引入jar包去调用的rpc接口(也不可能通过mvn引入jar包),java项目引入的方式是mvn。原来dubbo为了解决,不同编程语言之间调用rpc或消费者不依赖生产者jar包问题,提供了泛化调用能力。
泛接口调用方式主要用于客户端没有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 泛化调用和泛化实现依赖于下面两个过滤器 来完成。如下图:
img
以下是项目调用流程图:
无法复制加载中的内容
可以先分析http网关实现逻辑, 熟悉了http网关,基本上也就了解了dubbo-go的原理
//标准的泛化调用
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());
//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
//请求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 项目 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 方法
image-20220831175011141
返回了一个httpResponse对象,并设置了 content-type=application/json; charset=utf-8
所以http网关返回值是字符串
这两个问题的答案就是将该对象进行序列化,然后保存在文件中或者进行网络传输到另一个JVM,由另外一个JVM反序列化成一个对象,然后供JVM使用。
序列化:Java
中的序列化机制能够将一个实例对象信息写入到一个字节流中,序列化后的对象可用于网络传输,或者持久化到数据库、磁盘中。
img
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在使用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接口。否则同样会报错。如下图所示:
image-20220831175302056
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。
参考链接:https://mkyong.com/java-best-practices/understand-the-serialversionuid/
生成serialVersionUID算法链接:https://docs.oracle.com/javase/6/docs/platform/serialization/spec/class.html#4100
点击idea左上角File -> Settings -> Editor -> Inspections -> 搜索 Serialization issues ,找到 Serializable class without ‘serialVersionUID’ ->打上勾,再点击Apply->OK
img
只需要鼠标点击类名,点击 Add 'serialVersionUID' field 就可以一键生成serialVersionUID
img
image-20220831175731210
经过上面这么多的讲解、案例和对知识的思考,相信大家已经初步掌握了Serializable接口的使用方法和细节, 如果你觉得本文对你有一定的启发,引起了你的思考。 点赞、转发、收藏,下次你就能很快的找到我喽!