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:
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user