Add GlobalServerInterceptor to service discovery

This commit builds upon the previously added service discoverer
by adding support for finding all server interceptor beans that
are marked w/ the newly added `@GlobalServerInterceptor`
annotation.

Resolves #4

Signed-off-by: Chris Bono <chris.bono@gmail.com>
This commit is contained in:
Chris Bono
2024-11-06 11:15:28 -06:00
committed by Dave Syer
parent daf8e046c3
commit 4daf41392a
5 changed files with 255 additions and 9 deletions

View File

@@ -85,4 +85,24 @@ dependencies {
The `spring.grpc.server.*` properties will be ignored in facour of the regular `server.*` properties in this case.
The servlet that is created is mapped to process HTTP POST requests to the paths defined by the registered services, as `/<service-name>/*`.
Clients can connect to the server using that path, which is what any gRPC client library will do.
Clients can connect to the server using that path, which is what any gRPC client library will do.
[[server-interceptor]]
== Server Interceptors
=== Global
To add a server interceptor to be applied to all services you can simply register a server interceptor bean and then annotate it with `@GlobalServerInterceptor`.
The interceptors are ordered according to their bean natural ordering (i.e. `@Order`).
[source,java]
----
@Bean
@Order(100)
@GlobalServerInterceptor
ServerInterceptor myGlobalLoggingInterceptor() {
return new MyLoggingInterceptor();
}
----
=== Per-Service
This is a **WIP** and will be available soon.

View File

@@ -16,35 +16,58 @@
package org.springframework.grpc.autoconfigure.server;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import io.grpc.BindableService;
import io.grpc.ServerInterceptor;
import io.grpc.ServerInterceptors;
import io.grpc.ServerServiceDefinition;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.context.ApplicationContext;
import org.springframework.grpc.server.GrpcServiceDiscoverer;
/**
* The default {@link GrpcServiceDiscoverer} that finds all {@link BindableService} beans
* and configures and binds them.
*
* @author Michael (yidongnan@gmail.com)
* @author Chris Bono
*/
class DefaultGrpcServiceDiscoverer implements GrpcServiceDiscoverer {
private final ObjectProvider<BindableService> grpcServicesProvider;
DefaultGrpcServiceDiscoverer(ObjectProvider<BindableService> grpcServicesProvider) {
private final ObjectProvider<ServerInterceptor> serverInterceptorsProvider;
private final ApplicationContext applicationContext;
public DefaultGrpcServiceDiscoverer(ObjectProvider<BindableService> grpcServicesProvider,
ObjectProvider<ServerInterceptor> serverInterceptorsProvider, ApplicationContext applicationContext) {
this.grpcServicesProvider = grpcServicesProvider;
this.serverInterceptorsProvider = serverInterceptorsProvider;
this.applicationContext = applicationContext;
}
@Override
public List<ServerServiceDefinition> findServices() {
List<ServerServiceDefinition> list = new ArrayList<>(
grpcServicesProvider.orderedStream().map(BindableService::bindService).toList());
return list;
List<ServerInterceptor> globalInterceptors = findGlobalInterceptors();
return grpcServicesProvider.orderedStream()
.map(BindableService::bindService)
.map((svc) -> ServerInterceptors.interceptForward(svc, globalInterceptors))
.toList();
}
// VisibleForTesting
List<ServerInterceptor> findGlobalInterceptors() {
// We find unordered map of beans (keyed by name) with the annotation and then
// reverse the map for easy lookup by bean.
// We then get an ordered stream of all server interceptors and filter
// out those that are not present in map of annotated interceptor beans.
var nameToBeanMap = applicationContext.getBeansWithAnnotation(GlobalServerInterceptor.class);
var beanToNameMap = new HashMap<Object, String>();
nameToBeanMap.forEach((name, bean) -> beanToNameMap.put(bean, name));
return this.serverInterceptorsProvider.orderedStream().filter(beanToNameMap::containsKey).toList();
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2024-2024 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.grpc.autoconfigure.server;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import io.grpc.ServerInterceptor;
import org.springframework.core.annotation.Order;
/**
* Annotation that can be specified on a gRPC {@link ServerInterceptor} bean which will
* result in the interceptor being applied globally to all services.
* <p>
* The bean interceptor {@link Order} will be respected.
*
* @author Daniel Theuke (daniel.theuke@heuboe.de)
* @author Chris Bono
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface GlobalServerInterceptor {
}

View File

@@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
@@ -36,6 +37,7 @@ import io.grpc.BindableService;
import io.grpc.CompressorRegistry;
import io.grpc.DecompressorRegistry;
import io.grpc.ServerBuilder;
import io.grpc.ServerInterceptor;
/**
* {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side components.
@@ -75,8 +77,10 @@ public class GrpcServerAutoConfiguration {
@ConditionalOnMissingBean
@Bean
GrpcServiceDiscoverer grpcServiceDiscoverer(ObjectProvider<BindableService> bindableServicesProvider) {
return new DefaultGrpcServiceDiscoverer(bindableServicesProvider);
GrpcServiceDiscoverer grpcServiceDiscoverer(ObjectProvider<BindableService> bindableServicesProvider,
ObjectProvider<ServerInterceptor> serverInterceptorsProvider, ApplicationContext applicationContext) {
return new DefaultGrpcServiceDiscoverer(bindableServicesProvider, serverInterceptorsProvider,
applicationContext);
}
@ConditionalOnBean(CompressorRegistry.class)

View File

@@ -0,0 +1,156 @@
/*
* Copyright 2023-2024 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.grpc.autoconfigure.server;
import java.util.List;
import io.grpc.BindableService;
import io.grpc.ServerInterceptor;
import io.grpc.ServerInterceptors;
import io.grpc.ServerServiceDefinition;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.stubbing.Answer;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.when;
/**
* Tests for {@link DefaultGrpcServiceDiscoverer}.
*
* @author Chris Bono
*/
class DefaultGrpcServiceDiscovererTests {
private ApplicationContextRunner contextRunner() {
// NOTE: we use noop server lifecycle to avoid startup
return new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class))
.withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock);
}
@Test
void globalServerInterceptorsAreFoundInProperOrder() {
this.contextRunner()
.withUserConfiguration(GlobalServerInterceptorsConfig.class)
.run((context) -> assertThat(context).getBean(DefaultGrpcServiceDiscoverer.class)
.extracting(DefaultGrpcServiceDiscoverer::findGlobalInterceptors, InstanceOfAssertFactories.LIST)
.containsExactly(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO));
}
@Test
void servicesAreFoundInProperOrderWithGlobalInterceptorsApplied() {
// It gets difficult to verify interceptors are added properly to mocked services.
// To make it easier, we just static mock ServerInterceptors.interceptForward to
// echo back the service def. This way we can verify the interceptors were passed
// in the proper order as we rely/trust that ServerInterceptors.interceptForward
// is
// tested well in grpc-java.
try (MockedStatic<ServerInterceptors> serverInterceptorsMocked = Mockito.mockStatic(ServerInterceptors.class)) {
serverInterceptorsMocked
.when(() -> ServerInterceptors.interceptForward(any(ServerServiceDefinition.class), anyList()))
.thenAnswer((Answer<ServerServiceDefinition>) invocation -> invocation.getArgument(0));
this.contextRunner().withUserConfiguration(GlobalServerInterceptorsConfig.class).run((context) -> {
assertThat(context).getBean(DefaultGrpcServiceDiscoverer.class)
.extracting(DefaultGrpcServiceDiscoverer::findServices, InstanceOfAssertFactories.LIST)
.containsExactly(GlobalServerInterceptorsConfig.SERVICE_DEF_B,
GlobalServerInterceptorsConfig.SERVICE_DEF_A);
ArgumentCaptor<ServerServiceDefinition> serviceDefArg = ArgumentCaptor.captor();
ArgumentCaptor<List<ServerInterceptor>> interceptorsArg = ArgumentCaptor.captor();
serverInterceptorsMocked.verify(
() -> ServerInterceptors.interceptForward(serviceDefArg.capture(), interceptorsArg.capture()),
times(2));
assertThat(serviceDefArg.getAllValues()).containsExactly(GlobalServerInterceptorsConfig.SERVICE_DEF_B,
GlobalServerInterceptorsConfig.SERVICE_DEF_A);
assertThat(interceptorsArg.getAllValues()).containsExactly(
List.of(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO),
List.of(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO));
});
}
}
@Configuration(proxyBeanMethods = false)
static class GlobalServerInterceptorsConfig {
static BindableService SERVICE_A = mock();
static ServerServiceDefinition SERVICE_DEF_A = mock();
static BindableService SERVICE_B = mock();
static ServerServiceDefinition SERVICE_DEF_B = mock();
static ServerInterceptor GLOBAL_INTERCEPTOR_FOO = mock();
static ServerInterceptor GLOBAL_INTERCEPTOR_IGNORED = mock();
static ServerInterceptor GLOBAL_INTERCEPTOR_BAR = mock();
@Bean
@Order(200)
BindableService serviceA() {
when(SERVICE_A.bindService()).thenReturn(SERVICE_DEF_A);
return SERVICE_A;
}
@Bean
@Order(100)
BindableService serviceB() {
when(SERVICE_B.bindService()).thenReturn(SERVICE_DEF_B);
return SERVICE_B;
}
@Bean
@Order(200)
@GlobalServerInterceptor
ServerInterceptor globalInterceptorFoo() {
return GLOBAL_INTERCEPTOR_FOO;
}
@Bean
@Order(150)
ServerInterceptor globalInterceptorIgnored() {
return GLOBAL_INTERCEPTOR_IGNORED;
}
@Bean
@Order(100)
@GlobalServerInterceptor
ServerInterceptor globalInterceptorBar() {
return GLOBAL_INTERCEPTOR_BAR;
}
}
}