Skip to content

Commit

Permalink
Merge pull request quarkusio#19443 from michalszynkiewicz/grpc-per-se…
Browse files Browse the repository at this point in the history
…rvice-interceptors

gRPC: support per service interceptors
  • Loading branch information
michalszynkiewicz authored Aug 20, 2021
2 parents 1a63a85 + ad9d45a commit adf1cec
Show file tree
Hide file tree
Showing 20 changed files with 567 additions and 40 deletions.
22 changes: 20 additions & 2 deletions docs/src/main/asciidoc/grpc-service-implementation.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -243,11 +243,14 @@ quarkus.grpc.server.ssl.client-auth=REQUIRED

== Server Interceptors

You can implement a gRPC server interceptor by implementing an `@ApplicationScoped` bean implementing `io.grpc.ServerInterceptor`:
gRPC server interceptors let you perform logic, such as authentication, before your service is invoked.

You can implement a gRPC server interceptor by creating an `@ApplicationScoped` bean implementing `io.grpc.ServerInterceptor`:

[source, java]
----
@ApplicationScoped
// add @GlobalInterceptor for interceptors meant to be invoked for every service
public class MyInterceptor implements ServerInterceptor {
@Override
Expand All @@ -260,7 +263,22 @@ public class MyInterceptor implements ServerInterceptor {

TIP: Check the https://grpc.github.io/grpc-java/javadoc/io/grpc/ServerInterceptor.html[ServerInterceptor JavaDoc] to properly implement your interceptor.

When you have multiple server interceptors, you can order them by implementing the `javax.enterprise.inject.spi.Prioritized` interface:
To apply an interceptor to all exposed services, annotate it with `@io.quarkus.grpc.GlobalInterceptor`.
To apply an interceptor to a single service, register it on the service with `@io.quarkus.grpc.RegisterInterceptor`:
[source, java]
----
import io.quarkus.grpc.GrpcService;
import io.quarkus.grpc.RegisterInterceptor;
@GrpcService
@RegisterInterceptor(MyInterceptor.class)
public class StreamingService implements Streaming {
// ...
}
----

When you have multiple server interceptors, you can order them by implementing the `javax.enterprise.inject.spi.Prioritized` interface. Please note that all the global interceptors are invoked before the service-specific
interceptors.

[source, java]
----
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.quarkus.grpc.deployment;

import io.quarkus.builder.item.MultiBuildItem;

public final class AdditionalGlobalInterceptorBuildItem extends MultiBuildItem {
private final String interceptorClass;

public AdditionalGlobalInterceptorBuildItem(String interceptorClass) {
this.interceptorClass = interceptorClass;
}

public String interceptorClass() {
return interceptorClass;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.quarkus.grpc.deployment;

import org.jboss.jandex.ClassInfo;

import io.quarkus.builder.item.MultiBuildItem;

final class DelegatingGrpcBeanBuildItem extends MultiBuildItem {
final ClassInfo generatedBean;
final ClassInfo userDefinedBean;

DelegatingGrpcBeanBuildItem(ClassInfo generatedBean, ClassInfo userDefinedBean) {
this.generatedBean = generatedBean;
this.userDefinedBean = userDefinedBean;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@

import io.grpc.BindableService;
import io.grpc.Channel;
import io.grpc.ServerInterceptor;
import io.grpc.stub.AbstractBlockingStub;
import io.grpc.stub.AbstractStub;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.grpc.GlobalInterceptor;
import io.quarkus.grpc.GrpcClient;
import io.quarkus.grpc.GrpcService;
import io.quarkus.grpc.RegisterInterceptor;
import io.quarkus.grpc.RegisterInterceptors;
import io.quarkus.grpc.runtime.MutinyBean;
import io.quarkus.grpc.runtime.MutinyClient;
import io.quarkus.grpc.runtime.MutinyGrpc;
Expand Down Expand Up @@ -38,6 +42,11 @@ public class GrpcDotNames {
public static final DotName MUTINY_BEAN = DotName.createSimple(MutinyBean.class.getName());
public static final DotName MUTINY_SERVICE = DotName.createSimple(MutinyService.class.getName());

public static final DotName GLOBAL_INTERCEPTOR = DotName.createSimple(GlobalInterceptor.class.getName());
public static final DotName REGISTER_INTERCEPTOR = DotName.createSimple(RegisterInterceptor.class.getName());
public static final DotName REGISTER_INTERCEPTORS = DotName.createSimple(RegisterInterceptors.class.getName());
public static final DotName SERVER_INTERCEPTOR = DotName.createSimple(ServerInterceptor.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",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package io.quarkus.grpc.deployment;

import static io.quarkus.deployment.Feature.GRPC_SERVER;
import static io.quarkus.grpc.deployment.GrpcDotNames.GLOBAL_INTERCEPTOR;
import static io.quarkus.grpc.deployment.GrpcDotNames.REGISTER_INTERCEPTOR;
import static io.quarkus.grpc.deployment.GrpcDotNames.REGISTER_INTERCEPTORS;
import static java.util.Arrays.asList;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -14,13 +19,16 @@
import java.util.Set;
import java.util.function.Predicate;

import javax.inject.Singleton;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;
import org.jboss.logging.Logger;
Expand All @@ -29,7 +37,10 @@
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
import io.quarkus.arc.deployment.CustomScopeAnnotationsBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanBuildItem;
import io.quarkus.arc.deployment.GeneratedBeanGizmoAdaptor;
import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem;
import io.quarkus.arc.deployment.UnremovableBeanBuildItem;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem;
import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.arc.processor.BeanInfo;
Expand All @@ -50,11 +61,15 @@
import io.quarkus.deployment.builditem.ShutdownContextBuildItem;
import io.quarkus.deployment.builditem.nativeimage.NativeImageResourceBuildItem;
import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.grpc.GrpcService;
import io.quarkus.grpc.deployment.devmode.FieldDefinalizingVisitor;
import io.quarkus.grpc.protoc.plugin.MutinyGrpcGenerator;
import io.quarkus.grpc.runtime.GrpcContainer;
import io.quarkus.grpc.runtime.GrpcServerRecorder;
import io.quarkus.grpc.runtime.InterceptorStorage;
import io.quarkus.grpc.runtime.config.GrpcConfiguration;
import io.quarkus.grpc.runtime.config.GrpcServerBuildTimeConfig;
import io.quarkus.grpc.runtime.health.GrpcHealthEndpoint;
Expand All @@ -71,13 +86,14 @@ public class GrpcServerProcessor {

private static final Set<String> BLOCKING_SKIPPED_METHODS = Set.of("bindService", "<init>", "withCompression");

private static final Logger logger = Logger.getLogger(GrpcServerProcessor.class);
private static final Logger log = Logger.getLogger(GrpcServerProcessor.class);

private static final String SSL_PREFIX = "quarkus.grpc.server.ssl.";
private static final String CERTIFICATE = SSL_PREFIX + "certificate";
private static final String KEY = SSL_PREFIX + "key";
private static final String KEY_STORE = SSL_PREFIX + "key-store";
private static final String TRUST_STORE = SSL_PREFIX + "trust-store";
private static final String METRICS_SERVER_INTERCEPTOR = "io.quarkus.grpc.runtime.metrics.GrpcMetricsServerInterceptor";

@BuildStep
MinNettyAllocatorMaxOrderBuildItem setMinimalNettyMaxOrderSize() {
Expand All @@ -86,7 +102,8 @@ MinNettyAllocatorMaxOrderBuildItem setMinimalNettyMaxOrderSize() {

@BuildStep
void processGeneratedBeans(CombinedIndexBuildItem index, BuildProducer<AnnotationsTransformerBuildItem> transformers,
BuildProducer<BindableServiceBuildItem> bindables) {
BuildProducer<BindableServiceBuildItem> bindables,
BuildProducer<DelegatingGrpcBeanBuildItem> delegatingBeans) {

// generated bean class -> blocking methods
Map<DotName, Set<MethodInfo>> generatedBeans = new HashMap<>();
Expand Down Expand Up @@ -130,7 +147,7 @@ void processGeneratedBeans(CombinedIndexBuildItem index, BuildProducer<Annotatio
// Some class extends the impl base
continue;
}
// Finally exclude some packages
// Finally, exclude some packages
boolean excluded = false;
for (String excludedPackage : excludedPackages) {
if (mutinyImplBaseName.startsWith(excludedPackage)) {
Expand All @@ -139,7 +156,8 @@ void processGeneratedBeans(CombinedIndexBuildItem index, BuildProducer<Annotatio
}
}
if (!excluded) {
logger.debugf("Registering generated gRPC bean %s that will delegate to %s", generatedBean, userDefinedBean);
log.debugf("Registering generated gRPC bean %s that will delegate to %s", generatedBean, userDefinedBean);
delegatingBeans.produce(new DelegatingGrpcBeanBuildItem(generatedBean, userDefinedBean));
Set<MethodInfo> blockingMethods = gatherBlockingMethods(userDefinedBean);

generatedBeans.put(generatedBean.name(), blockingMethods);
Expand Down Expand Up @@ -307,8 +325,121 @@ void registerBeans(BuildProducer<AdditionalBeanBuildItem> beans,
beans.produce(AdditionalBeanBuildItem.unremovableOf(GrpcRequestContextGrpcInterceptor.class));
features.produce(new FeatureBuildItem(GRPC_SERVER));
} else {
logger.debug("Unable to find beans exposing the `BindableService` interface - not starting the gRPC server");
log.debug("Unable to find beans exposing the `BindableService` interface - not starting the gRPC server");
}
}

@BuildStep
void registerAdditionalInterceptors(BuildProducer<AdditionalGlobalInterceptorBuildItem> additionalInterceptors) {
additionalInterceptors
.produce(new AdditionalGlobalInterceptorBuildItem(GrpcRequestContextGrpcInterceptor.class.getName()));
}

@BuildStep
void gatherGrpcInterceptors(CombinedIndexBuildItem indexBuildItem,
List<AdditionalGlobalInterceptorBuildItem> additionalGlobalInterceptors,
List<DelegatingGrpcBeanBuildItem> delegatingGrpcBeans,
BuildProducer<GeneratedBeanBuildItem> generatedBeans,
BuildProducer<UnremovableBeanBuildItem> unremovableBeans) {

Map<String, String> delegateMap = new HashMap<>();
for (DelegatingGrpcBeanBuildItem delegatingGrpcBean : delegatingGrpcBeans) {
delegateMap.put(delegatingGrpcBean.userDefinedBean.name().toString(),
delegatingGrpcBean.generatedBean.name().toString());
}

IndexView index = indexBuildItem.getIndex();

GrpcInterceptors interceptors = gatherInterceptors(index);

// let's gather all the non-abstract, non-global interceptors, from these we'll filter out ones used per-service ones
// the rest, if anything stays, should be logged as problematic
Set<String> superfluousInterceptors = new HashSet<>(interceptors.nonGlobalInterceptors);

Map<String, List<AnnotationInstance>> annotationsByClassName = new HashMap<>();

for (AnnotationInstance annotation : index.getAnnotations(REGISTER_INTERCEPTOR)) {
String targetClass = annotation.target().asClass().name().toString();
annotationsByClassName.computeIfAbsent(targetClass, key -> new ArrayList<>())
.add(annotation);
}

for (AnnotationInstance annotation : index.getAnnotations(REGISTER_INTERCEPTORS)) {
String targetClass = annotation.target().asClass().name().toString();
annotationsByClassName.computeIfAbsent(targetClass, key -> new ArrayList<>())
.addAll(Arrays.asList(annotation.value().asNestedArray()));
}

String perServiceInterceptorsImpl = InterceptorStorage.class.getName() + "Impl";
try (ClassCreator classCreator = ClassCreator.builder()
.className(perServiceInterceptorsImpl)
.classOutput(new GeneratedBeanGizmoAdaptor(generatedBeans))
.superClass(InterceptorStorage.class)
.build()) {

classCreator.addAnnotation(Singleton.class.getName());
MethodCreator constructor = classCreator
.getMethodCreator(MethodDescriptor.ofConstructor(perServiceInterceptorsImpl));
constructor.invokeSpecialMethod(MethodDescriptor.ofConstructor(InterceptorStorage.class),
constructor.getThis());

for (Map.Entry<String, List<AnnotationInstance>> annotationsForClass : annotationsByClassName.entrySet()) {
for (AnnotationInstance value : annotationsForClass.getValue()) {
String className = annotationsForClass.getKey();

// if the user bean is invoked by a generated bean
// the interceptors defined on the user bean have to be applied to the generated bean:
className = delegateMap.getOrDefault(className, className);

String interceptorClassName = value.value().asString();
superfluousInterceptors.remove(interceptorClassName);

constructor.invokeVirtualMethod(
MethodDescriptor.ofMethod(InterceptorStorage.class, "addInterceptor", void.class,
String.class, Class.class),
constructor.getThis(), constructor.load(className), constructor.loadClass(interceptorClassName));
}
}

for (String globalInterceptor : interceptors.globalInterceptors) {
constructor.invokeVirtualMethod(
MethodDescriptor.ofMethod(InterceptorStorage.class, "addGlobalInterceptor", void.class, Class.class),
constructor.getThis(), constructor.loadClass(globalInterceptor));
}

for (AdditionalGlobalInterceptorBuildItem globalInterceptorBuildItem : additionalGlobalInterceptors) {
constructor.invokeVirtualMethod(
MethodDescriptor.ofMethod(InterceptorStorage.class, "addGlobalInterceptor", void.class, Class.class),
constructor.getThis(), constructor.loadClass(globalInterceptorBuildItem.interceptorClass()));
}

constructor.returnValue(null);
}

if (!superfluousInterceptors.isEmpty()) {
log.warnf("At least one unused gRPC interceptor found: %s. If there are meant to be used globally, " +
"annotate them with @GlobalInterceptor.", String.join(", ", superfluousInterceptors));
}

unremovableBeans.produce(UnremovableBeanBuildItem.beanClassNames(perServiceInterceptorsImpl));
}

private GrpcInterceptors gatherInterceptors(IndexView index) {
Set<String> globalInterceptors = new HashSet<>();
Set<String> nonGlobalInterceptors = new HashSet<>();

Collection<ClassInfo> interceptorImplClasses = index.getAllKnownImplementors(GrpcDotNames.SERVER_INTERCEPTOR);
for (ClassInfo interceptorImplClass : interceptorImplClasses) {
if (!Modifier.isAbstract(interceptorImplClass.flags())
&& !Modifier.isInterface(interceptorImplClass.flags())) {
if (interceptorImplClass.classAnnotation(GLOBAL_INTERCEPTOR) == null) {
nonGlobalInterceptors.add(interceptorImplClass.name().toString());
} else {
globalInterceptors.add(interceptorImplClass.name().toString());
}
}
}
return new GrpcInterceptors(globalInterceptors, nonGlobalInterceptors);
}

@BuildStep
Expand Down Expand Up @@ -379,18 +510,31 @@ ExtensionSslNativeSupportBuildItem extensionSslNativeSupport() {

@BuildStep
void configureMetrics(GrpcBuildTimeConfig configuration, Optional<MetricsCapabilityBuildItem> metricsCapability,
BuildProducer<AdditionalBeanBuildItem> beans) {
BuildProducer<AdditionalBeanBuildItem> beans,
BuildProducer<AdditionalGlobalInterceptorBuildItem> additionalInterceptors) {

// Note that this build steps confgures both the server side and the client side
// Note that this build steps configures both the server side and the client side
if (configuration.metricsEnabled && metricsCapability.isPresent()) {
if (metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER)) {
// Strings are used intentionally - micrometer-core is an optional dependency of the runtime module
beans.produce(new AdditionalBeanBuildItem("io.quarkus.grpc.runtime.metrics.GrpcMetricsServerInterceptor",
beans.produce(new AdditionalBeanBuildItem(METRICS_SERVER_INTERCEPTOR,
"io.quarkus.grpc.runtime.metrics.GrpcMetricsClientInterceptor"));
additionalInterceptors
.produce(new AdditionalGlobalInterceptorBuildItem(METRICS_SERVER_INTERCEPTOR));
} else {
logger.warn("Only Micrometer-based metrics system is supported by quarkus-grpc");
log.warn("Only Micrometer-based metrics system is supported by quarkus-grpc");
}
}
}

private static class GrpcInterceptors {
final Set<String> globalInterceptors;
final Set<String> nonGlobalInterceptors;

GrpcInterceptors(Set<String> globalInterceptors, Set<String> nonGlobalInterceptors) {
this.globalInterceptors = globalInterceptors;
this.nonGlobalInterceptors = nonGlobalInterceptors;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@
import io.grpc.ServerCall;
import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.quarkus.grpc.GlobalInterceptor;

@ApplicationScoped
@GlobalInterceptor
public class DevModeTestInterceptor implements ServerInterceptor {

private volatile String lastStatus = "initial";
Expand All @@ -17,7 +19,7 @@ public class DevModeTestInterceptor implements ServerInterceptor {
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall,
Metadata metadata, ServerCallHandler<ReqT, RespT> serverCallHandler) {
return serverCallHandler
.startCall(new ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(serverCall) {
.startCall(new ForwardingServerCall.SimpleForwardingServerCall<>(serverCall) {
@Override
protected ServerCall<ReqT, RespT> delegate() {
lastStatus = getStatus();
Expand Down
Loading

0 comments on commit adf1cec

Please sign in to comment.