このページは?
gRPCを勉強している際にoption java_generic_services = true
が非推奨になっているが理由が不明だった。 そこで、option java_generic_services
がtrue
とfalse
(default)の時で、スタブにどのような違いが出てくるかについて調査した時の記録を紹介するものである
調査記録を紹介するにあたって、以下の流れで進めていく
- grpcの利用する流れを紹介
- ↑の具体的なサンプルを紹介
- 具体的なサンプルの
java_generic_services
をtrueにしてみてスタブにどのような違いが出るかを見てみる
grpcとは?
- Google が開発した RPC ( Remote Procedure Call )システム
- Protocol Buffers と呼ばれるインタフェース記述言語(IDL)でインタフェースを定義する
- バイナリファイルでデータの送受信がされるためRESTなどで用いられるjsonに比べて軽量である
- 様々な言語に変換可能である
gRPCのgはgoogleのgではなく、versionによって表すものが変わる
引用:https://github.com/grpc/grpc/blob/master/doc/g_stands_for.md
grpcを利用する流れ
-
.proto
ファイルにデータの定義を行う - protocを用いて特定のプログラミング言語に応じてスタブを作成する
- 作成したスタブを用いて、指定した開発言語のオブジェクトを生成する
- サーバープログラムを作成する
- クライアントプログラムを作成する
- ↑のプログラムを実行する
grpcの利用する流れを実際に体験してみる
今回は、option java_generic_services = true;
を説明するのが目的のため、クライアントプログラムは、grpcで代用する
なので、作成するファイルは以下の3つになっている
- EchoService.proto
- EchoServer.java
- pom.xml
また、ディレクトリ構成は以下のようになる。
.
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── example
│ └── grpc
│ └── server
│ └── EchoServer.java
└── proto
└── EchoService.proto
.proto
ファイルにデータの定義を行う
syntax = "proto3";
package com.example.grpc;
option java_multiple_files = true;
message EchoRequest {
string message = 1;
}
message EchoResponse {
string message = 1;
string from = 2;
}
service EchoService {
rpc echo(EchoRequest) returns (EchoResponse);
}
protocを用いて特定のプログラミング言語に応じてスタブを作成する
Mavenでスタブを作成する。
generate-sources フェーズで protoc
が実行される
$ mvn package
ちなみに pom.xml
は以下のように記述している
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>echo-server</artifactId>
<version>1.0-SNAPSHOT</version>
<groupId>com.example.grpc</groupId>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<grpc.version>1.64.0</grpc.version>
<protobuf.version>3.25.3</protobuf.version>
</properties>
<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>${protobuf.version}</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>echo-server</finalName>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.0</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
<!-- .protoからJavaに変換 -->
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.25.3:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.64.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 実行可能なJARを作成 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.example.grpc.server.EchoServer</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
<transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
<resource>META-INF/io.netty.versions.properties</resource>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
作成したスタブを用いて、指定した開発言語のオブジェクトを生成する
サーバープログラム
package com.example.grpc.server;
import com.example.grpc.EchoRequest;
import com.example.grpc.EchoResponse;
import com.example.grpc.EchoServiceGrpc;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
public class EchoServer {
static public void main(String[] args) throws IOException, InterruptedException {
Server server = ServerBuilder.forPort(8080)
.addService(new EchoServiceImpl()).build();
System.out.println("Starting server...");
server.start();
System.out.println("Server started!");
server.awaitTermination();
}
}
// protoから作成したスタブを利用してサーバープログラムを実装
class EchoServiceImpl extends EchoServiceGrpc.EchoServiceImplBase {
@Override
public void echo(EchoRequest request, StreamObserver<EchoResponse> responseObserver) {
try {
String from = InetAddress.getLocalHost().getHostAddress();
System.out.println("Received: " + request.getMessage());
responseObserver.onNext(EchoResponse.newBuilder()
.setFrom(from)
.setMessage(request.getMessage())
.build());
responseObserver.onCompleted();
} catch (UnknownHostException e) {
responseObserver.onError(e);
}
}
}
クライアントプログラム
(再掲)今回は、grpcurl
で代用するので省略
↑のプログラムを実行する
$ java -jar target/echo-server.jar &
$ grpcurl --plaintext \
-d '{"message": "hello world!"}' \
-proto src/main/proto/EchoService.proto \
localhost:8080 com.example.grpc.EchoService.echo
{
"message": "hello world!",
"from": "127.0.0.1"
}
とりあえずプログラムは実行できたので、protoにoption java_generic_services = true;
を追加して、スタブにどのような変化があるかをみていく
protoにoption java_generic_services = true;
を追加してみる
まずは、option java_generic_services = true;
なしの状態でスタブを作成する
そして、差分を見やすくするためにgitで管理する
$ mvn clean package
$ git init
$ git add -A
$ git commit -m "first commit"
option java_generic_services = true;
をつける
syntax = "proto3";
package com.example.grpc;
option java_multiple_files = true;
+ option java_generic_services = true;
message EchoRequest {
string message = 1;
}
message EchoResponse {
string message = 1;
string from = 2;
}
service EchoService {
rpc echo(EchoRequest) returns (EchoResponse);
}
$ mvn clean package
$ git add -A
$ git commit -m "modify proto"
$ git status target/generated-sources/protobuf
On branch master
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: target/generated-sources/protobuf/java/com/example/grpc/EchoService.java
modified: target/generated-sources/protobuf/java/com/example/grpc/EchoServiceOuterClass.java
target/generated-sources/protobuf
の配下を見てみると、
- EchoService.javaが新規作成されて
- EchoServiceOuterClass.javaが変更されていた
EchoService.javaを見てみる
EchoService.java全文
// Generated by the protocol buffer compiler. DO NOT EDIT!
// source: EchoService.proto
// Protobuf Java Version: 3.25.3
package com.example.grpc;
/**
* Protobuf service {@code com.example.grpc.EchoService}
*/
public abstract class EchoService
implements com.google.protobuf.Service {
protected EchoService() {}
public interface Interface {
/**
* <code>rpc echo(.com.example.grpc.EchoRequest) returns (.com.example.grpc.EchoResponse);</code>
*/
public abstract void echo(
com.google.protobuf.RpcController controller,
com.example.grpc.EchoRequest request,
com.google.protobuf.RpcCallback<com.example.grpc.EchoResponse> done);
}
public static com.google.protobuf.Service newReflectiveService(
final Interface impl) {
return new EchoService() {
@java.lang.Override
public void echo(
com.google.protobuf.RpcController controller,
com.example.grpc.EchoRequest request,
com.google.protobuf.RpcCallback<com.example.grpc.EchoResponse> done) {
impl.echo(controller, request, done);
}
};
}
public static com.google.protobuf.BlockingService
newReflectiveBlockingService(final BlockingInterface impl) {
return new com.google.protobuf.BlockingService() {
public final com.google.protobuf.Descriptors.ServiceDescriptor
getDescriptorForType() {
return getDescriptor();
}
public final com.google.protobuf.Message callBlockingMethod(
com.google.protobuf.Descriptors.MethodDescriptor method,
com.google.protobuf.RpcController controller,
com.google.protobuf.Message request)
throws com.google.protobuf.ServiceException {
if (method.getService() != getDescriptor()) {
throw new java.lang.IllegalArgumentException(
"Service.callBlockingMethod() given method descriptor for " +
"wrong service type.");
}
switch(method.getIndex()) {
case 0:
return impl.echo(controller, (com.example.grpc.EchoRequest)request);
default:
throw new java.lang.AssertionError("Can't get here.");
}
}
public final com.google.protobuf.Message
getRequestPrototype(
com.google.protobuf.Descriptors.MethodDescriptor method) {
if (method.getService() != getDescriptor()) {
throw new java.lang.IllegalArgumentException(
"Service.getRequestPrototype() given method " +
"descriptor for wrong service type.");
}
switch(method.getIndex()) {
case 0:
return com.example.grpc.EchoRequest.getDefaultInstance();
default:
throw new java.lang.AssertionError("Can't get here.");
}
}
public final com.google.protobuf.Message
getResponsePrototype(
com.google.protobuf.Descriptors.MethodDescriptor method) {
if (method.getService() != getDescriptor()) {
throw new java.lang.IllegalArgumentException(
"Service.getResponsePrototype() given method " +
"descriptor for wrong service type.");
}
switch(method.getIndex()) {
case 0:
return com.example.grpc.EchoResponse.getDefaultInstance();
default:
throw new java.lang.AssertionError("Can't get here.");
}
}
};
}
/**
* <code>rpc echo(.com.example.grpc.EchoRequest) returns (.com.example.grpc.EchoResponse);</code>
*/
public abstract void echo(
com.google.protobuf.RpcController controller,
com.example.grpc.EchoRequest request,
com.google.protobuf.RpcCallback<com.example.grpc.EchoResponse> done);
public static final
com.google.protobuf.Descriptors.ServiceDescriptor
getDescriptor() {
return com.example.grpc.EchoServiceOuterClass.getDescriptor().getServices().get(0);
}
public final com.google.protobuf.Descriptors.ServiceDescriptor
getDescriptorForType() {
return getDescriptor();
}
public final void callMethod(
com.google.protobuf.Descriptors.MethodDescriptor method,
com.google.protobuf.RpcController controller,
com.google.protobuf.Message request,
com.google.protobuf.RpcCallback<
com.google.protobuf.Message> done) {
if (method.getService() != getDescriptor()) {
throw new java.lang.IllegalArgumentException(
"Service.callMethod() given method descriptor for wrong " +
"service type.");
}
switch(method.getIndex()) {
case 0:
this.echo(controller, (com.example.grpc.EchoRequest)request,
com.google.protobuf.RpcUtil.<com.example.grpc.EchoResponse>specializeCallback(
done));
return;
default:
throw new java.lang.AssertionError("Can't get here.");
}
}
public final com.google.protobuf.Message
getRequestPrototype(
com.google.protobuf.Descriptors.MethodDescriptor method) {
if (method.getService() != getDescriptor()) {
throw new java.lang.IllegalArgumentException(
"Service.getRequestPrototype() given method " +
"descriptor for wrong service type.");
}
switch(method.getIndex()) {
case 0:
return com.example.grpc.EchoRequest.getDefaultInstance();
default:
throw new java.lang.AssertionError("Can't get here.");
}
}
public final com.google.protobuf.Message
getResponsePrototype(
com.google.protobuf.Descriptors.MethodDescriptor method) {
if (method.getService() != getDescriptor()) {
throw new java.lang.IllegalArgumentException(
"Service.getResponsePrototype() given method " +
"descriptor for wrong service type.");
}
switch(method.getIndex()) {
case 0:
return com.example.grpc.EchoResponse.getDefaultInstance();
default:
throw new java.lang.AssertionError("Can't get here.");
}
}
public static Stub newStub(
com.google.protobuf.RpcChannel channel) {
return new Stub(channel);
}
public static final class Stub extends com.example.grpc.EchoService implements Interface {
private Stub(com.google.protobuf.RpcChannel channel) {
this.channel = channel;
}
private final com.google.protobuf.RpcChannel channel;
public com.google.protobuf.RpcChannel getChannel() {
return channel;
}
public void echo(
com.google.protobuf.RpcController controller,
com.example.grpc.EchoRequest request,
com.google.protobuf.RpcCallback<com.example.grpc.EchoResponse> done) {
channel.callMethod(
getDescriptor().getMethods().get(0),
controller,
request,
com.example.grpc.EchoResponse.getDefaultInstance(),
com.google.protobuf.RpcUtil.generalizeCallback(
done,
com.example.grpc.EchoResponse.class,
com.example.grpc.EchoResponse.getDefaultInstance()));
}
}
public static BlockingInterface newBlockingStub(
com.google.protobuf.BlockingRpcChannel channel) {
return new BlockingStub(channel);
}
public interface BlockingInterface {
public com.example.grpc.EchoResponse echo(
com.google.protobuf.RpcController controller,
com.example.grpc.EchoRequest request)
throws com.google.protobuf.ServiceException;
}
private static final class BlockingStub implements BlockingInterface {
private BlockingStub(com.google.protobuf.BlockingRpcChannel channel) {
this.channel = channel;
}
private final com.google.protobuf.BlockingRpcChannel channel;
public com.example.grpc.EchoResponse echo(
com.google.protobuf.RpcController controller,
com.example.grpc.EchoRequest request)
throws com.google.protobuf.ServiceException {
return (com.example.grpc.EchoResponse) channel.callBlockingMethod(
getDescriptor().getMethods().get(0),
controller,
request,
com.example.grpc.EchoResponse.getDefaultInstance());
}
}
// @@protoc_insertion_point(class_scope:com.example.grpc.EchoService)
}
Starting with version 2.3.0, RPC implementations should not try to build on this, but should instead provide code generator plugins which generate code specific to the particular RPC implementation.
com.google.protobuf Interface Service
RPC 2.3.0以降はこのinterfaceを実装するのではなく、言語に応じてスタブを生成するプラグインを利用するのが推奨になった。
つまり、このファイル(EchoServiece)は、RPC 2.2.xまでで利用されていた形式。
詳細は割愛するが、この汎用APIの実装方式の場合はgRPCと比較して以下のような違いがある
観点 | 旧方式(汎用RPC) | 新方式(gRPCで主に利用) |
---|---|---|
コネクション管理・再試行 | 利用者が RpcChannel などで実装 |
ManagedChannel や CallOptions に組み込まれており、再試行やタイムアウトも設定可能 |
ロードバランシング & 名前解決 | 外部ロードバランサや独自実装に依存 |
PickFirst や RoundRobin 、xDS による動的構成が gRPC 標準でサポートされている |
複数の言語での通信方法 | 各言語ごとに RpcChannel 相当の実装が必要 |
.proto から各言語に対応したスタブを自動生成(公式サポート:Java、Go、Python、C++ など) |
EchoServiceOuterClass.javaを見てみる
差分は以下のような感じ
$ git diff HEAD target/generated-sources/protobuf/java/com/example/grpc/EchoServiceOuterClass.java
diff --git a/target/generated-sources/protobuf/java/com/example/grpc/EchoServiceOuterClass.java b/target/generated-sources/protobuf/java/com/example/grpc/EchoServiceOuterClass.java
index 7d88860..94be40b 100644
--- a/target/generated-sources/protobuf/java/com/example/grpc/EchoServiceOuterClass.java
+++ b/target/generated-sources/protobuf/java/com/example/grpc/EchoServiceOuterClass.java
@@ -39,7 +39,7 @@ public final class EchoServiceOuterClass {
"ponse\022\017\n\007message\030\001 \001(\t\022\014\n\004from\030\002 \001(\t2T\n\013" +
"EchoService\022E\n\004echo\022\035.com.example.grpc.E" +
"choRequest\032\036.com.example.grpc.EchoRespon" +
- "seB\002P\001b\006proto3"
+ "seB\005P\001\210\001\001b\006proto3"
};
descriptor = com.google.protobuf.Descriptors.FileDescriptor
.internalBuildGeneratedFileFrom(descriptorData,
変更後のファイルは以下のようになっている
private static com.google.protobuf.Descriptors.FileDescriptor
descriptor;
static {
java.lang.String[] descriptorData = {
"\n\021EchoService.proto\022\020com.example.grpc\"\036\n" +
"\013EchoRequest\022\017\n\007message\030\001 \001(\t\"-\n\014EchoRes" +
"ponse\022\017\n\007message\030\001 \001(\t\022\014\n\004from\030\002 \001(\t2T\n\013" +
"EchoService\022E\n\004echo\022\035.com.example.grpc.E" +
"choRequest\032\036.com.example.grpc.EchoRespon" +
"seB\005P\001\210\001\001b\006proto3"
};
descriptor = com.google.protobuf.Descriptors.FileDescriptor
.internalBuildGeneratedFileFrom(descriptorData,
new com.google.protobuf.Descriptors.FileDescriptor[] {
});
上記のコードのクラスは、FileDescriptor
に変換してから、FileDescriptor#toProto
で差分を比較してみると、java_generic_services
のみが有効になっていることがわかった。
FileDescriptor#toProtoで差分を比較する
import com.google.protobuf.Descriptors.FileDescriptor;
public class ShowFileOptions {
public static void main(String[] args) {
String[] descriptorData = {
"\n\021EchoService.proto\022\020com.example.grpc\"\036\n" +
"\013EchoRequest\022\017\n\007message\030\001 \001(\t\"-\n\014EchoRes" +
"ponse\022\017\n\007message\030\001 \001(\t\022\014\n\004from\030\002 \001(\t2T\n\013" +
"EchoService\022E\n\004echo\022\035.com.example.grpc.E" +
"choRequest\032\036.com.example.grpc.EchoRespon" +
"seB\002P\001b\006proto3"
};
String[] descriptorDataWithOption = {
"\n\021EchoService.proto\022\020com.example.grpc\"\036\n" +
"\013EchoRequest\022\017\n\007message\030\001 \001(\t\"-\n\014EchoRes" +
"ponse\022\017\n\007message\030\001 \001(\t\022\014\n\004from\030\002 \001(\t2T\n\013" +
"EchoService\022E\n\004echo\022\035.com.example.grpc.E" +
"choRequest\032\036.com.example.grpc.EchoRespon" +
"seB\005P\001\210\001\001b\006proto3"
};
FileDescriptor descriptor = com.google.protobuf.Descriptors.FileDescriptor
.internalBuildGeneratedFileFrom(descriptorData,
new com.google.protobuf.Descriptors.FileDescriptor[] {
});
FileDescriptor descriptorWithOption = com.google.protobuf.Descriptors.FileDescriptor
.internalBuildGeneratedFileFrom(descriptorDataWithOption,
new com.google.protobuf.Descriptors.FileDescriptor[] {
});
System.out.println(descriptorWithOption.toProto());
}
}
name: "EchoService.proto"
package: "com.example.grpc"
message_type {
name: "EchoRequest"
field {
name: "message"
number: 1
label: LABEL_OPTIONAL
type: TYPE_STRING
}
}
message_type {
name: "EchoResponse"
field {
name: "message"
number: 1
label: LABEL_OPTIONAL
type: TYPE_STRING
}
field {
name: "from"
number: 2
label: LABEL_OPTIONAL
type: TYPE_STRING
}
}
service {
name: "EchoService"
method {
name: "echo"
input_type: ".com.example.grpc.EchoRequest"
output_type: ".com.example.grpc.EchoResponse"
}
}
options {
java_multiple_files: true
+ java_generic_services: true
}
syntax: "proto3"
まとめ
-
option java_generic_services = true;
は、gRPC Java がまだ正式リリース前だった時代の当時はprotoc-gen-grpc-java
が無く、「Protobuf の標準 Service API(汎用サービス API)で RPC を書く」方式だった - その後、gRPC 公式の Java プラグイン(protoc-gen-grpc-java)が登場したため、汎用サービス APIで書く必要は無くなった
- なので、汎用サービス APIを使わない場合は
java_generic_services
で生成されるインタフェースはほとんど利用されず、コードベースに余計なファイルを増やすだけになってしまう(不要なファイルが増える) - また、汎用サービスAPIを利用する場合は、自前で実装する場合が多くかなり使いずらい(grpcの場合はライブラリに任せられる)