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

@@ -86,3 +86,23 @@ 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.
[[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;
}
}
}