1.背景
在我们日常的开发中,前后端之间的接口联调很麻烦,经常出现后端加了字段,前端还不知道,所谓接口文档,经常和代码是不同步的。
好在现在有了grpc,它可以定义好ProtoBuf接口文件后,自动生成代码。原理就是RPC,在客户端生成一个远程服务的代理,可以像访问访问本地方法一样,访问远程方法。
但是,在客户端使用grpc的实践中,我们发现了grpc有几个弊端:
生成的实体类太大,很占内存,所以,我们最好只是用grpc的实体类来传输,但是在业务层,还是使用自己写的实体类更方便一点
自动生成的同步方法和异步方法,都不够好用,使用起来还是有一点点麻烦的
grpc生成的类都是final类,不便修改和继承
grpc生成的类没有实现Parcelable和Serializable接口,没法通过Intent传递,这在不同页面之间传递数据比较麻烦
为了解决grpc的弊端,我们最好在业务层自己封装一套实体类,grpc实体类只负责传输层,但是这又带来了一个新的问题:两套实体类之间互相转换的问题。
在我们的应用中,实体类的数量已经有接近800个,服务也有100多个。因为历史遗留问题,现在这些grpc的类还是广泛存在于业务层,迁移的话,非常麻烦。
经过一段时间的研究,我终于想到了一个方案:利用ProtoBuf接口文件,一键生成Java代码。
实现过程中,遇到了几个问题:
如何解析ProtoBuf文件?
因为我主要是玩Java的,所以还是想用Java代码来解析,但是在网上找了半天,好像都没有直接用Java解析ProtoBuf的方案。好在有一个替代方案,先将proto文件编译成desc文件,然后再来解析desc文件。
编译的话,就用Java调用一个cmd指令就好了
解析的话,就有不少坑了:
第1坑:List里的int需要额外处理
第2坑:map字段需要额外处理
第3坑:message嵌套message需要额外处理
第4坑:类名重复问题,因为我之前都是直接生成到一个包里面的,这就难免重复了,所以protobuf里面定义的package字段也要用上,
其它的小坑就略过不提了~~~
如何生成Java代码?
这肯定要用到模板引擎来做代码自动生成了,这里我使用的是FreeMaker。因为之前没玩过它,在写ftl模板文件时,经常运行失败。后来才知道这个坑:FreeMaker的数据模型必须有get方法,哪怕你的字段是public的也必须要有。
了解了FreeMaker的语法之后,再来写的话,就方便多了,后来而遇到了一个坑:Class里面嵌套Class的问题。这种内部类该怎么生成?这就需要用到FreeMaker的宏来做递归了。
2. 案例
ProtoBuf定义
文件名:user.proto,这里仅是一个示例,其实这个定义文件也不规范,更好的定义规范,还需要实践探索,和自己的项目结合
syntax = "proto3";
package crm.usercenter;
message UserMessage {
string name = 1;
int32 id = 2;
string message = 3;
MessageType type=4;
}
message FindUserMessageByIdReq {
int32 id = 1;
}
message Result {
string msg = 4;
int32 code = 5;
}
enum MessageType {
SYSTEM = 0;
CUSTOMER = 1;
OTHER = 2;
}
service UserPublic {
rpc FindUserMessageById (FindUserMessageByIdReq) returns (UserMessage);
rpc AddUserMessage (UserMessage) returns (Result);
}
生成的实体类
这里就只贴一个message了,其它类似
package dest.bean.crm.usercenter;
import dest.bean.crm.usercenter.MessageType;
public class UserMessage {
public String name;
public int id;
public String message;
public MessageType type;
}
生成的枚举类
package dest.bean.crm.usercenter;
public enum MessageType {
SYSTEM,
CUSTOMER,
OTHER,
}
生成的接口类
package dest.bean.crm.usercenter;
import dest.bean.crm.usercenter.FindUserMessageByIdReq;
import dest.bean.crm.usercenter.UserMessage;
import dest.bean.crm.usercenter.Result;
public interface UserPublic {
UserMessage findUserMessageById(FindUserMessageByIdReq findUserMessageByIdReq);
Result addUserMessage(UserMessage userMessage);
}
3. 异步封装
上面只是最基本的封装而已,其实用处不是很大,存在问题:
实体类自动生成,虽然减轻了一点工作量,不用自己再写那么多字段了,但是和Grpc生产的实体类之间的转换还是问题
生成的接口是同步的,但是我们在Android端用的话,肯定是要异步的接口的
下面,好戏开始上演了~~~
生成的异步接口
这里的异步接口,涉及到了我自己封装的网络层框架的几个接口了,
ResourceObserver是基于观察者模式实现的,类似于一个Rx的Disposable,用于取消网络请求,需要注册到ResourceSubject上。我们最好是将BaseActivity和BaseFragment都实现ResourceSubject接口,然后在退出Activity就可以自动取消网络请求了。
然后Consumer是我自己封装的函数式接口,之所以不用Rx和Java8的,主要是这种接口定义本来很简单,没必要过度依赖第三方,万一哪一天,我们不想用Rx了的话,用到这个接口的地方都得改,很蛋疼的。类似的我还封装了Mapper、Provider等几个函数式接口。
package dest.bean.crm.usercenter;
import com.ezbuy.functions.Consumer;
import com.ezbuy.web.ResourceObserver;
import dest.bean.crm.usercenter.FindUserMessageByIdReq;
import dest.bean.crm.usercenter.UserMessage;
import dest.bean.crm.usercenter.Result;
public interface UserPublicWebService {
ResourceObserver findUserMessageById(FindUserMessageByIdReq findUserMessageByIdReq, Consumer consumer);
ResourceObserver addUserMessage(UserMessage userMessage, Consumer consumer);
}
生成异步接口的实现类
这里