• gRPCJava实践

16.1 gRPC框架简介

最近一些年,随着网络的快速发展,一些软件项目越来越大、越来越复杂,也越来越变得难以维护。为了改变这样的困境,项目拆分成了一个选择。但不是没有代价的,原先作为一个单一项目,数据都在服务内部进行交互、处理,拆分为若干项目之后,数据间的交互成为一个问题。这个时候,RPC框架应运而生,解决了服务进程间的数据交互问题。

16.1.1 RPC简介及原理

RPC,远程过程调用协议(Remote Procedure Call Protocol),是为解决服务进程间数据交互而诞生的一种技术。客户端发起请求,服务端返回数据。它底层封装了网络交互的各种繁琐细节,使程序员可以专注于程序业务,快速实现进程间数据交互。

PRC 是怎样知道程序要交互的各个细节呢?这不得不提到现代编程语言的一种编程思想抽象!

RPC 内部,无论是客户端还是服务端,都有一个叫做stub存根的组件,它是代理进行网络调用的对象。stub之上,是程序间定义的交互规则——服务接口。客户端调用服务接口,进而调用stub及网络,然后网络传输到服务端,再通过网络、stub反向找到服务接口,找到服务端注册的服务接口的具体实现,响应调用请求,通过网络访问客户端,就完成了服务进程间的数据交互。

1.1 RPC 调用过程

从图中可以清楚看到,对于程序开发来说,在使用RPC过程中,服务接口最为重要,它定义了服务间的交互规则,是服务双方需要直接调用的组件。在服务端,服务接口具体实现真正实现了程序业务逻辑,进而实现了拆分程序的目标。而客户端,只需要调用服务接口解析访问数据即可。

一个定义良好规范的服务接口,对于RPC来说,非常关键和重要。

16.1.2 为何选择gRPC

gPRCgoogle开源实现的一个RPC框架,以支持多语言和适用移动场景应用为最大特点,自2015年项目发起以来受到广大关注。

目前,gRPC框架已经成熟,在互联网公司例如京东、腾讯等都开始有所应用。

1.2 gRPC 官方列出的使用项目

为什么选择gRPC框架呢,总的来说,有三点原因:1多语言支持;2基于HTTP2标准;3、可插拔的插件机制。

在一个规模较大的公司当中,通常开发语言都不只有一种。通过gRPC,只需定义一个服务接口,就可实行各种语言项目之间的交互,大大促进了各项目之间的融合,减少了内部设施的复杂度。到目前,gRPC已支持CC++C#Object-cJavaGoRuby…..等多种语言。

gRPC基于HTTP/2标准设计,相比 HTTP /1标准,压缩头部,节省数据传输,同时带来了诸如双向流、流控、单TCP连接上的多复用请求等特性。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。

另外,许多客户端通过 HTTP 代理访问网络,gRPC 采用 HTTP/2 实现,能透明转发 gRPC 的数据,无缝兼容 gRPC

Nginx1.13.10开始支持gRPC

gRPC 在设计之初,就注意到了后期扩展。通过提供可插拔的插件机制,可以实现负载平衡、跟踪、健康检查和身份验证等诸多高级功能。

16.1.3 使用 gRPC 的前置知识

对于使用RPC来说,服务接口的定义至关重要。

许多RPC框架,例如阿里Dubbo,都是使用RPC本身开发语言来定义服务接口,而gRPC,为了实现对多语言支持,采用了一种叫做Protobuf的服务中立语言,来定义服务接口。

Protobuf是一种序列化机制,也是一种中立接口。Protobuf自成一个体系,不与任何特定编程语言绑定。在这个体系中,它拥有对各种语言特性的描述。使用Protobuf定义接口后,只需要使用Protobuf对应各语言的编译器,反向生成编程语言,就能实现跨语言特性。

简单理解,Protobuf就是一门编程语言,而Protobuf特定语言编译器,就是编程语言的特定系统编译器。编程语言依靠对应的特定系统编译器实现了跨系统运行,同样,Protobuf也依靠特定语言编译器,实现了多语言支持。

因此,要使用gRPCprotobuf是必须要掌握的前置知识。

接下来,就让我们来了解 Protobuf吧!

16.2 了解 Protobuf

protobuf,全称 protocol buffersgoogle开发的一种序列化结构。

Protobuf是一种灵活、高效、自动化的序列化机制,与语言无关,平台无关,用于通信协议,数据存储等。

目前,Protobuf 已发展到第3版,又称为proto3,相对proto2添加了一些新功能,简化了protocol buffer语言,既易于使用,又可以在更广泛的编程语言中使用。

Protobuf自成体系,不与任何特定编程语言绑定,拥有对各种语言特性的描述。正是通过这些描述,实现对多语言支持。

要使用Protobuf,你需要先创建一个.proto文件,在其中定义protocol buffer消息类型,指定你希望构建的序列化信息。

下面是一个.proto文件的一个简单例子:

syntax = "proto3";

 

option java_package = "com.java.grpc.geral";

option java_outer_classname = "messages";

 

message Messages {

    int32 id = 1;

    int64 code = 2;

    string texts = 3;

}

代码16.1 定义一个protobuf

可以看到,我们使用protobuf定义了一条 Messages 消息。

我们使用了proto3。关于protobuf的版本信息,必须写在 .proto 文件第一条。

Protobuf 拥有自己的消息类型,例如int32int64string。也就是之前说的 Protobuf 对各种语言特性的描述。Protobuf 对应不同的语言都有相应的类型转换规则,通过 Protobuf 各语言的编译器生成编译语言时,自动完成消息类型转换。

下面是与Java语言对应的一些常用的protobuf消息类型:

.protobuf

java

int32

int

int64

long

bool

boolean

string

String

bytes

ByteString

我们看到,消息格式定义很简单——从右向左看,每个字段都有一个名称和一个消息类型;从左向右看,每种消息类型都有一个或多个唯一编号的字段,这些字段用于消息标识序列化位置。Protobuf是一种二进制序列化机制。你可以指定可选字段、必填字段和重复字段。

定义好了.proto文件,你就可以运行应用程序语言的protocol buffer编译器来生成程序语言数据访问类。它们为每个字段提供了简单的访问器(如name() set_name()),以及将整个结构序列化/解析为原始字节的方法。

很明显,在我们定义的.proto文件中,使用 protocol buffer java编译器最为合适。关于protocol buffer编译器的使用,我们可以查看protocol buffer 官网或者 gRPC 官网项目。我们在.proto文件中指定了java的一些选项,如果使用其他语言的protocol buffer编译器,则这些选项将被忽略。

生成程序语言数据访问类后,就可以在应用程序中使用此类来填充、序列化和检索Messages消息了。

我们可以写一些这样的代码:

…………

 

Messages message = Messages.newBuilder()

            .setId(1)

            .setCode(200)

            .setTexts("protobut")

            .build();

message.getId();

message.getCode();

message.getTexts();

…………

代码16.2 使用protobuf数据访问类填充、检索数据

正是基于Protobuf的灵活、高效和语言无关、平台无关特性,所以 gRPC 选择使用 Protobuf 来定义服务接口及通信协议,继而实现对多语言的支持。

接下来,我们就来开始使用 Protobuf 定义一个 gRPC 服务接口。

16.3 定义一个通用的请求、响应协议

在使用RPC的过程中,PRC服务接口的定义非常关键,定义一个通用、可扩展、适应各种场景的 RPC 服务接口,对于后期 PRC 的稳定十分重要。

gPRC 框架使用protobuf定义服务接口,服务接口使用protobuf消息协议发送响应数据。因此,在使用gRPC时,首先需要定义一个通用的请求(Request)、响应(Response)协议。

16.3.1 定义简单请求响应协议

一般情况下,查询特定数据、查询多条数据,是最为常见的网络请求。想象我们通过某个链接查看新闻或文章,还有在网上购物时出现的一列商品。

因此,在我们的简单请求协议中,对于id和分页(numberlimit)选项,必不可少。

在我们简单的请求中,有时会附带一些参数设置条件查询,因此,可以定义一个extras附录选项。

一般而言,都是在公司服务系统内部使用RPC框架,内部连接相对互联网公网连接少。因此可以充分发挥RPC效率高、性能好,支持网络长连接等特性。但是系统内部数据,是公司一种无形财产,需要做好保护,未经登记或许可的RPC服务访问都是无效,防止内部有人窃取数据。我们可以使用token令牌机制检查来访请求身份。

因此,综上所述,一个简单的gRPC请求协议定义如下:

syntax = "proto3";

 

option java_package = "com.java.grpc.geral";

option java_outer_classname = "Request";

message RPCRequest {

    int64 id = 1;

    int32 number = 2;

    int32 limit = 3;

    string extras = 4;

    string tokens = 5;

}

代码16.4 一个简单的protobuf请求协议

对应于服务端,在接收并处理了 gRPC 请求之后,需要响应返回数据到客户端。

在这种请求 - 响应模式中,响应状态码(code)很重要,它可以清晰快速地标识服务端状态,同时,还可以附上响应消息(mesg)告知响应情况。

在应对多条数据请求状况下,有时可能需要告知客户端数据总数(total)或分页(pages)情况,最后,还需要返回多条实体数据。关于服务端如何返回实体数据,或多条实体数据,我们稍后进行细致说明。

在这里,一个简单的gRPC响应协议定义如下:

syntax = "proto3";

 

option java_package = "com.java.grpc.geral";

option java_outer_classname = "Response";

 

message RPCResponse {

    int32 code = 1;

    int32 total = 2;

    int32 pages = 3;

    string mesg = 4;

}

代码16.5 一个简单的protobuf响应协议

最后,我们只需要定义一个 gRPC 服务接口,并引用定义的请求响应协议,就可以进行RPC交互了。

16.3.2 能适应特殊环境的请求响应协议

大多数情况,使用RPC都是为了拆分,但也有特殊情况下是合并的,当然,只是数据层面合并。

例如,认证授权、元数据等基础功能组件,这些基础功能组件数据量不是很大,但却是每一个管理或应用系统都不可或缺的部分。要在每个系统中都包含这些基础功能组件,即便使用了模块化开发或框架,但在数据层面,各基础数据分散,形成了信息孤岛,不利于数据统一管理。这个时候,就需要进行数据整合,将所有的这基础功能数据按照功能作用集中到一起,由一个服务统一进行处理,并通过RPC集中对外提供数据。

16.3 分散的基础数据形成了信息孤岛,不利于数据统一管理

16.4 数据整合后,通过PRC集中对外提供数据

经过数据整合之后,既加强了对数据的集中管理,同时又实现了对基础功能组件的统一管控,便于统一开发和维护基础功能组件,一举多得。

要注意的是,在经过数据整合后,原各系统基础数据依旧独立。数据整合只是将数据集中存储到一起,并不表示各系统使用一样数据。这时,无论是在数据表中还是在RPC请求中,都需要标识原系统模块(module),以得到正确数据。

一般情况下,基础数据都是不可变动,特别是对于权限菜单数据,这样的特殊数据都是在开发过程中与系统严格适配的,每个菜单都对应系统中的一个URL。有时候,出于特殊情况考虑,需要关闭安全检查,让每一个请求都能访问系统,同时又希望不破坏原有设计,可以快速恢复到安全保护模式。这种情况下,多是给数据设置一个状态(status),然后可以设置禁用或启用数据。

因此,在RPC请求协议中,也需要添加一个status标识。

作为一个可以面向移动端的RPC框架,在我们的请求协议中,也应该有能适应移动端场景的体现。

众所周知,移动端面向广大互联网人群,数据量庞大,数据更新快。如果以企业软件的思路,将每一条数据都呈现在移动端用户眼前,既不能发挥重大作用,还浪费移动端用户流量,同时让移动端用户感觉消息滞后。因为要拉取每一条数据,最新数据只能延迟拉取。在这样的情况下,我们应该算个思路,允许移动端用户跳跃拉取数据,拉取的数据只要不重复,不需要连续拉取数据。

对于移动端用户,特别是使用移动APP的用户,有两种形式拉取数据——上拉加载和下拉刷新。分别对应请求已有数据和请求最新数据。无论是上拉加载还是下拉刷新,它们都有一个拉取区间,就是不与已拉取数据重复,同时要能快速定位要拉伸的数据。对于这个区间,我们一般用minidmaxid表示,分别表示要拉取的结束位置和拉取的开始位置。通过移动端中已有的数据,我们可以确定这个区间。

同时,对于移动端,同一屏幕下通常要展现多种类型数据,以使程序丰富。这意味移动端同时要发送多个网络请求,返回多条数据。然后在移动端内部进行调整,一同呈现到移动端屏幕。这个时候,就需要对返回数据进行type类型标识,以便于移动端识别数据作用。

因此,改进后的 gRPC 请求响应协议如下所示:

syntax = "proto3";

 

option java_package = "com.java.grpc.geral";

option java_outer_classname = "Request";

 

message RPCRequest {

    int64 id = 1;

    int64 minid = 2;

    int64 maxid = 3;

    int32 number = 4;

    int32 limit = 5;

    string extras = 6;

    string status = 7;

    string module = 8;

    string tokens = 9;

}

 

 

syntax = "proto3";

 

option java_package = "com.java.grpc.geral";

option java_outer_classname = "Response";

 

message RPCResponse {

    int32 code = 1;

    int32 type = 2;

    int32 total = 3;

    int32 pages = 4;

    string mesg = 5;

}

代码16.6 能适应特殊环境的请求响应协议

16.3.3 能适应自定义数据模型的协议

经过上面2个演进,我们的请求响应协议已经基本定型,可以应对各种环境下的请求响应。

不过,我们通过RPC进行交互,不是仅仅只有请求数据,还需要提交(实体)数据,以进行存储或处理。同时,在 16.3.1 简单请求响应协议中,也提到了需要返回(实体)数据。

这个时候的我们,对于这种情况下的请求响应协议,会有两种选择。

一,根据要提交或返回的实体数据模型,直接将创建RPC实体数据模型作为请求响应协议。

这样的请求响应协议,是最常见和最易想到的。请求内容或响应内容就在模型定义中,一目了然,方便开发和调试。gRPC官网示例就是使用这样的请求响应协议。

但是,它存在诸多缺陷。

首先就是无法兼容我们已经规划好的协议规范,例如token令牌、idextras,这些标识是在向服务端提交数据时的一个身份信息,服务端要检查这些数据加强对于系统保护。在客户端,无法使用一个统一的响应解析器,因为可能每次返回的响应模型不一样。

其次,它也存在服务接口繁多和不稳定。每一个接口都有自己唯一的请求响应实体模型,没法最大程度的复用;每一次实体数据模型的更改与调整,都要对相关的服务接口定义和服务端接口实现、客户端调用做出改变。

很显然,这样的请求响应协议,无法适应我们的要求。

二,通过提供一个类似Java范型的包装器,包装我们创建的RPC实体数据模型,以这样的方式作为请求响应协议。

正好,在protobuf中,就有这样的包装器——Any

Any 可以包装单个gRPC实体数据模型,也可以包装多条gRPC实体数据模型。我们可以在请求extras或返回mesg消息中,标识Any包装的数据结构,例如single表示单个gRPC实体数据模型,listString表示多条String消息,listAny * 表示多条gRPC实体数据模型等等,方便开发和调试。

以这样的方式定义请求响应协议,不仅结构稳定,而且协议一致,通过在请求或响应中标注额外的提示信息,也可以做到一目了然。

所以,要定义一个标准、稳定的请求响应协议,我们使用第二种方式定义协议。

最后,做一点补充。让我们创建一个完整的请求响应协议!

有时在我们的请求过程中,我们需要传递一些不是很多的请求参数,这些请求参数又没有与某些gRPC实体数据类型对应,这个时候,对于不超过5个的参数请求,通常不建议为它们创建新的gRPC实体数据类型,而是使用类似Java Map这样的包装器传递参数,,简单方便。

还有,作为RPC服务端,是提供接口给客户端进行交互。有很多客户端程序,针对同一服务接口,有的客户端可能进行了升级提供更好的服务,有的客户端则没有。针对这样的情况,服务端可以返回一个版本号vers进行标识。

最终,我们的gRPC请求响应协议定义如下:

syntax = "proto3";

 

option java_package = "com.java.grpc.geral";

option java_outer_classname = "Request";

import "google/protobuf/any.proto";

 

message RPCRequest {

    int64 id = 1;

    int64 minid = 2;

    int64 maxid = 3;

    int32 number = 4;

    int32 limit = 5;

    string extras = 6;

    string status = 7;

    string module = 8;

    string tokens = 9;

    map<string, string> maps = 10;    

    google.protobuf.Any data = 11;

}

 

syntax = "proto3";

option java_package = "com.java.grpc.geral";

option java_outer_classname = "Response";

import "google/protobuf/any.proto";

 

message RPCResponse {

    int32 code = 1;

    int32 type = 2;

    int32 total = 3;

    int32 pages = 4;

    string mesg = 5;

    string vers = 6;

    google.protobuf.Any data = 7;

}

代码16.7 能适应自定义数据模型的请求响应协议

为了能够返回多条数据,我们又定义了这样两个gRPC包装器:

syntax = "proto3";

 

option java_package = "com.java.grpc.geral";

option java_outer_classname = "ListString";

 

message RPCListString {

    repeated string datas = 1;

}

 

 

syntax = "proto3";

 

option java_package = "com.java.grpc.geral ";

option java_outer_classname = "ListAny";

import "google/protobuf/any.proto";

 

message RPCListAny {

    repeated google.protobuf.Any datas = 1;

}

代码16.8 能包装多条数据的gRPC实体模型

16. 4 gRPC 发送与解析数据

我们已经定义好了RPC请求响应协议,只需定义服务接口,就可以实现RPC交互了。

现在,让我们来定义一个RPC服务接口!

syntax = "proto3";

 

option java_package = "com.java.grpc.geral.service";

option java_outer_classname = "EchoService";

import "RPCRequest.proto";

import "RPCResponse.proto";

 

service RPCServiceEcho {

    rpc echo(RPCRequest) returns (RPCResponse);

}

代码16.9 定义一个RPC服务接口

可以看到,我们的 RPCSerivceEcho 服务接口很简单,只定义了一个 echo 服务方法,使用 RPCRequest 发送请求,RPCResponse 返回响应。

使用protocol buffer java编译器生成java语言 RPCSerivceEchoGrpc 程序类。

16.5 编译器生成的RPCSerivceEchoGrpc 程序类

要实现 RPC 交互,我们需要注意RPCSerivceEchoGrpc 程序类中这么几个组件:内部类 RPCServiceEchoImplBase、方法 newStub(Channel)newBlockingStub(Channel)、和newFutureStub(Channel),这些方法分别调用其对应的内部类。

对于服务端而言,只需注意内部类RPCServiceEchoImplBase即可,它是实现服务方法具体逻辑和绑定服务方法的地方,其中绑定服务方法已经自动生成,开发人员只需要实现具体的业务逻辑即可。

让我们在服务端实现 gRPC 接口,在客户端调用 gRPC 接口吧。

接下来,就让我们一步一步,从简单到复杂,使用gRPC发送和解析数据!

16.4.1 发送解析简单数据

让我们继承RPCServiceEchoImplBase,实现一个简单的具体逻辑。

public class EchoService extends RPCServiceEchoGrpc.RPCServiceEchoImplBase {

 

    @Override

    public void echo(RPCRequest request,

            io.grpc.stub.StreamObserver<RPCResponse> responseObserver) {

        RPCResponse response = RPCResponse.newBuilder()

                .setCode(1)

                .setMesg("ok")

                .setData(Any.pack(request))

                .build();

        responseObserver.onNext(response);

        responseObserver.onCompleted();

    }

 

}

代码16.10 服务端实现 gPRC 服务接口方法

如果大家熟悉RxJava,可以一眼看出这是什么。

我们创建了一个响应对象 response,为它设置了响应数据 request,另外,还给它设置了响应码1,响应消息 ok 和响应版本 1.0;最后将 response 返回给客户端,并设置处理完毕。

很好,我们注册这个服务接口实现,这样gRPC服务端就可以向外提供服务了。

public class GRPCServer {

 

    public static void main(String[] args) throws Exception {

        Server server = ServerBuilder.forPort(8022)

                .addService(new EchoService()).build();

        System.out.println( "Starting GRPC Server ..." );

        server.start();

        System.out.println( "GRPC Server started!" );

        server.awaitTermination();

    }

}

代码16.11 服务端注册 gPRC 服务接口方法实现

接着,让我们在gRPC客户端调用服务接口,并解析返回数据。

我们知道在 RPCSerivceEchoGrpc 程序类中有三个存根方法,newStub(Channel) 发起异步请求、newBlockingStub(Channel) 发起同步请求、和newFutureStub(Channel) 发起监听回调式请求。

在这里,我们使用newBlockingStub(Channel) 发起同步请求。

public class GRPCClient {

 

    public static RPCServiceEchoBlockingStub connect() {

        ManagedChannel channel = ManagedChannelBuilder

                .forAddress("localhost", 8022).usePlaintext().build();

        return RPCServiceEchoGrpc.newBlockingStub(channel);

    }

 

    public static void main(String[] args) throws Exception {

        RPCServiceEchoBlockingStub stub = connect();

        RPCRequest request = RPCRequest.newBuilder()

                .setId(1L)

                .setExtras("gRPC test!")

                .setTokens("108203-1210122-2AASDA")

                .build();

        RPCResponse response = stub.echo(request);

        System.out.println(response.getCode());

        System.out.println(response.getMesg());

        Any any = (Any) response.getData();

        RPCRequest result = any.unpack(RPCRequest.class);

        System.out.println("result: id=" + result.getId()

                + " extras=" + result.getExtras()

                + " tokens=" + result.getTokens());

    }

}

代码16.12 客户端定义 gPRC 服务接口方法

运行服务端和客户端,看看结果:

1

ok

result: id=1 extras=gRPC test! tokens=108203-1210122-2AASDA

结果16.1 gPRC 服务简单请求响应结果

很棒,我们顺利实现了RPC数据交互!

在这里,除了Channel通道,其余都是我们已经了解过的概念。ChannelgRPC中的两端之间的虚拟连接,用于执行RPC服务。我们可以通过通道自由地与服务端建立零个或多个实际连接。通道也可以自由确定要使用的实际服务端,并且可以在每个RPC中更改它,从而允许客户端负载平衡。在应用程序通常使用stub调用通道。

16.4.2 发送解析多参数数据

对于这样的多参数数据,我们在上一篇定义请求响应协议中就聊过,使用Java Map类型封装多参数即可。

让我们来看一看示例即可。

public class GRPCClient {

 

    …………

 

    public static void main(String[] args) throws Exception {

        RPCServiceEchoBlockingStub stub = connect();

        RPCRequest request = RPCRequest.newBuilder()

                .setId(1L)

                .setExtras("gRPC test!")

                .setTokens("108203-1210122-2AASDA")

                .putMaps("firstName", "")

                .putMaps("middName", "")

                .putMaps("lastName", "")

                .build();

        RPCResponse response = stub.echo(request);

        System.out.println(response.getCode());

        System.out.println(response.getMesg());

        Any any = (Any) response.getData();

        RPCRequest result = any.unpack(RPCRequest.class);

        System.out.println("result: id=" + result.getId()

                + " extras=" + result.getExtras()

                + " tokens=" + result.getTokens());

        System.out.println("maps: "

                + " firstName=" + result.getMapsOrDefault("firstName", "")

                + " middName=" + result.getMapsOrDefault("middName", "")

                + " lastName=" + result.getMapsOrDefault("lastName", ""));

    }

}

代码16.13 gPRC 服务多参数请求响应结果

1

ok

result: id=1 extras=gRPC test! tokens=108203-1210122-2AASDA

maps:  firstName=middName=lastName=

结果16.2 gPRC 服务自定义数据模型请求响应结果

关于多参数Java Map类型的封装,gRPC 提供了多种不同的方式,有兴趣可以了解探索一下,这里就不深入了。

16.4.3 发送解析自定义数据模型

RPC多参数数据一样,自定义gRPC数据类型也是使用了一个包装器——Any

让我们再接再厉,使用最早定义的一个 Messagse gRPC 数据实体模型实现一个发送解析自定义gRPC数据模型的RPC交互。

public class GRPCClient {

 

    .........

 

    public static void main(String[] args) throws Exception {

        RPCServiceEchoBlockingStub stub = connect();

        Messages ms = Messages.newBuilder()

                .setTexts("Hello, gRPC!").build();

        RPCRequest request = RPCRequest.newBuilder()

                .setId(1L)

                .setExtras("gRPC test!")

                .setTokens("108203-1210122-2AASDA")

                .setData(Any.pack(ms))

                .build();

        RPCResponse response = stub.echo(request);

        System.out.println(response.getCode());

        System.out.println(response.getMesg());

        Any any = (Any) response.getData();

        RPCRequest result = any.unpack(RPCRequest.class);

        System.out.println("result: id=" + result.getId()

                + " extras=" + result.getExtras()

                + " tokens=" + result.getTokens());

        Any data = (Any) result.getData();

        Messages mg = data.unpack(Messages.class);

        System.out.println("mg: " + mg.getTexts());

    }

}

代码16.14 gPRC 服务自定义数据模型请求

1

ok

result: id=1 extras=gRPC test! tokens=108203-1210122-2AASDA

mg: Hello, gRPC!

结果16.3 gPRC 自定义请求响应结果

到这里,你应该看出事情有点复杂了。

我们使用gRPC数据模型装载我们的请求数据。然后,在解析返回的响应数据时,首先需要解包RPCResponse中的Any包装数据,这里是RPCRequest。然后,再从解包出的RPCRequest中,继续解包Any包装器中的数据——gRPC 数据实体Messages。经过这一复制繁琐操作,我们才解包到了RPC返回来的数据。要用到Java程序中,我们还需要再次装载到Java实体模型。

这里,我只装载了一条简单文本,如果数据内容来自Java数据实体,来自多条Java数据实体。服务端封包,客户端解包;或者客户端封包,然后发起请求,服务端解包处理数据,再封包处理结果,返回客户端,客户端再解包…… OhMy god,这么复杂的过程,我想你马上就会对gRPC弃之不用了,调个gRPC服务太折腾了。

尽管gRPC支持流式操作,就是一次操作中可以连续批量的请求或返回单条数据,但这也丝毫减少不了它的复杂度,毕竟gRPC的所有操作都是要在gRPC实体数据模型间进行的,而不是编程语言的实体数据模型,而程序业务逻辑的所有操作是在编程语言的实体数据模型进行,而不是在gRPC实体数据模型中进行。

强烈不支持直接将 gRPC 实体数据模型嵌入到程序实际编程业务中,这样完全侵入程序系统,不利于程序的灵活和扩展。

万一哪天你觉得gRPC不好了呢!

16.5 封装常用 gRPC 数据工具

由于gRPC的所有操作都是在gRPC实体数据模型间进行的,而程序业务逻辑的所有操作是在编程语言的实体数据模型进行。两种模型之间数据的转移导致了 gRPC 使用上的一些复杂和繁琐性。为了简化 gRPC 的操作,我们需要封装一些 gRPC 数据工具。

16.5.1 定义一些常用常量

gPRC 响应协议中,有一个codemesg标识,分别表示服务端接收到请求之后的响应状态和响应消息。

注意,这里是服务端已经成功接收到了客户端请求,服务端已经在处理逻辑了。如果gRPC服务本身而不是程序业务逻辑出现异常,gRPC会有自己的传递异常的方式。

这个时候,返回的响应状态应该永远表示服务端接收到了请求,我们用常量数字1表示。

接下来,就是处理逻辑的结果了,也就是返回的消息。这又分为几种情况:

1、逻辑处理成功。这样的情况,我们无话可说,直接使用常量字符串 ok 表示。

2、数据处理失败。例如,未能成功存储数据,数据类型转换错误等等,这个时候,我们会提示说处理失败

3、未能查询到数据。

4、已经存在该数据。

当然,还有更多的其他成功或错误,这时候,就需要自行设置mesg消息提示了。

我们目前只总结了这4种常见的处理结果。

public class RPCDataStants {

 

    /**服务端接收请求 1*/

    public static final int REQUEST_SUCCESS_CODE = 1;

 

    /**服务端处理成功 ok */

    public static final String HANDLER_DATA_OK = "ok";

 

    /**服务端处理数据失败 */

    public static final String HANDLER_DATA_FAIL = "The data handler fail";

 

    /**服务端未查询到数据 */

    public static final String HANDLER_DATA_UNFOUND = "No related data found";

 

    /**服务端已经存在该数据 */

    public static final String HANDLER_DATA_EXISTS = " Related data already exists";

 

}

代码16.15 一些RPC常用常量

16.5.2 日期类型转换工具

无论哪种编程语言,只要涉及到日期类型的处理,都是比较繁琐和复杂的。

日期,本质上是一个表示长度的数字。只是,现实生活中,太阳的东升西落,一年四季的春夏秋冬,有着一定的重复规律,而为了表示这样的规律,就有了年月日这样的计时法。表现到编程语言中,也有了按照一定数字长度划分年月日的算法。同时,由于时间的定位都是以当前所在区域太阳在正午时候的位置为标准,因此,在同一时间,不同地区的当地时间又不一样,就有了时区的概念。这样一来,日期算法的复杂性,可想而知。

不同的编程语言,有不同的日期形式,不同的日期算法结构。因此,当一种语言的日期要想以另一种语言的形式呈现时,就必须要做日期转换。

gRPC也一样。

protobuf中,预先定制了一个日期类型——Timestamp

如果Java 版的Date日期要通过gRPC传递,或者接收来自gRPC的日期,就必须要做日期类型转换。

下面是JavagRPC 之间的日期类型转换工具。

public class RPCDateUtil {

 

    /**

     * Date 转换到 probuf时间类型

     */

    public static Timestamp dateToTimestamp(Date date) {

        return Timestamp.newBuilder().setSeconds(date.getTime()).build();

    }

 

    /**

     * probuf时间类型转换到 Date

     */

    public static Date timestampToDate(Timestamp timestamp) {

        return new Date(timestamp.getSeconds());

    }

 

}

代码16.16 日期类型转换工具

16.5.3 常用发送解析数据处理器

本着绝不多写一条重复代码的原则,我们决定对gRPC发送解析数据进行封装和处理。

首先是gRPC数据协议的封装,请求协议和响应协议。

在第16.3我们已经看过 gRPC 数据类型的封装,首先找到builder构建器,然后设置数据,最后builder() 构建完成。我们初看觉得这不是很复杂,当你多次这样构建设置数据之后,你会被这些样板代码折磨。我们的重点在于提供数据,至于怎么装载数据,这是装载器的事情。

public class RPCSimpleRequest {

 

    public static RPCRequest idRequest(Long id, String extras, String tokens) {

        return RPCRequest.newBuilder().setId(id).setExtras(extras) .setTokens(tokens).build();

    }

 

    public static RPCRequest mapRequest(Map<String, String> values, String tokens) {

        return RPCRequest.newBuilder().setTokens(tokens).putAllMaps(values).build();

    }

 

    public static RPCRequest mapRequest(String param, String value, String tokens) {

        return RPCRequest.newBuilder().setTokens(tokens).putMaps(param, value).build();

    }

 

.........

 

}

代码16.17 gRPC请求协议的简单封装

public class RPCDataResult {

 

    private static RPCResponse.Builder builder() {

        return RPCResponse.newBuilder();

    }

 

    /**

     * 服务端请求处理成功

     */

    public static RPCResponse succes() {

        return builder().setCode(RPCDataStants.REQUEST_SUCCESS_CODE)

                .setMesg(RPCDataStants.HANDLER_DATA_OK).build();

    }

 

    /**

     * 服务端请求处理成功

     */

    public static RPCResponse succes(int total) {

        return builder().setCode(RPCDataStants.REQUEST_SUCCESS_CODE)

                .setTotal(total).setMesg(RPCDataStants.HANDLER_DATA_OK)

                .build();

    }

 

    /**

     * 服务端请求处理成功

     */

    public static RPCResponse succes(Any any) {

        return succes("1.0", any);

    }

 

    /**

     * 服务端请求处理成功

     */

    public static RPCResponse succes(String vers, Any any) {

        return builder().setCode(RPCDataStants.REQUEST_SUCCESS_CODE)

            .setMesg(RPCDataStants.HANDLER_DATA_OK).setVers(vers)

            .setData(any).build();

    }

 

    /**

     * 服务端请求处理成功

     */

    public static RPCResponse succes(int type, Any any) {

        return succes(type, "1.0", any);

    }

 

    /**

     * 服务端请求处理成功

     */

    public static RPCResponse succes(int type, String vers, Any any) {

        return builder().setCode(RPCDataStants.REQUEST_SUCCESS_CODE).setType(type)

            .setMesg(RPCDataStants.HANDLER_DATA_OK).setVers(vers)

            .setData(any).build();

    }

 

    /**

     * 服务端请求处理成功

     */

    public static RPCResponse succes(int type, int total, int pages, Any any) {

        return succes(type, total, pages, "1.0", any);

    }

 

    /**

     * 服务端请求处理成功

     */

    public static RPCResponse succes(int type, int total, int pages, String vers, Any any) {

        return builder().setCode(RPCDataStants.REQUEST_SUCCESS_CODE).setType(type)

        .setTotal(total).setPages(pages).setMesg(RPCDataStants.HANDLER_DATA_OK)

            .setVers(vers).setData(any).build();

    }

 

 

    /**

     * 服务端请求处理失败

     */

    public static RPCResponse failed() {

        return builder().setCode(RPCDataStants.REQUEST_SUCCESS_CODE)

            .setMesg(RPCDataStants.HANDLER_DATA_FAIL).build();

    }

 

    /**

     * 服务端请求处理失败

     */

    public static RPCResponse failed(String msg) {

        return builder().setCode(RPCDataStants.REQUEST_SUCCESS_CODE)

            .setMesg(msg).build();

    }

 

    /**

     * 服务端请求处理失败

     */

    public static RPCResponse failed(int type, String msg) {

        return builder().setCode(RPCDataStants.REQUEST_SUCCESS_CODE)

            .setType(type).setMesg(msg).build();

    }

 

    /**

     * 服务端请求处理失败

     */

    public static RPCResponse failed(int type, String msg, String vers) {

        return builder().setCode(RPCDataStants.REQUEST_SUCCESS_CODE)

            .setType(type).setMesg(msg)

            .setVers(vers).build();

    }

 

}

代码16.18 gRPC响应协议的简单封装

这里只是做了一个简单的封装,更多或更复杂封装大家可以自行探索。

接下来,是对 gRPC 发送响应和解析响应的封装和处理。

gRPC 发送响应的封装也是非常简单。

public class RPCSimpleResponse {

 

    .........

 

    /**

     * 发送 RPC 处理响应

     */

    public static void sendResponse(StreamObserver<RPCResponse> responseObserver, RPCResponse response) {

        responseObserver.onNext(response);

        responseObserver.onCompleted();

    }

 

}

代码16.19 gRPC发送响应的简单封装

至于响应结果的解析,首先,我们要检查协议状态。构建状态,我们再做后续决定。

在这里,有几种情况的结果解析。首先,我们查看mesg的返回消息,服务端逻辑是否处理成功。没处理成功,那我们要查看具体的消息提示。处理成功,我们又需要分几种情况。

一、查看返回的结果数量。有可能是处理结果数量,也有可能是去重检查时的数据数量。这两种情况,只需判断返回结果是否大于0即可。如果本身就是为了请求多条数据,这个结果数量不必十分在意。

二、解析真正的返回的gRPC数据实体类型。这是最复杂也最实质的一步。前面的解析都只是各种状况检查,这一步是真正的解析数据。这里,又分多种情况,返回的只是一个简单gPRC数据实体;返回的 gPRC数据实体包含 gPRC数据实体;返回的gPRC数据实体包含多条gPRC数据实体……

因此,如果我们不准备一个解析器,每次发起 gRPC 请求后自行解析 gRPC 响应,那个工作量,我敢保证你只要看一下就不想使用 gRPC 了。我就是简单调用一个 gRPC 方法,怎么这么复杂?头都大了!

好了,不说了,让我们看一下我们的gRPC响应结果解析器!

public class RPCDataParser {

    public static final String SERVER_NO_ACCEPT

        = "RPC server no accept request!";

 

    /**

     * RPC 响应代码解析

     */

    public static boolean codeResult(RPCResponse response) {

        return response.getCode() == RPCDataStants.REQUEST_SUCCESS_CODE;

    }

 

    /**

     * RPC 结果判断解析

     */

    public static boolean exitsResult(RPCResponse response) {

        return executeResult(response);

    }

 

    /**

     * RPC 结果执行解析

     */

    public static boolean executeResult(RPCResponse response) {

        boolean isExecute = false;

        if(simpleResult(response)) {

            if(response.getTotal() > 0) {

                isExecute = true;

            }

        }

        return isExecute;

    }

    /**

     * RPC 结果简单解析

     */

    public static boolean simpleResult(RPCResponse response) {

        return handlerResult(response).equals(RPCDataStants.HANDLER_DATA_OK);

    }

 

    /**

     * RPC 结果处理解析

     */

    public static String handlerResult(RPCResponse response) {

        if(codeResult(response)) {

            return response.getMesg();

        } else {

            return SERVER_NO_ACCEPT;

        }

    }

 

    /**

     * RPC 结果简单解析

     */

    public static <T extends com.google.protobuf.Message> T

        probufParser(RPCResponse response, Class<T> clazz) {

        if(response.getData() != null) {

            try {

                Any any = (Any) response.getData();

                return (T) any.unpack(clazz);

            } catch (InvalidProtocolBufferException e) {

                e.printStackTrace();

            }

        }

        return null;

    }

 

    /**

     * RPC 结果简单解析

     */

    public static <T extends com.google.protobuf.Message> T

        resultParser(RPCResponse response, Class<T> clazz) {

        if (simpleResult(response)) {

            return probufParser(response, clazz);

        }

        return null;

    }

}

代码16.20 gRPC响应结果解析器

最后,让我们用这些工具做一个RPC简单交互吧!

public class EchoService extends RPCServiceEchoGrpc.RPCServiceEchoImplBase {

 

    @Override

    public void echo(RPCRequest request,

            io.grpc.stub.StreamObserver<RPCResponse> responseObserver) {

        RPCResponse response = RPCDataResult.succes(Any.pack(request));

        RPCSimpleResponse.sendResponse(responseObserver, response);

    }

 

}

 

public class GRPCClient {

 

    public static void main(String[] args) throws Exception {

        RPCServiceEchoBlockingStub stub = connect();

        RPCRequest request = RPCSimpleRequest

                .idRequest(1L, "gRPC test!", "108203-1210122-2AASDA");

        RPCRequest result = RPCDataParser.resultParser(

                stub.echo(request), RPCRequest.class);

        System.out.println("result: id=" + result.getId()

                + " extras=" + result.getExtras()

                + " tokens=" + result.getTokens());

}

代码16.21 使用了数据处理工具的gRPC 服务端与客户端

运行gRPC服务端和客户端,看看结果:

result: id=1 extras=gRPC test! tokens=108203-1210122-2AASDA

结果16.4 gRPC 服务端与客户端交互结果

很好,我们的代码变得简单了,同时,还能得到正确的 gRPC 响应结果。

接下来,让我们继续,看一看对多条 gRPC 数据实体的处理。

16.6 gRPC Java 集合类数据处理

前面的章节,我们做了一些gRPC的简单交互。

在我们的程序中,是经常需要处理多条数据的,例如分页查询。我们一直没有涉及。

现在,就让我们做好准备,来处理有关多条 gRPC 数据实体之间的响应。

16.6.1 发送 Java 集合类

我们准备一组Java自定义实体数据,将这组数据转移到gRPC 数据实体,再由 gRPC 服务端发送到 gRPC 客户端。

public class Advert {

    private Long id;

    private String text;

 

    public Advert() {}

 

    public Advert(String text) {

        this.text = text;

    }

 

    /*************** set/get 方法省略 **********************/

 

    public static List<Advert> datas(int count) {

        List<Advert> list = null;

        if(count > 0) {

            list = new ArrayList<>(count);

            for(int i=0; i<count; i++) {

                list.add(new Advert("Advert - " + i));

            }

        }

        return list;

    }

}

代码16.22 服务端准备发送的Java集合数据

public class EchoService extends RPCServiceEchoGrpc.RPCServiceEchoImplBase {

 

    public RPCListAny conver() {

        List<Advert> datas = Advert.datas(5);

        int count = datas.size();

        if(count > 0) {

            List<Any> anys = new ArrayList<>(count);

            for(int i=0; i<count; i++) {

                Advert a = datas.get(i);

                Any any = Any.pack(

                        Messages.newBuilder()

                            .setTexts(a.getText())

                            .build());

                anys.add(any);

            }

            return RPCListAny.newBuilder().addAllDatas(anys).build();

        }

        return null;

    }

 

    @Override

    public void echo(RPCRequest request,

            io.grpc.stub.StreamObserver<RPCResponse> responseObserver) {

        RPCResponse response = RPCDataResult.succes(Any.pack(conver()));

        RPCSimpleResponse.sendResponse(responseObserver, response);

    }

 

}

代码16.23 gRPC 服务端发送一组数据

这是我们之前封装了一些数据工具之后,发送一组简单的数据实体的代码。

我们可以看到,样板代码太多了,真正核心的业务逻辑没有多少。有人可能会说,可以使用java lambda表达式。是的,lambda确实可以减少这些样板代码,但是lambda的意图不显而易见,而且调试不很方便。对于一次性的数据处理,支持使用lambda,但是对于规模化和可重复使用工具,不十分赞成。

让我们来看看客户端怎么接收解析这么一组数据吧。

16.6.2 处理 Java 集合类响应

与服务端发送数据的过程刚好相反,客户端先解析到一组 gRPC 数据实体,再将这组数据转移到一组Java自定义数据实体。可以想到,样板代码又是很多。

public class GRPCClient {

 

    .........

 

    public static void main(String[] args) throws Exception {

        RPCServiceEchoBlockingStub stub = connect();

        RPCRequest request = RPCSimpleRequest.idRequest(1L, "", "");

        RPCListAny listAny = RPCDataParser.resultParser(

                stub.echo(request), RPCListAny.class);

        int count = listAny.getDatasCount();

        if(count > 0) {

            List<Advert> datas = new ArrayList<Advert>();

            for(int i=0; i<count; i++) {

                Any any = listAny.getDatas(i);

                Messages ms = any.unpack(Messages.class);

                System.out.println("result ms: " + ms.getTexts());

                Advert a = new Advert(ms.getTexts());

                datas.add(a);

            }

            System.out.println("advert count: " + datas.size());

        }

    }

}

代码16.24 gRPC 客户端解析一组数据

result ms: Advert - 0

result ms: Advert - 1

result ms: Advert - 2

result ms: Advert - 3

result ms: Advert - 4

advert count: 5

结果16.5 gRPC 客户端数据解析结果

果然如我们所料,样板代码依旧很多。当我们在客户端的一个方法内部使用 gRPC 时,这么大段代码肯定会淹没我们的业务逻辑,让我们在繁琐的样板中折腾。哦,这到底谁是重点啊,我只是想简单通过 gRPC 拿到远端数据。

我们必须简化这些样板。

16.6.3 使用回调处理 Java 集合类

经过观察,我们发现,这些所谓的样板代码,就是有关多条数据的组装和拆解,所以,我们首先要想办法封装这些样板代码。

可是,当我们正准备这样动手时,我们突然发现一个问题。

多条数据的组装和拆解容易,过程都很清晰,可是实体数据类型不确定啊!如果我们以A类型为标准封装工具,那么B类型肯定没法使用,如果我们用范型,那么还是对我们的数据类型提出了一定的要求,至少要符合范型标准。

那么怎么办呢?

这时,经常在异步程序中使用的一种模式——回调,就派上用场了。

所谓回调,就是程序在此处预留的一个钩子,是对逻辑的一个抽象,它在程序运行到此处时会去寻找真正的业务实现,进而实现动态处理。因为在异步环境下,程序的响应时机不确定,而回调恰恰是应对这些不确定的最好工具。

因此,在这里,我们使用回调来应对这种不确定的数据模型。

我们对于多条数据的组装和拆解封装如下:

/**

 * gRPC 类型数据包装、解包器

 */

public class RPCUnInPackUtil {

 

    /**

     * Probuf 数据转换回调接口

     */

    public interface RPCDataConver<T> {

 

        public T dataConvert(int index);

 

    }

 

    /**

     * 将普通对象列表包装成Probuf类型列表

     */

    public static <T extends com.google.protobuf.Message> List<Any>

        packAnyList(List<?> list, RPCDataConver<T> dataConver) {

        List<Any> datas = null;

        if(list != null && dataConver != null) {

            int size = list.size();

            datas = new ArrayList<>(size);

            for(int i=0; i<size; i++) {

                T data = dataConver.dataConvert(i);

                datas.add(Any.pack(data));

            }

            list.clear();

        }

        return datas;

    }

 

    .........

 

}

 

public class RPCSimpleResponse {

 

    /**

     * 设置 RPC 处理结果

     */

    public static <T extends com.google.protobuf.Message>

        RPCListAny setAnys(List<?> datas, RPCDataConver<T> dataConver) {

        if (datas != null) {

            List<Any> anys = RPCUnInPackUtil.packAnyList(datas, dataConver);

            return RPCListAny.newBuilder().addAllDatas(anys).build();

        }

        return null;

    }

    }

代码16.25 将多条Java数据封装到gRPC 数据

使用我们的封装工具发送多条数据:

public class EchoService extends RPCServiceEchoGrpc.RPCServiceEchoImplBase {

    @Override

    public void echo(RPCRequest request,

            io.grpc.stub.StreamObserver responseObserver) {

        List datas = Advert.datas(5);

        RPCListAny listAny = RPCSimpleResponse.setAnys(

            datas, new RPCDataConver() {

 

                @Override

                public Messages dataConvert(int index) {

                    Advert a = datas.get(index);

                    return Messages.newBuilder()

                            .setTexts(a.getText())

                            .build();

                }

            }

        );

        RPCResponse response = RPCDataResult.succes(Any.pack(listAny));

        RPCSimpleResponse.sendResponse(responseObserver, response);

    }

}

代码16.26 改进后的gRPC 服务端发送多条数据

result ms: Advert - 0

result ms: Advert - 1

result ms: Advert - 2

result ms: Advert - 3

result ms: Advert - 4

advert count: 5

结果16.6 gRPC 客户端数据解析结果

运行正确!

这里我们就不对客户端数据拆解进行封装了。

我们发现,经过封装之后,我们的样板代码没了,只剩下真正的数据转移了。

好像挺简单了!

但是,再深思熟虑,发现它还是很啰里啰嗦。

我们这里的数据模型非常简单,只有一个字段。如果是多字段模型,而且多字段中某些字段是否有值还不错定呢,那个工作量和样板代码,又是一个令人十分恼火的东西。

我们还能不能再继续深入,彻底的让它实现自动化的数据转移,而不需要我们关心这些过程,只专注于处理业务逻辑呢?

要知道答案,请看下节内容

16.7 使用反射简化 gRPC 数据装包与解包

经过这一段gRPC的旅程,我们已经非常了解 gRPC Java 程序融合时所产生的匹配问题。我们之前一直在致力于解决这一问题,虽然有了一些成绩,但不是十分满意。

在上一最后,我们提出这样一个想法:彻底实现自动化的数据转移。我们不想再关心这些过程,只专注于处理业务逻辑!

我们知道,在程序运行中,各种细节内幕是不了解的,要想达成自动化,首先就要有能够探测运行时细节内幕的机制。还别说,Java 拥有这种机制 —— 反射!

16.7.1 不得再一次重复 Java 反射特性

反射,又称自省,是指在程序运行中检查和更改自身行为和结构的一种能力。

Java 程序在运行时,首先会通过类装载器装载 class 文件,然后读取 class 文件信息,执行程序。反射,就是通过获取这些 class 文件信息,动态地改变程序执行行为和结构,进而实现动态化处理。

16.6 Java 反射结构图解

反射的基础是class文件,让我们了解一下class

表面上,class是生成Java的二进制文件,而在反射那里,class是记录Java元信息的对象。这个对象记录了Java的名称、字段、方法等各种基础信息,通过这些信息,可以获得Java执行时的运行结构,实现按需修改或配置。

16.7 不同视角下的class文件

凡是需要动态化或自动化的过程,都需要依靠反射。

设计模式工厂模式依靠反射动态实例化对象,Spring框架最核心的模式就是工厂模式,BeanFactorySpring依靠反射完成注解配置,依靠反射实现动态代理,实现AOPMyBatis依靠反射自动生成Dao实现……

反射的历史源远流长,已经在各编程语言中得到实现。

就下来,我们就来具体熟悉Java反射,看看它的庐山真面目!

16.7.2 使用反射提取 Java 特征

首先,我们来通过反射提取Java对象字段。

public class RPCReflect {

 

    public static void reflectField(Class<?> clazz) {

        Field[] fields = clazz.getDeclaredFields();

        for(Field f : fields) {

            System.out.println("field name: " + f.getName());

            System.out.println("field type: " + f.getType().getSimpleName());

        }

    }

 

    public static void main(String[] args) {

        reflectField((new Advert()).getClass());

    }

}

代码16.27 使用反射获取对象字段

field name: id

field type: Long

field name: text

field type: String

结果16.7 反射获取对象字段结果

我们看到,我们不仅可以获得字段名称,还可以获取字段类型,甚至还有对象值,字段修饰符,设置强制访问字段,等等。

接着,我们来看看通过反射获取有关方法的操作:

public class RPCReflect {

 

    .........

 

    public static void reflectMethod(Object object) throws Exception {

        Method[] methods = object.getClass().getDeclaredMethods();

        for(Method m : methods) {

            if(m.getParameterCount() == 1) {

                Class<?> clazz = m.getParameterTypes()[0];

                if(clazz.getSimpleName().equals("String")) {

                    m.invoke(object, "哈哈哈!");

                }

            }

            System.out.println("method name: " + m.getName());

        }

    }

    public static void main(String[] args) throws Exception {

        Advert advert = new Advert();

        reflectMethod(advert);

        System.out.println("method result: " + advert.getText());

    }

}

代码16.28 使用反射操作对象方法

method name: getId

method name: getText

method name: setId

method name: setText

method result: 哈哈哈!

结果16.8 反射操作对象方法结果

我们获取了对象方法,并通过反射调用了方法。

我们已经隐约感觉到,我们可以动态化操作Java对象了。

是的,没错,通过反射实例化对象,调用对象方法,我们就可以实现自动化了。

这里是简单展示了一下 Java 反射,更多更深更强大的反射功能,大家感兴趣的可以自行探索。

让我们回归正题,使用反射简化 gRPC 数据处理。

16.7.3 使用反射简化 gRPC 数据处理

我们在gRPCJava工程融合过程中,遇到的主要问题是数据转移。就是从A对象向gRPC A对象转移数据时,因为是两个对象,所以需要繁琐的手动转移。

我们观察这两个对象,发现其实它们两个对象字段是有很多是一样的,尽管 gRPC 对象还有其他对象,那么问题就好办了。

这些数据模型的字段都是私有属性,都有相应的set\get方法,我们从这些set\get方法出发,通过比较这两个字段的set\get方法,然后再通过反射调用方法动态转移值。

好的,我们知道了怎么处理,下面就来实现吧!

首先,我们获取这两个对象字段的set\get方法。

public class BeanPerties {

 

    .........

 

    /**

     * 获取对象getter函数

     */

    public static Map<String, Class<?>> obtainReadableProperties(Object object) {

        Method[] methods = object.getClass().getMethods();

        Map<String, Class<?>> map = new HashMap<>();

        for (Method m : methods) {

            if(m.getName().startsWith("get")) {

                String key = m.getName().substring(3);

                Class<?> value = m.getReturnType();

                map.put(key, value);

            }

        }

        return map;

    }

 

    /**

     * 获取对象setter函数

     */

    public static Map<String, Class<?>> obtainWriteableProperties(Object object) {

        Method[] methods = object.getClass().getMethods();

        Map<String, Class<?>> map = new HashMap<>();

        for (Method m : methods) {

            if(m.getName().startsWith("set")) {

                if(m.getParameterCount() == 1) {

                    String key = m.getName().substring(3);

                    Class<?> value = m.getParameterTypes()[0];

                    map.put(key, value);

                }

            }

        }

        return map;

    }

}

代码16.28 获取对象字段的set\get方法

然后,我们比较这两个对象字段的set\get方法,当字段方法相等时,我们从源对象获取值,然后转移到目标对象。

我们准备一些获取字段值和设置字段值的工具。

public class BeanPerties {

 

    /**

     * 获取对象特定的getter函数值

     */

    public static Object getReadableValue(Object object, String name)

        throws IllegalArgumentException, SecurityException,

            IllegalAccessException, InvocationTargetException,

            NoSuchMethodException {

        String methodName = "get" + name.substring(0, 1)

            .toUpperCase(Locale.ENGLISH) + name.substring(1);

        return object.getClass().getMethod(methodName).invoke(object);

    }

 

    /**

     * 获取对象特定的setter函数

     */

    public static Method getWriteableMethod(Object object, String name, Class<?> parameterTypes)

        throws NoSuchMethodException, SecurityException {

        String methodName = "set" + name.substring(0, 1)

            .toUpperCase(Locale.ENGLISH) + name.substring(1);

        return object.getClass().getMethod(methodName, parameterTypes);

    }

    .........

}

代码16.29 获取\设置对象字段值工具

然后,我们比较字段方法。

public class RPCBeanUtil {

 

    .........

 

    /**

     *(浅)拷贝probuf属性到另一对象

     * @param target 目标对象

     * @param origin 源值对象

     */

    protected static void copyProperties(final Object target, final Object origin) {

 

    .........

 

        Map<String, Class<?>> targetMap = BeanPerties.obtainWriteableProperties(target);

        Map<String, Class<?>> originMap = BeanPerties.obtainReadableProperties(origin);

        for (Map.Entry<String, Class<?>> entry : targetMap.entrySet()) {

            String targetType = entry.getValue().getSimpleName();

            String name = entry.getKey();

            if(originMap.containsKey(name)) {

                try {

                    Object value = BeanPerties.getReadableValue(origin, name);

                    if(value != null) {

                        Method m = BeanPerties.getWriteableMethod(target, name, entry.getValue());

                        String originType = originMap.get(name).getSimpleName();

 

                            .........

 

                    }

                } catch (Exception e) {

                    e.printStackTrace();

                }

            }

        }

    }

}

代码16.30 比较对象字段方法

这里,要提示一下,在Java数据模型中,由于Spring MVCMyBatis的规范,我们一般字段类型全部都是使用包装对象,也就是说,对于数值类型,我们使用LongInteger,而不是使用longint;而恰恰相反,在gRPC对象模型中,对于数值类型,使用longint,而不是使用LongInteger

16.8 gRPC对象模型的数值类型

因此,在这里,我们要做一个类型转换,如果类型不一致,则不能成功调用对象方法。

public class RPCBeanUtil {

    .........

    /**

     * class 对象属性类型转换

     */

    protected static String propertiesClassCovert(String className) {

        String cover = "String";

        switch (className) {

            case "int":

                cover = "Integer";

                break;

            case "Integer":

                cover = "int";

                break;

            case "long":

                cover = "Long";

                break;

            case "Long":

                cover = "long";

                break;

            case "boolean":

                cover = "Boolean";

                break;

            case "Boolean":

                cover = "boolean";

                break;

            }

            return cover;

    }

}

代码16.31 对象字段数值类型的类型转换

之后,我们需要考虑到日期类型的转换。

关于日期类型的转换,我们已经在第5章中说过,这里就不再多说了。

我们的Java对象与gRPC对象数据自动转移方法如下:

public class RPCBeanUtil {

 

    .........

 

    /**

     *(浅)拷贝probuf属性到另一对象

     * @param target 目标对象

     * @param origin 源值对象

     */

    protected static void copyProperties(final Object target, final Object origin) {

        if (target == null) {

            throw new IllegalArgumentException("No target object bean specified");

        }

        if (origin == null) {

            throw new IllegalArgumentException("No origin Protobuf bean specified");

        }

        Map<String, Class<?>> targetMap = BeanPerties.obtainWriteableProperties(target);

        Map<String, Class<?>> originMap = BeanPerties.obtainReadableProperties(origin);

        for (Map.Entry<String, Class<?>> entry : targetMap.entrySet()) {

            String targetType = entry.getValue().getSimpleName();

            String name = entry.getKey();

            if(originMap.containsKey(name)) {

                try {

                    Object value = BeanPerties.getReadableValue(origin, name);

                    if(value != null) {

                        Method m = BeanPerties.getWriteableMethod(target, name, entry.getValue());

                        String originType = originMap.get(name).getSimpleName();

                        if(targetType.equals(propertiesClassCovert(originType))) {

                            if(targetType.equals("String")) {

                                if (!value.toString().trim().equals("")) {

                                    m.invoke(target, value);

                                }

                            } else {

                                m.invoke(target, value);

                            }

                        }

                        if(targetType.equals("Date")) {

                            Timestamp timestamp = (Timestamp) value;

                            Date date = new Date(timestamp.getSeconds());

                            m.invoke(target, date);

                        }

                        if(originType.equals("Date")) {

                            Date date = (Date) value;

                            Timestamp.Builder builder = Timestamp.newBuilder().setSeconds(date.getTime());

                            if(targetType.equals("Timestamp")) {

                                m.invoke(target, builder.build());

                            }

                            if(targetType.equals("Builder")) {

                                m.invoke(target, builder);

                            }

                        }

                    }

                } catch (Exception e) {

                    e.printStackTrace();

                }

            }

        }

    }

 

    .........

 

}

代码16.32 Java对象与gRPC对象数据自动转移

我们再做一些防御式的处理。因为这个方法是为特定环境下的目的定制的。

public class RPCBeanUtil {

 

    /**

     *(浅)拷贝对象属性到protobuf

     * @param pbfClazz pbf类定义

     * @param object 源值对象

     * @return protobuf pbf对象

     */

    @SuppressWarnings("unchecked")

    public static <T extends com.google.protobuf.Message, V> T

        toProtobuf(Class<T> pbfClazz, V object) {

        T protobuf = null;

        try {

            Object builder = pbfClazz.getDeclaredMethod("newBuilder").invoke(null);

            copyProperties(builder, object);

            protobuf = (T) builder.getClass().getDeclaredMethod("build").invoke(builder);

        } catch (Exception e) {

            e.printStackTrace();

        }

        return protobuf;

    }

 

    /**

     *(浅)拷贝protobuf属性到另一对象

     * @param target 目标对象

     * @param probuf 源值对象

     * @return target 目标对象

     */

    public static <T, V extends com.google.protobuf.Message> T

        copyProtobuf(T target, V protobuf) {

        if(protobuf != null) {

            copyProperties(target, protobuf);

        } else {

            target = null;

        }

        return target;

    }

 

    .........

 

}

代码16.33 对数据自动转移做防御性处理

最后,我们封装一些客户端自动解包数据。

public class RPCUnInPackUtil {

 

    .........

 

    /**

     * Probuf类型数据解包成普通对象数据

     */

    public static <T, V extends com.google.protobuf.Message> List<T>

        unpackAnyList(RPCListAny list, Class<T> target, Class<V> clazz) {

        List<T> datas = null;

        if(list != null) {

            int size = list.getDatasCount();

            datas = new ArrayList<>(size);

            try {

                for(int i=0; i<size; i++) {

                    Any any = list.getDatas(i);

                    V value = (V) any.unpack(clazz);

                    T t = BeanValues.newInstance(target);

                    RPCBeanUtil.copyProtobuf(t, value);

                    datas.add(t);

                }

            } catch (InstantiationException e) {

                e.printStackTrace();

            } catch (IllegalAccessException e) {

                e.printStackTrace();

            } catch (InvalidProtocolBufferException e) {

                e.printStackTrace();

            }

        }

        return datas;

    }

 

    /**

     * RPC类型数据解包成普通String数据

     */

    public static List<String> unpackStringList(RPCListString list) {

        List<String> datas = null;

        if(list != null) {

            int size = list.getDatasCount();

            datas = new ArrayList<>(size);

            for(int i=0; i<size; i++) {

                datas.add(list.getDatas(i));

            }    

        }

        return datas;

    }

 

}

代码16.34 客户端自动解包工具

好的,大功告成!

还记得上一章我们数据转移时所用的回调方法吗。我们现在可以删除回调方法,使用这个自动转移方法了。

最后,让我们来运行一下,看看效果如何:

public class EchoService extends RPCServiceEchoGrpc.RPCServiceEchoImplBase {

 

    @Override

    public void echo(RPCRequest request,

            io.grpc.stub.StreamObserver<RPCResponse> responseObserver) {

        List<Advert> datas = Advert.datas(5);

        RPCListAny listAny = RPCSimpleResponse.setAnys(datas, Messages.class);

        RPCResponse response = RPCDataResult.succes(Any.pack(listAny));

        RPCSimpleResponse.sendResponse(responseObserver, response);

    }

 

}

 

public class GRPCClient {

 

    .........

 

    public static void main(String[] args) throws Exception {

        RPCServiceEchoBlockingStub stub = connect();

        RPCRequest request = RPCSimpleRequest.idRequest(1L, "", "");

        RPCListAny listAny = RPCDataParser.resultParser(

                stub.echo(request), RPCListAny.class);

        List<Advert> datas = RPCUnInPackUtil

                .unpackAnyList(listAny, Advert.class, Messages.class);

            System.out.println("advert count: " + datas.size());

    }

 

}

代码16.36 运行测试数据自动转移工具

advert count: 5

结果16.8 测试自动转移工具结果

好的,非常完美,我们实现了自动化目标!


0 条 查看最新 评论

没有评论
暂时无法发表评论