diff --git a/dubbo-plugin/dubbo-compiler/src/main/java/org/apache/dubbo/gen/AbstractGenerator.java b/dubbo-plugin/dubbo-compiler/src/main/java/org/apache/dubbo/gen/AbstractGenerator.java index 7ea75cf5392b..3d4441aaaca3 100644 --- a/dubbo-plugin/dubbo-compiler/src/main/java/org/apache/dubbo/gen/AbstractGenerator.java +++ b/dubbo-plugin/dubbo-compiler/src/main/java/org/apache/dubbo/gen/AbstractGenerator.java @@ -53,6 +53,8 @@ public abstract class AbstractGenerator { private static final MustacheFactory MUSTACHE_FACTORY = new DefaultMustacheFactory(); private static final int SERVICE_NUMBER_OF_PATHS = 2; private static final int METHOD_NUMBER_OF_PATHS = 4; + private static final String GOOGLE_PROTOBUF_EMPTY_PROTO_TYPE = ".google.protobuf.Empty"; + private static final String GOOGLE_PROTOBUF_EMPTY_JAVA_TYPE = "com.google.protobuf.Empty"; protected abstract String getClassPrefix(); @@ -184,11 +186,17 @@ private MethodContext buildMethodContext( MethodContext methodContext = new MethodContext(); methodContext.originMethodName = methodProto.getName(); methodContext.methodName = lowerCaseFirst(methodProto.getName()); - methodContext.inputType = typeMap.toJavaTypeName(methodProto.getInputType()); - methodContext.outputType = typeMap.toJavaTypeName(methodProto.getOutputType()); + methodContext.inputType = toJavaTypeName(methodProto.getInputType(), typeMap); + methodContext.outputType = toJavaTypeName(methodProto.getOutputType(), typeMap); methodContext.deprecated = methodProto.getOptions().getDeprecated(); methodContext.isManyInput = methodProto.getClientStreaming(); methodContext.isManyOutput = methodProto.getServerStreaming(); + methodContext.isEmptyOutput = GOOGLE_PROTOBUF_EMPTY_PROTO_TYPE.equals(methodProto.getOutputType()); + methodContext.returnType = methodContext.isEmptyOutput + && !methodContext.isManyInput + && !methodContext.isManyOutput + ? "void" + : methodContext.outputType; methodContext.methodNumber = methodNumber; // compile google.api.http option @@ -251,6 +259,13 @@ private MethodContext buildMethodContext( return methodContext; } + private String toJavaTypeName(String protoTypeName, ProtoTypeMap typeMap) { + if (GOOGLE_PROTOBUF_EMPTY_PROTO_TYPE.equals(protoTypeName)) { + return GOOGLE_PROTOBUF_EMPTY_JAVA_TYPE; + } + return typeMap.toJavaTypeName(protoTypeName); + } + private HttpRule parseHttpRule(MethodDescriptorProto methodProto) { HttpRule rule = null; // check methodProto have options @@ -383,6 +398,10 @@ public List unaryMethods() { .collect(Collectors.toList()); } + public boolean hasEmptyUnaryMethods() { + return unaryMethods().stream().anyMatch(m -> m.isEmptyOutput); + } + public List serverStreamingMethods() { return methods.stream() .filter(m -> !m.isManyInput && m.isManyOutput) @@ -413,14 +432,16 @@ public List methods() { */ private static class MethodContext { - // CHECKSTYLE DISABLE VisibilityModifier FOR 10 LINES + // CHECKSTYLE DISABLE VisibilityModifier FOR 12 LINES public String originMethodName; public String methodName; public String inputType; public String outputType; + public String returnType; public boolean deprecated; public boolean isManyInput; public boolean isManyOutput; + public boolean isEmptyOutput; public String reactiveCallsMethodName; public String grpcCallsMethodName; public int methodNumber; diff --git a/dubbo-plugin/dubbo-compiler/src/main/resources/Dubbo3TripleInterfaceStub.mustache b/dubbo-plugin/dubbo-compiler/src/main/resources/Dubbo3TripleInterfaceStub.mustache index 26c8e7bb5a02..0d2e8e396f35 100644 --- a/dubbo-plugin/dubbo-compiler/src/main/resources/Dubbo3TripleInterfaceStub.mustache +++ b/dubbo-plugin/dubbo-compiler/src/main/resources/Dubbo3TripleInterfaceStub.mustache @@ -51,7 +51,7 @@ public interface {{interfaceClassName}} extends org.apache.dubbo.rpc.model.Dubbo {{#hasMapping}} @Mapping(method = HttpMethods.{{httpMethod}}, path = "{{path}}") {{/hasMapping}} - {{outputType}} {{methodName}}({{#needRequestAnnotation}}@GRequest{{#body}}("{{body}}"){{/body}} {{/needRequestAnnotation}}{{inputType}} request); + {{returnType}} {{methodName}}({{#needRequestAnnotation}}@GRequest{{#body}}("{{body}}"){{/body}} {{/needRequestAnnotation}}{{inputType}} request); CompletableFuture<{{outputType}}> {{methodName}}Async({{#needRequestAnnotation}}@GRequest {{/needRequestAnnotation}}{{inputType}} request); {{/unaryMethods}} diff --git a/dubbo-plugin/dubbo-compiler/src/main/resources/Dubbo3TripleStub.mustache b/dubbo-plugin/dubbo-compiler/src/main/resources/Dubbo3TripleStub.mustache index 4bdf31830374..84d8b7266194 100644 --- a/dubbo-plugin/dubbo-compiler/src/main/resources/Dubbo3TripleStub.mustache +++ b/dubbo-plugin/dubbo-compiler/src/main/resources/Dubbo3TripleStub.mustache @@ -131,9 +131,16 @@ public final class {{className}} { {{#unaryMethods}} @Override + {{#isEmptyOutput}} + public void {{methodName}}({{inputType}} request){ + StubInvocationUtil.unaryCall(invoker, {{methodName}}Method, request); + } + {{/isEmptyOutput}} + {{^isEmptyOutput}} public {{outputType}} {{methodName}}({{inputType}} request){ return StubInvocationUtil.unaryCall(invoker, {{methodName}}Method, request); } + {{/isEmptyOutput}} public CompletableFuture<{{outputType}}> {{methodName}}Async({{inputType}} request){ return StubInvocationUtil.unaryCall(invoker, {{methodName}}AsyncMethod, request); @@ -181,12 +188,37 @@ public final class {{className}} { } }; } + {{#hasEmptyUnaryMethods}} + private BiConsumer> syncToAsyncVoid(java.util.function.Consumer syncFun, + R response) { + return new BiConsumer>() { + @Override + public void accept(T t, StreamObserver observer) { + try { + syncFun.accept(t); + observer.onNext(response); + observer.onCompleted(); + } catch (Throwable e) { + observer.onError(e); + } + } + }; + } + {{/hasEmptyUnaryMethods}} {{#unaryMethods}} @Override + {{#isEmptyOutput}} + public CompletableFuture<{{outputType}}> {{methodName}}Async({{inputType}} request){ + {{methodName}}(request); + return CompletableFuture.completedFuture({{outputType}}.getDefaultInstance()); + } + {{/isEmptyOutput}} + {{^isEmptyOutput}} public CompletableFuture<{{outputType}}> {{methodName}}Async({{inputType}} request){ return CompletableFuture.completedFuture({{methodName}}(request)); } + {{/isEmptyOutput}} {{/unaryMethods}} // This server stream type unary method is only used for generated stub to support async unary method. @@ -221,7 +253,12 @@ public final class {{className}} { {{#unaryMethods}} BiConsumer<{{inputType}}, StreamObserver<{{outputType}}>> {{methodName}}Func = this::{{methodName}}; handlers.put({{methodName}}Method.getMethodName(), new UnaryStubMethodHandler<>({{methodName}}Func)); + {{#isEmptyOutput}} + BiConsumer<{{inputType}}, StreamObserver<{{outputType}}>> {{methodName}}AsyncFunc = syncToAsyncVoid(this::{{methodName}}, {{outputType}}.getDefaultInstance()); + {{/isEmptyOutput}} + {{^isEmptyOutput}} BiConsumer<{{inputType}}, StreamObserver<{{outputType}}>> {{methodName}}AsyncFunc = syncToAsync(this::{{methodName}}); + {{/isEmptyOutput}} handlers.put({{methodName}}ProxyAsyncMethod.getMethodName(), new UnaryStubMethodHandler<>({{methodName}}AsyncFunc)); {{/unaryMethods}} {{#serverStreamingMethods}} @@ -239,7 +276,7 @@ public final class {{className}} { {{#unaryMethods}} @Override - public {{outputType}} {{methodName}}({{inputType}} request){ + public {{returnType}} {{methodName}}({{inputType}} request){ throw unimplementedMethodException({{methodName}}Method); } {{/unaryMethods}} diff --git a/dubbo-plugin/dubbo-compiler/src/test/java/org/apache/dubbo/gen/tri/Dubbo3TripleGeneratorTest.java b/dubbo-plugin/dubbo-compiler/src/test/java/org/apache/dubbo/gen/tri/Dubbo3TripleGeneratorTest.java new file mode 100644 index 000000000000..9c5acabfcfe9 --- /dev/null +++ b/dubbo-plugin/dubbo-compiler/src/test/java/org/apache/dubbo/gen/tri/Dubbo3TripleGeneratorTest.java @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.dubbo.gen.tri; + +import java.util.List; + +import com.google.protobuf.DescriptorProtos.DescriptorProto; +import com.google.protobuf.DescriptorProtos.FileDescriptorProto; +import com.google.protobuf.DescriptorProtos.FileOptions; +import com.google.protobuf.DescriptorProtos.MethodDescriptorProto; +import com.google.protobuf.DescriptorProtos.ServiceDescriptorProto; +import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest; +import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class Dubbo3TripleGeneratorTest { + + @Test + void generateVoidMethodForGoogleProtobufEmptyOutput() { + List files = new Dubbo3TripleGenerator().generateFiles(emptyOutputRequest()); + + String interfaceContent = + findFile(files, "org/apache/dubbo/test/EmptyService.java").getContent(); + String stubContent = findFile(files, "org/apache/dubbo/test/DubboEmptyServiceTriple.java") + .getContent(); + + assertTrue(interfaceContent.contains("void ack("), interfaceContent); + assertTrue( + interfaceContent.contains("CompletableFuture ackAsync("), interfaceContent); + assertTrue(stubContent.contains("public void ack("), stubContent); + assertTrue(stubContent.contains("StubInvocationUtil.unaryCall(invoker, ackMethod, request);")); + assertTrue(stubContent.contains("com.google.protobuf.Empty::parseFrom")); + assertTrue(stubContent.contains("syncToAsyncVoid(this::ack")); + assertTrue(stubContent.contains( + "CompletableFuture.completedFuture(com.google.protobuf.Empty.getDefaultInstance());")); + } + + @Test + void generateMessageReturnMethodForNormalUnaryOutput() { + List files = new Dubbo3TripleGenerator().generateFiles(normalOutputRequest()); + + String interfaceContent = + findFile(files, "org/apache/dubbo/test/EchoService.java").getContent(); + String stubContent = findFile(files, "org/apache/dubbo/test/DubboEchoServiceTriple.java") + .getContent(); + + assertTrue(interfaceContent.contains("org.apache.dubbo.test.SampleResponse echo("), interfaceContent); + assertTrue( + interfaceContent.contains("CompletableFuture echoAsync("), + interfaceContent); + assertTrue(stubContent.contains("public org.apache.dubbo.test.SampleResponse echo("), stubContent); + assertTrue(stubContent.contains("return StubInvocationUtil.unaryCall(invoker, echoMethod, request);")); + assertFalse(stubContent.contains("syncToAsyncVoid("), stubContent); + } + + private CodeGeneratorResponse.File findFile(List files, String name) { + return files.stream() + .filter(file -> name.equals(file.getName())) + .findFirst() + .orElseThrow(() -> new AssertionError("Generated file not found: " + name)); + } + + private CodeGeneratorRequest emptyOutputRequest() { + FileDescriptorProto sampleProto = FileDescriptorProto.newBuilder() + .setName("sample.proto") + .setPackage("org.apache.dubbo.test") + .addDependency("google/protobuf/empty.proto") + .setOptions(FileOptions.newBuilder() + .setJavaPackage("org.apache.dubbo.test") + .setJavaMultipleFiles(true)) + .addMessageType(DescriptorProto.newBuilder().setName("SampleRequest")) + .addService(ServiceDescriptorProto.newBuilder() + .setName("EmptyService") + .addMethod(MethodDescriptorProto.newBuilder() + .setName("Ack") + .setInputType(".org.apache.dubbo.test.SampleRequest") + .setOutputType(".google.protobuf.Empty"))) + .build(); + + return CodeGeneratorRequest.newBuilder() + .addFileToGenerate("sample.proto") + .addProtoFile(sampleProto) + .build(); + } + + private CodeGeneratorRequest normalOutputRequest() { + FileDescriptorProto sampleProto = FileDescriptorProto.newBuilder() + .setName("normal.proto") + .setPackage("org.apache.dubbo.test") + .setOptions(FileOptions.newBuilder() + .setJavaPackage("org.apache.dubbo.test") + .setJavaMultipleFiles(true)) + .addMessageType(DescriptorProto.newBuilder().setName("SampleRequest")) + .addMessageType(DescriptorProto.newBuilder().setName("SampleResponse")) + .addService(ServiceDescriptorProto.newBuilder() + .setName("EchoService") + .addMethod(MethodDescriptorProto.newBuilder() + .setName("Echo") + .setInputType(".org.apache.dubbo.test.SampleRequest") + .setOutputType(".org.apache.dubbo.test.SampleResponse"))) + .build(); + + return CodeGeneratorRequest.newBuilder() + .addFileToGenerate("normal.proto") + .addProtoFile(sampleProto) + .build(); + } +}