From 307ebf1fc9475073a5130af4c1a03323ee37f3e2 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Mon, 10 May 2021 11:53:44 +0200 Subject: [PATCH] gRPC - fail the build if an unsupported client stub is injected - resolves #17062 - also rename GrpcServiceBuildItem to GrpcClientBuildItem - Mutiny generated stub now implements the MutinyStub marker interface --- .../grpc/deployment/GrpcClientBuildItem.java | 80 ++++++++++ .../grpc/deployment/GrpcClientProcessor.java | 144 ++++++++++-------- .../quarkus/grpc/deployment/GrpcDotNames.java | 5 + .../grpc/deployment/GrpcServiceBuildItem.java | 30 ---- .../grpc/client/InvalidInjectionTypeTest.java | 45 ++++++ .../src/main/resources/MutinyStub.mustache | 2 +- .../io/quarkus/grpc/runtime/MutinyStub.java | 8 + 7 files changed, 222 insertions(+), 92 deletions(-) create mode 100644 extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcClientBuildItem.java delete mode 100644 extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServiceBuildItem.java create mode 100644 extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/InvalidInjectionTypeTest.java create mode 100644 extensions/grpc/stubs/src/main/java/io/quarkus/grpc/runtime/MutinyStub.java diff --git a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcClientBuildItem.java b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcClientBuildItem.java new file mode 100644 index 0000000000000..27ad357e9972e --- /dev/null +++ b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcClientBuildItem.java @@ -0,0 +1,80 @@ +package io.quarkus.grpc.deployment; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import org.jboss.jandex.DotName; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class GrpcClientBuildItem extends MultiBuildItem { + + private final String name; + private final Set stubs; + + public GrpcClientBuildItem(String name) { + this.name = name; + this.stubs = new HashSet<>(); + } + + public Set getStubs() { + return stubs; + } + + public void addStub(DotName stubClass, StubType type) { + stubs.add(new StubInfo(stubClass, type)); + } + + public String getServiceName() { + return name; + } + + public static final class StubInfo { + + public final DotName className; + public final StubType type; + + public StubInfo(DotName className, StubType type) { + this.className = className; + this.type = type; + } + + @Override + public int hashCode() { + return Objects.hash(className); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + StubInfo other = (StubInfo) obj; + return Objects.equals(className, other.className); + } + + } + + public enum StubType { + + BLOCKING("newBlockingStub"), + MUTINY("newMutinyStub"); + + private final String factoryMethodName; + + StubType(String factoryMethodName) { + this.factoryMethodName = factoryMethodName; + } + + public String getFactoryMethodName() { + return factoryMethodName; + } + } +} diff --git a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcClientProcessor.java b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcClientProcessor.java index 7c2074cb35c5c..c347990f8f4d7 100644 --- a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcClientProcessor.java +++ b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcClientProcessor.java @@ -6,8 +6,10 @@ import static io.quarkus.grpc.deployment.ResourceRegistrationUtils.registerResourcesForProperties; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Consumer; import java.util.regex.Pattern; @@ -19,8 +21,9 @@ import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget.Kind; import org.jboss.jandex.AnnotationValue; -import org.jboss.jandex.ClassType; +import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodParameterInfo; import org.jboss.jandex.Type; import org.jboss.logging.Logger; @@ -28,11 +31,12 @@ import io.grpc.Channel; import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem; -import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem; -import io.quarkus.arc.processor.BeanConfigurator; +import io.quarkus.arc.deployment.SyntheticBeanBuildItem; +import io.quarkus.arc.processor.DotNames; import io.quarkus.arc.processor.InjectionPointInfo; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; @@ -40,6 +44,8 @@ import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import io.quarkus.grpc.GrpcClient; +import io.quarkus.grpc.deployment.GrpcClientBuildItem.StubInfo; +import io.quarkus.grpc.deployment.GrpcClientBuildItem.StubType; import io.quarkus.grpc.runtime.GrpcClientInterceptorContainer; import io.quarkus.grpc.runtime.supports.Channels; import io.quarkus.grpc.runtime.supports.GrpcClientConfigProvider; @@ -64,10 +70,11 @@ void registerBeans(BuildProducer beans) { @BuildStep void discoverInjectedGrpcServices(BeanDiscoveryFinishedBuildItem beanDiscovery, - BuildProducer services, - BuildProducer features) { + BuildProducer services, + BuildProducer features, + CombinedIndexBuildItem index) { - Map items = new HashMap<>(); + Map items = new HashMap<>(); for (InjectionPointInfo injectionPoint : beanDiscovery.getInjectionPoints()) { AnnotationInstance clientAnnotation = injectionPoint.getRequiredQualifier(GrpcDotNames.GRPC_CLIENT); @@ -104,52 +111,85 @@ void discoverInjectedGrpcServices(BeanDiscoveryFinishedBuildItem beanDiscovery, "Invalid @GrpcClient `" + injectionPoint.getTargetInfo() + "` - service name cannot be empty"); } - GrpcServiceBuildItem item; + GrpcClientBuildItem item; if (items.containsKey(serviceName)) { item = items.get(serviceName); } else { - item = new GrpcServiceBuildItem(serviceName); + item = new GrpcClientBuildItem(serviceName); items.put(serviceName, item); } Type injectionType = injectionPoint.getRequiredType(); - ClassType type; - if (injectionType.kind() == Type.Kind.PARAMETERIZED_TYPE) { - // Instance - type = injectionType.asParameterizedType().arguments().get(0).asClassType(); - } else { - // X directly - type = injectionType.asClassType(); + + // Programmatic lookup - take the param type + if (DotNames.INSTANCE.equals(injectionType.name()) || DotNames.INJECTABLE_INSTANCE.equals(injectionType.name())) { + injectionType = injectionType.asParameterizedType().arguments().get(0); } - if (!type.name().equals(GrpcDotNames.CHANNEL)) { - item.addStubClass(type); + + if (injectionType.name().equals(GrpcDotNames.CHANNEL)) { + // No need to add the stub class for Channel + continue; } - } - items.values().forEach(new Consumer() { - @Override - public void accept(GrpcServiceBuildItem item) { - services.produce(item); - LOGGER.debugf("Detected GrpcService associated with the '%s' configuration prefix", item.name); + // Only blocking and Mutiny stubs are supported + // The required type must have io.grpc.stub.AbstractBlockingStub or io.quarkus.grpc.runtime.MutinyStub in the hierarchy + // Note that we must use the computing index because the generated stubs are not part of the app index + Set rawTypes = getRawTypeClosure(index.getComputingIndex().getClassByName(injectionType.name()), + index.getComputingIndex()); + + if (rawTypes.contains(GrpcDotNames.ABSTRACT_BLOCKING_STUB)) { + item.addStub(injectionType.name(), StubType.BLOCKING); + } else if (rawTypes.contains(GrpcDotNames.MUTINY_STUB)) { + item.addStub(injectionType.name(), StubType.MUTINY); + } else { + throw new DeploymentException( + injectionType + " cannot be injected into " + injectionPoint.getTargetInfo() + + " - only blocking stubs, reactive stubs based on Mutiny and io.grpc.Channel can be injected via @GrpcClient"); } - }); + } if (!items.isEmpty()) { + for (GrpcClientBuildItem item : items.values()) { + services.produce(item); + LOGGER.debugf("Detected GrpcService associated with the '%s' configuration prefix", item.getServiceName()); + } features.produce(new FeatureBuildItem(GRPC_CLIENT)); } } - private boolean isMutinyStub(DotName name) { - return name.local().startsWith("Mutiny") && name.local().endsWith("Stub"); + private static Set getRawTypeClosure(ClassInfo classInfo, IndexView index) { + Set types = new HashSet<>(); + types.add(classInfo.name()); + // Interfaces + for (DotName name : classInfo.interfaceNames()) { + ClassInfo interfaceClassInfo = index.getClassByName(name); + if (interfaceClassInfo != null) { + types.addAll(getRawTypeClosure(interfaceClassInfo, index)); + } else { + // Interface not found in the index + types.add(name); + } + } + // Superclass + DotName superName = classInfo.superName(); + if (superName != null && !DotNames.OBJECT.equals(superName)) { + ClassInfo superClassInfo = index.getClassByName(superName); + if (superClassInfo != null) { + types.addAll(getRawTypeClosure(superClassInfo, index)); + } else { + // Superclass not found in the index + types.add(superName); + } + } + return types; } @BuildStep - public void generateGrpcServicesProducers(List services, - BeanRegistrationPhaseBuildItem phase, - BuildProducer beans) { + public void generateGrpcServicesProducers(List services, + BuildProducer syntheticBeans) { - for (GrpcServiceBuildItem svc : services) { - // We generate 3 producers: + for (GrpcClientBuildItem svc : services) { + // We generate 3 synthetic beans: // 1. the channel // 2. the blocking stub - if blocking stub is set // 3. the mutiny stub - if mutiny stub is set @@ -157,9 +197,7 @@ public void generateGrpcServicesProducers(List services, // IMPORTANT: the channel producer relies on the io.quarkus.grpc.runtime.supports.GrpcClientConfigProvider // bean that provides the GrpcClientConfiguration for the specific service. - BeanConfigurator channelProducer = phase.getContext() - .configure(GrpcDotNames.CHANNEL) - .types(Channel.class) + syntheticBeans.produce(SyntheticBeanBuildItem.configure(GrpcDotNames.CHANNEL) .addQualifier().annotation(GrpcDotNames.GRPC_CLIENT).addValue("value", svc.getServiceName()).done() .scope(Singleton.class) .unremovable() @@ -169,27 +207,19 @@ public void accept(MethodCreator mc) { GrpcClientProcessor.this.generateChannelProducer(mc, svc); } }) - .destroyer(Channels.ChannelDestroyer.class); - channelProducer.done(); - beans.produce(new BeanRegistrationPhaseBuildItem.BeanConfiguratorBuildItem(channelProducer)); + .destroyer(Channels.ChannelDestroyer.class).done()); String svcName = svc.getServiceName(); - for (ClassType stubClass : svc.getStubClasses()) { - DotName stubClassName = stubClass.name(); - BeanConfigurator stubProducer = phase.getContext() - .configure(stubClassName) - .types(stubClass) + for (StubInfo stub : svc.getStubs()) { + syntheticBeans.produce(SyntheticBeanBuildItem.configure(stub.className) .addQualifier().annotation(GrpcDotNames.GRPC_CLIENT).addValue("value", svcName).done() .scope(Singleton.class) .creator(new Consumer() { @Override public void accept(MethodCreator mc) { - GrpcClientProcessor.this.generateStubProducer(mc, svcName, stubClassName, - isMutinyStub(stubClassName)); + GrpcClientProcessor.this.generateStubProducer(mc, svcName, stub); } - }); - stubProducer.done(); - beans.produce(new BeanRegistrationPhaseBuildItem.BeanConfiguratorBuildItem(stubProducer)); + }).done()); } } } @@ -206,29 +236,21 @@ void runtimeInitialize(BuildProducer producer) producer.produce(new RuntimeInitializedClassBuildItem("io.grpc.internal.RetriableStream")); } - private void generateChannelProducer(MethodCreator mc, GrpcServiceBuildItem svc) { + private void generateChannelProducer(MethodCreator mc, GrpcClientBuildItem svc) { ResultHandle name = mc.load(svc.getServiceName()); ResultHandle result = mc.invokeStaticMethod(CREATE_CHANNEL_METHOD, name); mc.returnValue(result); mc.close(); } - private void generateStubProducer(MethodCreator mc, String svcName, DotName stubClassName, boolean mutiny) { + private void generateStubProducer(MethodCreator mc, String svcName, StubInfo stubInfo) { ResultHandle prefix = mc.load(svcName); ResultHandle channel = mc.invokeStaticMethod(RETRIEVE_CHANNEL_METHOD, prefix); - MethodDescriptor descriptor; - if (mutiny) { - descriptor = MethodDescriptor - .ofMethod(convertToServiceName(stubClassName), "newMutinyStub", - stubClassName.toString(), - Channel.class.getName()); - } else { - descriptor = MethodDescriptor - .ofMethod(convertToServiceName(stubClassName), "newBlockingStub", - stubClassName.toString(), - Channel.class.getName()); - } + MethodDescriptor descriptor = MethodDescriptor + .ofMethod(convertToServiceName(stubInfo.className), stubInfo.type.getFactoryMethodName(), + stubInfo.className.toString(), + Channel.class.getName()); ResultHandle stub = mc.invokeStaticMethod(descriptor, channel); mc.returnValue(stub); diff --git a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcDotNames.java b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcDotNames.java index d951df524c3e3..4d8402c3d3cea 100644 --- a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcDotNames.java +++ b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcDotNames.java @@ -4,9 +4,11 @@ import io.grpc.BindableService; import io.grpc.Channel; +import io.grpc.stub.AbstractBlockingStub; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.grpc.GrpcClient; import io.quarkus.grpc.GrpcService; +import io.quarkus.grpc.runtime.MutinyStub; import io.quarkus.grpc.runtime.supports.Channels; import io.smallrye.common.annotation.Blocking; @@ -19,6 +21,9 @@ public class GrpcDotNames { static final DotName BLOCKING = DotName.createSimple(Blocking.class.getName()); + static final DotName ABSTRACT_BLOCKING_STUB = DotName.createSimple(AbstractBlockingStub.class.getName()); + static final DotName MUTINY_STUB = DotName.createSimple(MutinyStub.class.getName()); + static final MethodDescriptor CREATE_CHANNEL_METHOD = MethodDescriptor.ofMethod(Channels.class, "createChannel", Channel.class, String.class); static final MethodDescriptor RETRIEVE_CHANNEL_METHOD = MethodDescriptor.ofMethod(Channels.class, "retrieveChannel", diff --git a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServiceBuildItem.java b/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServiceBuildItem.java deleted file mode 100644 index 3a13af76c03fe..0000000000000 --- a/extensions/grpc/deployment/src/main/java/io/quarkus/grpc/deployment/GrpcServiceBuildItem.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.quarkus.grpc.deployment; - -import java.util.HashSet; -import java.util.Set; - -import org.jboss.jandex.ClassType; - -import io.quarkus.builder.item.MultiBuildItem; - -public final class GrpcServiceBuildItem extends MultiBuildItem { - - final String name; - Set stubClasses = new HashSet(); - - public GrpcServiceBuildItem(String name) { - this.name = name; - } - - public Set getStubClasses() { - return stubClasses; - } - - public void addStubClass(ClassType stubClass) { - stubClasses.add(stubClass); - } - - public String getServiceName() { - return name; - } -} diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/InvalidInjectionTypeTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/InvalidInjectionTypeTest.java new file mode 100644 index 0000000000000..fcc798338f4eb --- /dev/null +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/client/InvalidInjectionTypeTest.java @@ -0,0 +1,45 @@ +package io.quarkus.grpc.client; + +import static org.junit.jupiter.api.Assertions.fail; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.spi.DeploymentException; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.grpc.examples.helloworld.GreeterGrpc; +import io.grpc.examples.helloworld.HelloReply; +import io.grpc.examples.helloworld.HelloReplyOrBuilder; +import io.grpc.examples.helloworld.HelloRequest; +import io.grpc.examples.helloworld.HelloRequestOrBuilder; +import io.grpc.examples.helloworld.MutinyGreeterGrpc; +import io.quarkus.grpc.GrpcClient; +import io.quarkus.test.QuarkusUnitTest; + +public class InvalidInjectionTypeTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(GreeterGrpc.class, GreeterGrpc.GreeterStub.class, + MutinyGreeterGrpc.MutinyGreeterStub.class, MutinyGreeterGrpc.class, + HelloRequest.class, HelloReply.class, + HelloReplyOrBuilder.class, HelloRequestOrBuilder.class)) + .setExpectedException(DeploymentException.class); + + @Test + public void runTest() { + fail(); + } + + @ApplicationScoped + static class MyConsumer { + + @GrpcClient + GreeterGrpc.GreeterStub stub; + + } +} diff --git a/extensions/grpc/protoc/src/main/resources/MutinyStub.mustache b/extensions/grpc/protoc/src/main/resources/MutinyStub.mustache index b71dfc8fc1977..c9e50dd12982d 100644 --- a/extensions/grpc/protoc/src/main/resources/MutinyStub.mustache +++ b/extensions/grpc/protoc/src/main/resources/MutinyStub.mustache @@ -23,7 +23,7 @@ public final class {{className}} { } {{#javaDoc}}{{{javaDoc}}}{{/javaDoc}} - public static final class {{classPrefix}}{{serviceName}}Stub extends io.grpc.stub.AbstractStub<{{classPrefix}}{{serviceName}}Stub> { + public static final class {{classPrefix}}{{serviceName}}Stub extends io.grpc.stub.AbstractStub<{{classPrefix}}{{serviceName}}Stub> implements io.quarkus.grpc.runtime.MutinyStub { private {{serviceName}}Grpc.{{serviceName}}Stub delegateStub; private {{classPrefix}}{{serviceName}}Stub(io.grpc.Channel channel) { diff --git a/extensions/grpc/stubs/src/main/java/io/quarkus/grpc/runtime/MutinyStub.java b/extensions/grpc/stubs/src/main/java/io/quarkus/grpc/runtime/MutinyStub.java new file mode 100644 index 0000000000000..46f312d2e9a68 --- /dev/null +++ b/extensions/grpc/stubs/src/main/java/io/quarkus/grpc/runtime/MutinyStub.java @@ -0,0 +1,8 @@ +package io.quarkus.grpc.runtime; + +/** + * A marker interface that represents a generated Mutiny gRPC stub. + */ +public interface MutinyStub { + +}