使用 IDL + Protobuf 跨语言定义服务

服务是 Dubbo 中的核心概念,一个服务代表一组 RPC 方法的集合,服务是面向用户编程、服务发现机制等的基本单位。Dubbo 开发的基本流程是:用户定义 RPC 服务,通过约定的配置 方式将 RPC 声明为 Dubbo 服务,然后就可以基于服务 API 进行编程了。对服务提供者来说是提供 RPC 服务的具体实现,而对服务消费者来说则是使用特定数据发起服务调用。

下面从定义服务、编译服务、配置并加载服务三个方面说明如何快速的开发 Dubbo 服务。

具体用例可以参考:dubbo-samples-triple/stub;

定义服务

Dubbo3 推荐使用 IDL 定义跨语言服务,如您更习惯使用特定语言的服务定义方式,请移步多语言 SDK查看。

syntax = "proto3";

option java_multiple_files = true;
option java_package = "org.apache.dubbo.demo";
option java_outer_classname = "DemoServiceProto";
option objc_class_prefix = "DEMOSRV";

package demoservice;

// The demo service definition.
service DemoService {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

以上是使用 IDL 定义服务的一个简单示例,我们可以把它命名为 DemoService.proto,proto 文件中定义了 RPC 服务名称 DemoService 与方法签名 SayHello (HelloRequest) returns (HelloReply) {},同时还定义了方法的入参结构体、出参结构体 HelloRequestHelloReply。 IDL 格式的服务依赖 Protobuf 编译器,用来生成可以被用户调用的客户端与服务端编程 API,Dubbo 在原生 Protobuf Compiler 的基础上提供了适配多种语言的特有插件,用于适配 Dubbo 框架特有的 API 与编程模型。

使用 Dubbo3 IDL 定义的服务只允许一个入参与出参,这种形式的服务签名有两个优势,一是对多语言实现更友好,二是可以保证服务的向后兼容性,依赖于 Protobuf 序列化的兼容性,我们可以很容易的调整传输的数据结构如增、删字段等,完全不用担心接口的兼容性。

编译服务

根据当前采用的语言,配置相应的 Protobuf 插件,编译后将生产语言相关的服务定义 stub。

Java

Java compiler 配置参考:

<plugin>
    <groupId>org.xolstice.maven.plugins</groupId>
    <artifactId>protobuf-maven-plugin</artifactId>
    <version>0.6.1</version>
    <configuration>
        <protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}
        </protocArtifact>
        <pluginId>grpc-java</pluginId>
        <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
        </pluginArtifact>
        <protocPlugins>
            <protocPlugin>
                <id>dubbo</id>
                <groupId>org.apache.dubbo</groupId>
                <artifactId>dubbo-compiler</artifactId>
                <version>3.0.10</version>
                <mainClass>org.apache.dubbo.gen.tri.Dubbo3TripleGenerator</mainClass>
            </protocPlugin>
        </protocPlugins>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>compile</goal>
                <goal>test-compile</goal>
                <goal>compile-custom</goal>
                <goal>test-compile-custom</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Java 语言生成的 stub 如下,核心是一个接口定义

@javax.annotation.Generated(
value = "by Dubbo generator",
comments = "Source: DemoService.proto")
public interface DemoService {
    static final String JAVA_SERVICE_NAME = "org.apache.dubbo.demo.DemoService";
    static final String SERVICE_NAME = "demoservice.DemoService";

    org.apache.dubbo.demo.HelloReply sayHello(org.apache.dubbo.demo.HelloRequest request);

    CompletableFuture<org.apache.dubbo.demo.HelloReply> sayHelloAsync(org.apache.dubbo.demo.HelloRequest request);
}

Golang

Go 语言生成的 stub 如下,这个 stub 里存了用户定义的接口和数据的类型。

func _DUBBO_Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
	in := new(HelloRequest)
	if err := dec(in); err != nil {
		return nil, err
	}
	base := srv.(dgrpc.Dubbo3GrpcService)
	args := []interface{}{}
	args = append(args, in)
	invo := invocation.NewRPCInvocation("SayHello", args, nil)
	if interceptor == nil {
		result := base.GetProxyImpl().Invoke(ctx, invo)
		return result.Result(), result.Error()
	}
	info := &grpc.UnaryServerInfo{
		Server:     srv,
		FullMethod: "/main.Greeter/SayHello",
	}
	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
		result := base.GetProxyImpl().Invoke(context.Background(), invo)
		return result.Result(), result.Error()
	}
	return interceptor(ctx, in, info, handler)
}

配置并加载服务

提供端负责提供具体的 Dubbo 服务实现,也就是遵循 RPC 签名所约束的格式,去实现具体的业务逻辑代码。在实现服务之后,要将服务实现注册为标准的 Dubbo 服务, 之后 Dubbo 框架就能根据接收到的请求转发给服务实现,执行方法,并将结果返回。

消费端的配置会更简单一些,只需要声明 IDL 定义的服务为标准的 Dubbo 服务,框架就可以帮助开发者生成相应的 proxy,开发者将完全面向 proxy 编程, 基本上 Dubbo 所有语言的实现都保证了 proxy 依据 IDL 服务定义暴露标准化的接口。

Java

提供端,实现服务

public class DemoServiceImpl implements DemoService {
    private static final Logger logger = LoggerFactory.getLogger(DemoServiceImpl.class);

    @Override
    public HelloReply sayHello(HelloRequest request) {
        logger.info("Hello " + request.getName() + ", request from consumer: " + RpcContext.getContext().getRemoteAddress());
        return HelloReply.newBuilder()
    .setMessage("Hello " + request.getName() + ", response from provider: "
            + RpcContext.getContext().getLocalAddress())
    .build();
    }

    @Override
    public CompletableFuture<HelloReply> sayHelloAsync(HelloRequest request) {
        return CompletableFuture.completedFuture(sayHello(request));
    }
}

提供端,注册服务(以 Spring XML 为例)

<bean id="demoServiceImpl" class="org.apache.dubbo.demo.provider.DemoServiceImpl"/>
<dubbo:service serialization="protobuf" interface="org.apache.dubbo.demo.DemoService" ref="demoServiceImpl"/>

消费端,引用服务

<dubbo:reference scope="remote" id="demoService" check="false" interface="org.apache.dubbo.demo.DemoService"/>

消费端,使用服务 proxy

public void callService() throws Exception {
    ...
    DemoService demoService = context.getBean("demoService", DemoService.class);
    HelloRequest request = HelloRequest.newBuilder().setName("Hello").build();
    HelloReply reply = demoService.sayHello(request);
    System.out.println("result: " + reply.getMessage());
}

Golang

提供端,实现服务

type User struct {
	ID   string
	Name string
	Age  int32
	Time time.Time
}

type UserProvider struct {
}

func (u *UserProvider) GetUser(ctx context.Context, req []interface{}) (*User, error) {
	gxlog.CInfo("req:%#v", req)
	rsp := User{"A001", "Alex Stocks", 18, time.Now()}
	gxlog.CInfo("rsp:%#v", rsp)
	return &rsp, nil
}

func (u *UserProvider) Reference() string {
	return "UserProvider"
}

func (u User) JavaClassName() string {
	return "org.apache.dubbo.User"
}

func main() {
    hessian.RegisterPOJO(&User{})
	config.SetProviderService(new(UserProvider))
}

消费端,使用服务 proxy

func main() {
	config.Load()
	user := &pkg.User{}
	err := userProvider.GetUser(context.TODO(), []interface{}{"A001"}, user)
	if err != nil {
		os.Exit(1)
		return
	}
	gxlog.CInfo("response result: %v\n", user)
}

最后修改 December 6, 2022: Fix check (#1661) (12cc00a1e5)