From 2c9457d2a26250fade582ba7d24a811a78f274a9 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Tue, 1 Jul 2025 16:24:52 -0500 Subject: [PATCH] Allow filtering of global server interceptors This commit adds support for filtering the server-side global interceptors. Resolves #208 Signed-off-by: Chris Bono --- .../grpc/server/ServletGrpcServerFactory.java | 45 ++++++ .../service/DefaultGrpcServiceConfigurer.java | 27 +++- .../server/service/GrpcServiceConfigurer.java | 10 +- .../service/ServerInterceptorFilter.java | 43 ++++++ .../DefaultGrpcServiceConfigurerTests.java | 141 +++++++++++++----- .../antora/modules/ROOT/pages/server.adoc | 16 ++ .../server/GrpcServerAutoConfiguration.java | 7 +- .../GrpcServerFactoryAutoConfiguration.java | 3 +- .../GrpcServerFactoryConfigurations.java | 15 +- .../GrpcServerAutoConfigurationTests.java | 72 ++++++--- .../test/InProcessTestAutoConfiguration.java | 5 +- 11 files changed, 312 insertions(+), 72 deletions(-) create mode 100644 spring-grpc-core/src/main/java/org/springframework/grpc/server/ServletGrpcServerFactory.java create mode 100644 spring-grpc-core/src/main/java/org/springframework/grpc/server/service/ServerInterceptorFilter.java diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/ServletGrpcServerFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/ServletGrpcServerFactory.java new file mode 100644 index 00000000..cd936a15 --- /dev/null +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/ServletGrpcServerFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024-2025 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.server; + +import io.grpc.Server; +import io.grpc.ServerServiceDefinition; + +/** + * Marker interface for {@link GrpcServerFactory} that is to be handled by the servlet + * container. + * + * @author Chris Bono + */ +public class ServletGrpcServerFactory implements GrpcServerFactory { + + /** + * Default instance of marker interface. + */ + public static ServletGrpcServerFactory INSTANCE = new ServletGrpcServerFactory(); + + @Override + public Server createServer() { + throw new UnsupportedOperationException("Marker interface only"); + } + + @Override + public void addService(ServerServiceDefinition service) { + throw new UnsupportedOperationException("Marker interface only"); + } + +} diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/service/DefaultGrpcServiceConfigurer.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/service/DefaultGrpcServiceConfigurer.java index 572aaebc..d474cb73 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/server/service/DefaultGrpcServiceConfigurer.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/service/DefaultGrpcServiceConfigurer.java @@ -23,7 +23,9 @@ import org.springframework.context.ApplicationContext; import org.springframework.grpc.internal.ApplicationContextBeanLookupUtils; import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import io.grpc.BindableService; import io.grpc.ServerInterceptor; @@ -42,8 +44,12 @@ public class DefaultGrpcServiceConfigurer implements GrpcServiceConfigurer, Init private List globalInterceptors; - public DefaultGrpcServiceConfigurer(ApplicationContext applicationContext) { + private ServerInterceptorFilter interceptorFilter; + + public DefaultGrpcServiceConfigurer(ApplicationContext applicationContext, + @Nullable ServerInterceptorFilter interceptorFilter) { this.applicationContext = applicationContext; + this.interceptorFilter = interceptorFilter; } @Override @@ -52,8 +58,10 @@ public void afterPropertiesSet() { } @Override - public ServerServiceDefinition configure(ServerServiceDefinitionSpec serviceDefinitionSpec) { - return bindInterceptors(serviceDefinitionSpec.service(), serviceDefinitionSpec.serviceInfo()); + public ServerServiceDefinition configure(ServerServiceDefinitionSpec serviceSpec, GrpcServerFactory serverFactory) { + Assert.notNull(serviceSpec, () -> "serviceSpec must not be null"); + Assert.notNull(serverFactory, () -> "serverFactory must not be null"); + return bindInterceptors(serviceSpec.service(), serviceSpec.serviceInfo(), serverFactory); } private List findGlobalInterceptors() { @@ -62,13 +70,18 @@ private List findGlobalInterceptors() { } private ServerServiceDefinition bindInterceptors(BindableService bindableService, - @Nullable GrpcServiceInfo serviceInfo) { + @Nullable GrpcServiceInfo serviceInfo, GrpcServerFactory serverFactory) { var serviceDef = bindableService.bindService(); + + // Add and filter global interceptors first + List allInterceptors = new ArrayList<>(this.globalInterceptors); + if (this.interceptorFilter != null) { + allInterceptors + .removeIf(interceptor -> !this.interceptorFilter.filter(interceptor, serviceDef, serverFactory)); + } if (serviceInfo == null) { - return ServerInterceptors.interceptForward(serviceDef, this.globalInterceptors); + return ServerInterceptors.interceptForward(serviceDef, allInterceptors); } - // Add global interceptors first - List allInterceptors = new ArrayList<>(this.globalInterceptors); // Add interceptors by type Arrays.stream(serviceInfo.interceptors()) .forEachOrdered( diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/service/GrpcServiceConfigurer.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/service/GrpcServiceConfigurer.java index f5391ad8..23f97189 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/server/service/GrpcServiceConfigurer.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/service/GrpcServiceConfigurer.java @@ -16,6 +16,8 @@ package org.springframework.grpc.server.service; +import org.springframework.grpc.server.GrpcServerFactory; + import io.grpc.ServerServiceDefinition; /** @@ -29,12 +31,14 @@ public interface GrpcServiceConfigurer { /** - * Configure and bind a gRPC service spec resulting in a service definition that can + * Configure and bind a gRPC server spec resulting in a service definition that can * then be added to a gRPC server. * @param serviceSpec the spec containing the info about the service - * @return bound and configured service definition that is ready to be added to the + * @param serverFactory the factory that provides the server that the service will be + * added to + * @return bound and configured service definition that is ready to be added to a * server */ - ServerServiceDefinition configure(ServerServiceDefinitionSpec serviceSpec); + ServerServiceDefinition configure(ServerServiceDefinitionSpec serviceSpec, GrpcServerFactory serverFactory); } diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/service/ServerInterceptorFilter.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/service/ServerInterceptorFilter.java new file mode 100644 index 00000000..1aef0d49 --- /dev/null +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/service/ServerInterceptorFilter.java @@ -0,0 +1,43 @@ +/* + * Copyright 2023-2025 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.server.service; + +import org.springframework.grpc.server.GrpcServerFactory; + +import io.grpc.ServerInterceptor; +import io.grpc.ServerServiceDefinition; + +/** + * Strategy to determine whether a global {@link ServerInterceptor server interceptor} + * should be applied to {@link ServerServiceDefinition gRPC service}. + * + * @author Chris Bono + */ +@FunctionalInterface +public interface ServerInterceptorFilter { + + /** + * Determine whether an interceptor should be applied to a service when the service is + * running on a server provided by the given server factory. + * @param interceptor the server interceptor under consideration. + * @param service the service being added. + * @param serverFactory the server factory in use. + * @return {@code true} if the interceptor should be included; {@code false} + * otherwise. + */ + boolean filter(ServerInterceptor interceptor, ServerServiceDefinition service, GrpcServerFactory serverFactory); + +} diff --git a/spring-grpc-core/src/test/java/org/springframework/grpc/server/service/DefaultGrpcServiceConfigurerTests.java b/spring-grpc-core/src/test/java/org/springframework/grpc/server/service/DefaultGrpcServiceConfigurerTests.java index 432950aa..67daf473 100644 --- a/spring-grpc-core/src/test/java/org/springframework/grpc/server/service/DefaultGrpcServiceConfigurerTests.java +++ b/spring-grpc-core/src/test/java/org/springframework/grpc/server/service/DefaultGrpcServiceConfigurerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 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. @@ -16,9 +16,12 @@ package org.springframework.grpc.server.service; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.util.List; import java.util.function.Function; @@ -40,6 +43,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.grpc.server.GlobalServerInterceptor; +import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; import org.springframework.lang.Nullable; @@ -73,21 +77,24 @@ void globalServerInterceptorsAreFoundInProperOrder() { } private void customizeContextAndRunServiceConfigurerWithServiceInfo( - Function contextCustomizer, GrpcServiceInfo serviceInfo, + Function contextCustomizer, + GrpcServerFactory serverFactory, GrpcServiceInfo serviceInfo, List expectedInterceptors) { - this.customizeContextAndRunServiceConfigurerWithServiceInfo(contextCustomizer, serviceInfo, + this.doCustomizeContextAndRunServiceConfigurerWithServiceInfo(contextCustomizer, serverFactory, serviceInfo, expectedInterceptors, null); } - private void customizeContextAndRunServiceConfigurerWithServiceInfo( - Function contextCustomizer, GrpcServiceInfo serviceInfo, + private void customizeContextAndRunServiceConfigurerWithServiceInfoExpectingException( + Function contextCustomizer, + GrpcServerFactory serverFactory, GrpcServiceInfo serviceInfo, Class expectedExceptionType) { - this.customizeContextAndRunServiceConfigurerWithServiceInfo(contextCustomizer, serviceInfo, null, - expectedExceptionType); + this.doCustomizeContextAndRunServiceConfigurerWithServiceInfo(contextCustomizer, serverFactory, serviceInfo, + null, expectedExceptionType); } - private void customizeContextAndRunServiceConfigurerWithServiceInfo( - Function contextCustomizer, GrpcServiceInfo serviceInfo, + private void doCustomizeContextAndRunServiceConfigurerWithServiceInfo( + Function contextCustomizer, + GrpcServerFactory serverFactory, GrpcServiceInfo serviceInfo, @Nullable List expectedInterceptors, @Nullable Class expectedExceptionType) { // It gets difficult to verify interceptors are added properly to mocked services. @@ -99,22 +106,22 @@ private void customizeContextAndRunServiceConfigurerWithServiceInfo( serverInterceptorsMocked .when(() -> ServerInterceptors.interceptForward(any(ServerServiceDefinition.class), anyList())) .thenAnswer((Answer) invocation -> invocation.getArgument(0)); - BindableService service = Mockito.mock(); - ServerServiceDefinition serviceDef = Mockito.mock(); - Mockito.when(service.bindService()).thenReturn(serviceDef); + BindableService service = mock(); + ServerServiceDefinition serviceDef = mock(); + when(service.bindService()).thenReturn(serviceDef); this.contextRunner() .withBean("service", BindableService.class, () -> service) .with(contextCustomizer) .run((context) -> { DefaultGrpcServiceConfigurer configurer = context.getBean(DefaultGrpcServiceConfigurer.class); if (expectedExceptionType != null) { - assertThatThrownBy( - () -> configurer.configure(new ServerServiceDefinitionSpec(service, serviceInfo))) + assertThatThrownBy(() -> configurer + .configure(new ServerServiceDefinitionSpec(service, serviceInfo), serverFactory)) .isInstanceOf(expectedExceptionType); serverInterceptorsMocked.verifyNoInteractions(); } else { - configurer.configure(new ServerServiceDefinitionSpec(service, serviceInfo)); + configurer.configure(new ServerServiceDefinitionSpec(service, serviceInfo), serverFactory); serverInterceptorsMocked .verify(() -> ServerInterceptors.interceptForward(serviceDef, expectedInterceptors)); } @@ -122,19 +129,39 @@ private void customizeContextAndRunServiceConfigurerWithServiceInfo( } } + @Test + void whenNoServerFactoryThenThrowsException() { + this.contextRunner().run((context) -> { + var configurer = context.getBean(DefaultGrpcServiceConfigurer.class); + ServerServiceDefinitionSpec serviceSpec = mock(); + assertThatIllegalArgumentException().isThrownBy(() -> configurer.configure(serviceSpec, null)) + .withMessage("serverFactory must not be null"); + }); + } + + @Test + void whenNoServiceSpecThenThrowsException() { + this.contextRunner().run((context) -> { + var configurer = context.getBean(DefaultGrpcServiceConfigurer.class); + GrpcServerFactory serverFactory = mock(); + assertThatIllegalArgumentException().isThrownBy(() -> configurer.configure(null, serverFactory)) + .withMessage("serviceSpec must not be null"); + }); + } + @Nested class WithNoServiceInfoSpecified { @Test void whenNoGlobalInterceptorsRegisteredThenServiceGetsNoInterceptors() { - customizeContextAndRunServiceConfigurerWithServiceInfo(Function.identity(), null, List.of()); + customizeContextAndRunServiceConfigurerWithServiceInfo(Function.identity(), mock(), null, List.of()); } @Test void whenGlobalInterceptorsRegisteredThenServiceGetsGlobalInterceptors() { customizeContextAndRunServiceConfigurerWithServiceInfo( - (contextRunner) -> contextRunner.withUserConfiguration(GlobalServerInterceptorsConfig.class), null, - List.of(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR, + (contextRunner) -> contextRunner.withUserConfiguration(GlobalServerInterceptorsConfig.class), + mock(), null, List.of(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR, GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO)); } @@ -149,22 +176,22 @@ void whenSingleBeanOfInterceptorTypeRegisteredThenItIsUsed() { List expectedInterceptors = List.of(ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A); customizeContextAndRunServiceConfigurerWithServiceInfo( (contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class), - serviceInfo, expectedInterceptors); + mock(), serviceInfo, expectedInterceptors); } @Test void whenMultipleBeansOfInterceptorTypeRegisteredThenThrowsException() { GrpcServiceInfo serviceInfo = GrpcServiceInfo.withInterceptors(List.of(ServerInterceptor.class)); - customizeContextAndRunServiceConfigurerWithServiceInfo( + customizeContextAndRunServiceConfigurerWithServiceInfoExpectingException( (contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class), - serviceInfo, NoUniqueBeanDefinitionException.class); + mock(), serviceInfo, NoUniqueBeanDefinitionException.class); } @Test void whenNoBeanOfInterceptorTypeRegisteredThenThrowsException() { GrpcServiceInfo serviceInfo = GrpcServiceInfo.withInterceptors(List.of(ServerInterceptor.class)); - customizeContextAndRunServiceConfigurerWithServiceInfo(Function.identity(), serviceInfo, - NoSuchBeanDefinitionException.class); + customizeContextAndRunServiceConfigurerWithServiceInfoExpectingException(Function.identity(), mock(), + serviceInfo, NoSuchBeanDefinitionException.class); } } @@ -180,7 +207,7 @@ void whenSingleBeanOfEachInterceptorTypeRegisteredThenTheyAreUsed() { ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A); customizeContextAndRunServiceConfigurerWithServiceInfo( (contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class), - serviceInfo, expectedInterceptors); + mock(), serviceInfo, expectedInterceptors); } } @@ -194,14 +221,14 @@ void whenSingleBeanWithInterceptorNameRegisteredThenItIsUsed() { List expectedInterceptors = List.of(ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_B); customizeContextAndRunServiceConfigurerWithServiceInfo( (contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class), - serviceInfo, expectedInterceptors); + mock(), serviceInfo, expectedInterceptors); } @Test void whenNoBeanWithInterceptorNameRegisteredThenThrowsException() { GrpcServiceInfo serviceInfo = GrpcServiceInfo.withInterceptorNames(List.of("interceptor1")); - customizeContextAndRunServiceConfigurerWithServiceInfo(Function.identity(), serviceInfo, - NoSuchBeanDefinitionException.class); + customizeContextAndRunServiceConfigurerWithServiceInfoExpectingException(Function.identity(), mock(), + serviceInfo, NoSuchBeanDefinitionException.class); } } @@ -216,7 +243,7 @@ void whenSingleBeanWithEachInterceptorNameRegisteredThenTheyAreUsed() { ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A); customizeContextAndRunServiceConfigurerWithServiceInfo( (contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class), - serviceInfo, expectedInterceptors); + mock(), serviceInfo, expectedInterceptors); } } @@ -233,7 +260,7 @@ void whenSingleBeanOfEachAvailableThenTheyAreBothUsed() { ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A); customizeContextAndRunServiceConfigurerWithServiceInfo( (contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class), - serviceInfo, expectedInterceptors); + mock(), serviceInfo, expectedInterceptors); } } @@ -252,7 +279,7 @@ void whenBlendInterceptorsFalseThenGlobalInterceptorsAddedFirst() { ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A); customizeContextAndRunServiceConfigurerWithServiceInfo((contextRunner) -> contextRunner .withUserConfiguration(GlobalServerInterceptorsConfig.class, ServiceSpecificInterceptorsConfig.class), - serviceInfo, expectedInterceptors); + mock(), serviceInfo, expectedInterceptors); } @SuppressWarnings("unchecked") @@ -267,7 +294,44 @@ void whenBlendInterceptorsTrueThenGlobalInterceptorsBlended() { ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A); customizeContextAndRunServiceConfigurerWithServiceInfo((contextRunner) -> contextRunner .withUserConfiguration(GlobalServerInterceptorsConfig.class, ServiceSpecificInterceptorsConfig.class), - serviceInfo, expectedInterceptors); + mock(), serviceInfo, expectedInterceptors); + } + + } + + @Nested + class WithInterceptorFilters { + + @Test + void whenFilterIncludesOneInterceptorThenItIsAddedToServiceInfo() { + var serviceInfo = GrpcServiceInfo.withInterceptors(List.of(TestServerInterceptorA.class)); + var factory = mock(GrpcServerFactory.class); + ServerInterceptorFilter interceptorFilter = (interceptor, __, + serverFactory) -> (interceptor == GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR + && serverFactory == factory); + var expectedInterceptors = List.of(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR, + ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A); + customizeContextAndRunServiceConfigurerWithServiceInfo((contextRunner) -> contextRunner + .withBean(ServerInterceptorFilter.class, () -> interceptorFilter) + .withUserConfiguration(GlobalServerInterceptorsConfig.class, ServiceSpecificInterceptorsConfig.class), + factory, serviceInfo, expectedInterceptors); + } + + @Test + void whenFilterIncludesAllInterceptorsThenTheyAreAllAddedToServiceInfo() { + var serviceInfo = GrpcServiceInfo.withInterceptors(List.of(TestServerInterceptorA.class)); + var factory = mock(GrpcServerFactory.class); + ServerInterceptorFilter interceptorFilter = (interceptor, __, + serverFactory) -> ((interceptor == GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR + || interceptor == GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO) + && serverFactory == factory); + var expectedInterceptors = List.of(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR, + GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO, + ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A); + customizeContextAndRunServiceConfigurerWithServiceInfo((contextRunner) -> contextRunner + .withBean(ServerInterceptorFilter.class, () -> interceptorFilter) + .withUserConfiguration(GlobalServerInterceptorsConfig.class, ServiceSpecificInterceptorsConfig.class), + factory, serviceInfo, expectedInterceptors); } } @@ -284,8 +348,9 @@ interface TestServerInterceptorB extends ServerInterceptor { static class ServiceConfigurerConfig { @Bean - GrpcServiceConfigurer grpcServiceConfigurer(ApplicationContext applicationContext) { - return new DefaultGrpcServiceConfigurer(applicationContext); + GrpcServiceConfigurer grpcServiceConfigurer(ApplicationContext applicationContext, + @Nullable ServerInterceptorFilter interceptorFilter) { + return new DefaultGrpcServiceConfigurer(applicationContext, interceptorFilter); } } @@ -293,11 +358,11 @@ GrpcServiceConfigurer grpcServiceConfigurer(ApplicationContext applicationContex @Configuration(proxyBeanMethods = false) static class GlobalServerInterceptorsConfig { - static ServerInterceptor GLOBAL_INTERCEPTOR_FOO = Mockito.mock(); + static ServerInterceptor GLOBAL_INTERCEPTOR_FOO = mock(); - static ServerInterceptor GLOBAL_INTERCEPTOR_IGNORED = Mockito.mock(); + static ServerInterceptor GLOBAL_INTERCEPTOR_IGNORED = mock(); - static ServerInterceptor GLOBAL_INTERCEPTOR_BAR = Mockito.mock(); + static ServerInterceptor GLOBAL_INTERCEPTOR_BAR = mock(); @Bean @Order(200) @@ -324,9 +389,9 @@ ServerInterceptor globalInterceptorBar() { @Configuration(proxyBeanMethods = false) static class ServiceSpecificInterceptorsConfig { - static TestServerInterceptorB SVC_INTERCEPTOR_B = Mockito.mock(); + static TestServerInterceptorB SVC_INTERCEPTOR_B = mock(); - static TestServerInterceptorA SVC_INTERCEPTOR_A = Mockito.mock(); + static TestServerInterceptorA SVC_INTERCEPTOR_A = mock(); @Bean @Order(150) diff --git a/spring-grpc-docs/src/main/antora/modules/ROOT/pages/server.adoc b/spring-grpc-docs/src/main/antora/modules/ROOT/pages/server.adoc index 1c8fcf82..08c1ee54 100644 --- a/spring-grpc-docs/src/main/antora/modules/ROOT/pages/server.adoc +++ b/spring-grpc-docs/src/main/antora/modules/ROOT/pages/server.adoc @@ -119,6 +119,22 @@ ServerInterceptor myGlobalLoggingInterceptor() { } ---- +[[global-server-interceptor-filtering]] +==== Filtering +All global interceptors are applied to all created services by default. +However, you can register a `ServerInterceptorFilter` bean to decide which interceptors are applied to which server factories. + +The following example prevents the `ExtraThingsInterceptor` interceptor from being applied to any servers created by the `InProcessGrpcServerFactory` server factory. + +[source,java] +---- +@Bean +ServerInterceptorFilter myInterceptorFilter() { + return (interceptor, service, serverFactory) -> + !(interceptor instanceof ExtraThingsInterceptor && serverFactory instanceof InProcessGrpcServerFactory); +} +---- + === Per-Service To add a server interceptor to be applied to a single service you can simply register a server interceptor bean and then annotate your `BindableService` bean with `@GrpcService`, specifying the interceptor using either the `interceptors` or `interceptorNames` attribute. diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfiguration.java index 53c3d9bd..7e424c2e 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfiguration.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfiguration.java @@ -33,6 +33,8 @@ import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer; import org.springframework.grpc.server.service.GrpcServiceConfigurer; import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.grpc.server.service.ServerInterceptorFilter; +import org.springframework.lang.Nullable; import io.grpc.BindableService; import io.grpc.CompressorRegistry; @@ -69,8 +71,9 @@ ServerBuilderCustomizers serverBuilderCustomizers(ObjectProvider grpcServlet(GrpcServerProperties pro ServletServerBuilder servletServerBuilder = new ServletServerBuilder(); serviceDiscoverer.findServices() .stream() - .map(serviceConfigurer::configure) + .map((serviceSpec) -> serviceConfigurer.configure(serviceSpec, ServletGrpcServerFactory.INSTANCE)) .forEach(servletServerBuilder::addService); PropertyMapper mapper = PropertyMapper.get().alwaysApplyingWhenNonNull(); mapper.from(properties.getMaxInboundMessageSize()) diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryConfigurations.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryConfigurations.java index edebcad5..fe4da72a 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryConfigurations.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerFactoryConfigurations.java @@ -76,7 +76,10 @@ ShadedNettyGrpcServerFactory shadedNettyGrpcServerFactory(GrpcServerProperties p } ShadedNettyGrpcServerFactory factory = new ShadedNettyGrpcServerFactory(properties.getAddress(), builderCustomizers, keyManager, trustManager, properties.getSsl().getClientAuth()); - serviceDiscoverer.findServices().stream().map(serviceConfigurer::configure).forEach(factory::addService); + serviceDiscoverer.findServices() + .stream() + .map((serviceSpec) -> serviceConfigurer.configure(serviceSpec, factory)) + .forEach(factory::addService); return factory; } @@ -115,7 +118,10 @@ NettyGrpcServerFactory nettyGrpcServerFactory(GrpcServerProperties properties, } NettyGrpcServerFactory factory = new NettyGrpcServerFactory(properties.getAddress(), builderCustomizers, keyManager, trustManager, properties.getSsl().getClientAuth()); - serviceDiscoverer.findServices().stream().map(serviceConfigurer::configure).forEach(factory::addService); + serviceDiscoverer.findServices() + .stream() + .map((serviceSpec) -> serviceConfigurer.configure(serviceSpec, factory)) + .forEach(factory::addService); return factory; } @@ -145,7 +151,10 @@ InProcessGrpcServerFactory inProcessGrpcServerFactory(GrpcServerProperties prope .of(mapper::customizeServerBuilder, serverBuilderCustomizers::customize); InProcessGrpcServerFactory factory = new InProcessGrpcServerFactory(properties.getInprocess().getName(), builderCustomizers); - serviceDiscoverer.findServices().stream().map(serviceConfigurer::configure).forEach(factory::addService); + serviceDiscoverer.findServices() + .stream() + .map((serviceSpec) -> serviceConfigurer.configure(serviceSpec, factory)) + .forEach(factory::addService); return factory; } diff --git a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfigurationTests.java b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfigurationTests.java index 7249654c..2f7fd6c3 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfigurationTests.java +++ b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfigurationTests.java @@ -29,6 +29,7 @@ import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.InOrder; import org.mockito.MockedStatic; @@ -54,6 +55,7 @@ import org.springframework.grpc.server.service.DefaultGrpcServiceDiscoverer; import org.springframework.grpc.server.service.GrpcServiceConfigurer; import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.grpc.server.service.ServerInterceptorFilter; import io.grpc.BindableService; import io.grpc.Grpc; @@ -152,23 +154,6 @@ void grpcServiceDiscovererAutoConfiguredAsExpected() { .isInstanceOf(DefaultGrpcServiceDiscoverer.class)); } - @Test - void whenHasUserDefinedGrpcServiceConfigurerDoesNotAutoConfigureBean() { - GrpcServiceConfigurer customGrpcServiceConfigurer = mock(GrpcServiceConfigurer.class); - this.contextRunner() - .withBean("customGrpcServiceConfigurer", GrpcServiceConfigurer.class, () -> customGrpcServiceConfigurer) - .run((context) -> assertThat(context).getBean(GrpcServiceConfigurer.class) - .isSameAs(customGrpcServiceConfigurer)); - } - - @Test - void grpcServiceConfigurerAutoConfiguredAsExpected() { - this.contextRunnerWithLifecyle() - .withPropertyValues("spring.grpc.server.port=0") - .run((context) -> assertThat(context).getBean(GrpcServiceConfigurer.class) - .isInstanceOf(DefaultGrpcServiceConfigurer.class)); - } - @Test void whenHasUserDefinedServerBuilderCustomizersDoesNotAutoConfigureBean() { ServerBuilderCustomizers customCustomizers = mock(ServerBuilderCustomizers.class); @@ -407,6 +392,59 @@ void nettyServerFactoryAutoConfiguredWithSsl() { NettyGrpcServerFactory.class, "myhost:6160", "nettyGrpcServerLifecycle"); } + @Nested + class WithGrpcServiceConfigurerAutoConfig { + + @Test + void whenHasUserDefinedBeanDoesNotAutoConfigureBean() { + GrpcServiceConfigurer customGrpcServiceConfigurer = mock(GrpcServiceConfigurer.class); + GrpcServerAutoConfigurationTests.this.contextRunner() + .withBean("customGrpcServiceConfigurer", GrpcServiceConfigurer.class, () -> customGrpcServiceConfigurer) + .run((context) -> assertThat(context).getBean(GrpcServiceConfigurer.class) + .isSameAs(customGrpcServiceConfigurer)); + } + + @Test + void configurerAutoConfiguredAsExpected() { + GrpcServerAutoConfigurationTests.this.contextRunnerWithLifecyle() + .withPropertyValues("spring.grpc.server.port=0") + .run((context) -> assertThat(context).getBean(GrpcServiceConfigurer.class) + .isInstanceOf(DefaultGrpcServiceConfigurer.class)); + } + + @Test + void whenNoServerInterceptorFilterThenConfigurerUsesNoFilter() { + GrpcServerAutoConfigurationTests.this.contextRunnerWithLifecyle() + .withPropertyValues("spring.grpc.server.port=0") + .run((context) -> assertThat(context).getBean(GrpcServiceConfigurer.class) + .extracting("interceptorFilter") + .isNull()); + } + + @Test + void whenUniqueServerInterceptorFilterThenConfigurerUsesFilter() { + ServerInterceptorFilter interceptorFilter = mock(); + GrpcServerAutoConfigurationTests.this.contextRunnerWithLifecyle() + .withPropertyValues("spring.grpc.server.port=0") + .withBean(ServerInterceptorFilter.class, () -> interceptorFilter) + .run((context) -> assertThat(context).getBean(GrpcServiceConfigurer.class) + .extracting("interceptorFilter") + .isSameAs(interceptorFilter)); + } + + @Test + void whenMultipleServerInterceptorFiltersThenThrowsException() { + GrpcServerAutoConfigurationTests.this.contextRunnerWithLifecyle() + .withPropertyValues("spring.grpc.server.port=0") + .withBean("filter1", ServerInterceptorFilter.class, Mockito::mock) + .withBean("filter2", ServerInterceptorFilter.class, Mockito::mock) + .run((context) -> assertThat(context).hasFailed() + .getFailure() + .hasMessageContaining("expected single matching bean but found 2: filter1,filter2")); + } + + } + @Configuration(proxyBeanMethods = false) static class ServerBuilderCustomizersConfig { diff --git a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessTestAutoConfiguration.java b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessTestAutoConfiguration.java index 359dd796..8c7ab4a6 100644 --- a/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessTestAutoConfiguration.java +++ b/spring-grpc-test/src/main/java/org/springframework/grpc/test/InProcessTestAutoConfiguration.java @@ -58,7 +58,10 @@ TestInProcessGrpcServerFactory testInProcessGrpcServerFactory(GrpcServiceDiscove GrpcServiceConfigurer serviceConfigurer, List> customizers) { var factory = new TestInProcessGrpcServerFactory(address, customizers); - serviceDiscoverer.findServices().stream().map(serviceConfigurer::configure).forEach(factory::addService); + serviceDiscoverer.findServices() + .stream() + .map((serviceSpec) -> serviceConfigurer.configure(serviceSpec, factory)) + .forEach(factory::addService); return factory; }