From 2c9457d2a26250fade582ba7d24a811a78f274a9 Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Tue, 1 Jul 2025 16:24:52 -0500 Subject: [PATCH 1/3] 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; } From 0fccea188f9a1cae88e25f325cbd4c0c5dfb382b Mon Sep 17 00:00:00 2001 From: Andrey Litvitski Date: Tue, 1 Jul 2025 08:22:27 -0500 Subject: [PATCH 2/3] Allow filtering of services This commit adds the ability to filter out services from being bound to server factories. Resolves #207 Signed-off-by: Andrey Litvitski Signed-off-by: Chris Bono --- .../grpc/server/DefaultGrpcServerFactory.java | 14 +++++-- .../server/InProcessGrpcServerFactory.java | 8 +++- .../grpc/server/NettyGrpcServerFactory.java | 10 +++-- .../server/ServerServiceDefinitionFilter.java | 37 +++++++++++++++++++ .../server/ShadedNettyGrpcServerFactory.java | 10 +++-- .../GrpcServerFactoryConfigurations.java | 17 ++++++--- .../GrpcServerAutoConfigurationTests.java | 3 ++ .../test/InProcessTestAutoConfiguration.java | 12 ++++-- 8 files changed, 90 insertions(+), 21 deletions(-) create mode 100644 spring-grpc-core/src/main/java/org/springframework/grpc/server/ServerServiceDefinitionFilter.java diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/DefaultGrpcServerFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/DefaultGrpcServerFactory.java index 3b4646be..982f15f5 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/server/DefaultGrpcServerFactory.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/DefaultGrpcServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2024-2024 the original author or authors. + * 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. @@ -28,6 +28,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.grpc.internal.GrpcUtils; +import org.springframework.lang.Nullable; import com.google.common.collect.Lists; import io.grpc.Grpc; @@ -49,6 +50,7 @@ * @param the type of server builder * @author David Syer * @author Chris Bono + * @author Andrey Litvitski * @see ServerProvider#provider() */ public class DefaultGrpcServerFactory> implements GrpcServerFactory { @@ -68,17 +70,21 @@ public class DefaultGrpcServerFactory> implements Grp private ClientAuth clientAuth; + private ServerServiceDefinitionFilter serviceFilter; + public DefaultGrpcServerFactory(String address, List> serverBuilderCustomizers) { this.address = address; this.serverBuilderCustomizers = Objects.requireNonNull(serverBuilderCustomizers, "serverBuilderCustomizers"); } public DefaultGrpcServerFactory(String address, List> serverBuilderCustomizers, - KeyManagerFactory keyManager, TrustManagerFactory trustManager, ClientAuth clientAuth) { + KeyManagerFactory keyManager, TrustManagerFactory trustManager, ClientAuth clientAuth, + @Nullable ServerServiceDefinitionFilter serviceFilter) { this(address, serverBuilderCustomizers); this.keyManager = keyManager; this.trustManager = trustManager; this.clientAuth = clientAuth; + this.serviceFilter = serviceFilter; } protected String address() { @@ -94,7 +100,9 @@ public Server createServer() { @Override public void addService(ServerServiceDefinition service) { - this.serviceList.add(service); + if (this.serviceFilter == null || this.serviceFilter.filter(service)) { + this.serviceList.add(service); + } } /** diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/InProcessGrpcServerFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/InProcessGrpcServerFactory.java index cdbc4ca1..259a2e5a 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/server/InProcessGrpcServerFactory.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/InProcessGrpcServerFactory.java @@ -17,18 +17,22 @@ import java.util.List; +import org.springframework.lang.Nullable; + import io.grpc.inprocess.InProcessServerBuilder; /** * {@link GrpcServerFactory} that can be used to create an in-process gRPC server. * * @author Chris Bono + * @author Andrey Litvitski */ public class InProcessGrpcServerFactory extends DefaultGrpcServerFactory { public InProcessGrpcServerFactory(String address, - List> serverBuilderCustomizers) { - super(address, serverBuilderCustomizers); + List> serverBuilderCustomizers, + @Nullable ServerServiceDefinitionFilter serviceFilter) { + super(address, serverBuilderCustomizers, null, null, null, serviceFilter); } @Override diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/NettyGrpcServerFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/NettyGrpcServerFactory.java index 2e6c9c1f..67aefcbd 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/server/NettyGrpcServerFactory.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/NettyGrpcServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2024-2024 the original author or authors. + * 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. @@ -21,6 +21,8 @@ import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManagerFactory; +import org.springframework.lang.Nullable; + import io.grpc.TlsServerCredentials.ClientAuth; import io.grpc.netty.NettyServerBuilder; import io.netty.channel.epoll.EpollEventLoopGroup; @@ -32,13 +34,15 @@ * * @author David Syer * @author Chris Bono + * @author Andrey Litvitski */ public class NettyGrpcServerFactory extends DefaultGrpcServerFactory { public NettyGrpcServerFactory(String address, List> serverBuilderCustomizers, KeyManagerFactory keyManager, - TrustManagerFactory trustManager, ClientAuth clientAuth) { - super(address, serverBuilderCustomizers, keyManager, trustManager, clientAuth); + TrustManagerFactory trustManager, ClientAuth clientAuth, + @Nullable ServerServiceDefinitionFilter serviceFilter) { + super(address, serverBuilderCustomizers, keyManager, trustManager, clientAuth, serviceFilter); } @Override diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/ServerServiceDefinitionFilter.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/ServerServiceDefinitionFilter.java new file mode 100644 index 00000000..bf294471 --- /dev/null +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/ServerServiceDefinitionFilter.java @@ -0,0 +1,37 @@ +/* + * 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; + +import io.grpc.ServerServiceDefinition; + +/** + * Strategy to determine whether a {@link ServerServiceDefinition} should be included for + * the {@link GrpcServerFactory server factory}. + * + * @author Andrey Litvitski + */ +@FunctionalInterface +public interface ServerServiceDefinitionFilter { + + /** + * Determine whether the given {@link ServerServiceDefinition} should be included for + * the {@link GrpcServerFactory server factory}. + * @param serviceDefinition the gRPC service definition under consideration. + * @return {@code true} if the service should be included; {@code false} otherwise. + */ + boolean filter(ServerServiceDefinition serviceDefinition); + +} diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/ShadedNettyGrpcServerFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/ShadedNettyGrpcServerFactory.java index 3ad992a8..ac14202a 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/server/ShadedNettyGrpcServerFactory.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/ShadedNettyGrpcServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2024-2024 the original author or authors. + * 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. @@ -21,6 +21,8 @@ import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManagerFactory; +import org.springframework.lang.Nullable; + import io.grpc.TlsServerCredentials.ClientAuth; import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; import io.grpc.netty.shaded.io.netty.channel.epoll.EpollEventLoopGroup; @@ -32,13 +34,15 @@ * * @author David Syer * @author Chris Bono + * @author Andrey Litvitski */ public class ShadedNettyGrpcServerFactory extends DefaultGrpcServerFactory { public ShadedNettyGrpcServerFactory(String address, List> serverBuilderCustomizers, KeyManagerFactory keyManager, - TrustManagerFactory trustManager, ClientAuth clientAuth) { - super(address, serverBuilderCustomizers, keyManager, trustManager, clientAuth); + TrustManagerFactory trustManager, ClientAuth clientAuth, + @Nullable ServerServiceDefinitionFilter serviceFilter) { + super(address, serverBuilderCustomizers, keyManager, trustManager, clientAuth, serviceFilter); } @Override 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 fe4da72a..7e82ab93 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 @@ -30,11 +30,13 @@ import org.springframework.boot.ssl.SslBundles; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; +import org.springframework.lang.Nullable; import org.springframework.context.annotation.Configuration; import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.grpc.server.InProcessGrpcServerFactory; import org.springframework.grpc.server.NettyGrpcServerFactory; import org.springframework.grpc.server.ServerBuilderCustomizer; +import org.springframework.grpc.server.ServerServiceDefinitionFilter; import org.springframework.grpc.server.ShadedNettyGrpcServerFactory; import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; import org.springframework.grpc.server.service.GrpcServiceConfigurer; @@ -62,7 +64,8 @@ static class ShadedNettyServerFactoryConfiguration { @Bean ShadedNettyGrpcServerFactory shadedNettyGrpcServerFactory(GrpcServerProperties properties, GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer, - ServerBuilderCustomizers serverBuilderCustomizers, SslBundles bundles) { + ServerBuilderCustomizers serverBuilderCustomizers, SslBundles bundles, + @Nullable ServerServiceDefinitionFilter serviceFilter) { ShadedNettyServerFactoryPropertyMapper mapper = new ShadedNettyServerFactoryPropertyMapper(properties); List> builderCustomizers = List .of(mapper::customizeServerBuilder, serverBuilderCustomizers::customize); @@ -75,7 +78,7 @@ ShadedNettyGrpcServerFactory shadedNettyGrpcServerFactory(GrpcServerProperties p : io.grpc.netty.shaded.io.netty.handler.ssl.util.InsecureTrustManagerFactory.INSTANCE; } ShadedNettyGrpcServerFactory factory = new ShadedNettyGrpcServerFactory(properties.getAddress(), - builderCustomizers, keyManager, trustManager, properties.getSsl().getClientAuth()); + builderCustomizers, keyManager, trustManager, properties.getSsl().getClientAuth(), serviceFilter); serviceDiscoverer.findServices() .stream() .map((serviceSpec) -> serviceConfigurer.configure(serviceSpec, factory)) @@ -104,7 +107,8 @@ static class NettyServerFactoryConfiguration { @Bean NettyGrpcServerFactory nettyGrpcServerFactory(GrpcServerProperties properties, GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer, - ServerBuilderCustomizers serverBuilderCustomizers, SslBundles bundles) { + ServerBuilderCustomizers serverBuilderCustomizers, SslBundles bundles, + @Nullable ServerServiceDefinitionFilter serviceFilter) { NettyServerFactoryPropertyMapper mapper = new NettyServerFactoryPropertyMapper(properties); List> builderCustomizers = List .of(mapper::customizeServerBuilder, serverBuilderCustomizers::customize); @@ -117,7 +121,7 @@ NettyGrpcServerFactory nettyGrpcServerFactory(GrpcServerProperties properties, : InsecureTrustManagerFactory.INSTANCE; } NettyGrpcServerFactory factory = new NettyGrpcServerFactory(properties.getAddress(), builderCustomizers, - keyManager, trustManager, properties.getSsl().getClientAuth()); + keyManager, trustManager, properties.getSsl().getClientAuth(), serviceFilter); serviceDiscoverer.findServices() .stream() .map((serviceSpec) -> serviceConfigurer.configure(serviceSpec, factory)) @@ -145,12 +149,13 @@ static class InProcessServerFactoryConfiguration { @Bean InProcessGrpcServerFactory inProcessGrpcServerFactory(GrpcServerProperties properties, GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer, - ServerBuilderCustomizers serverBuilderCustomizers) { + ServerBuilderCustomizers serverBuilderCustomizers, + @Nullable ServerServiceDefinitionFilter serviceFilter) { var mapper = new InProcessServerFactoryPropertyMapper(properties); List> builderCustomizers = List .of(mapper::customizeServerBuilder, serverBuilderCustomizers::customize); InProcessGrpcServerFactory factory = new InProcessGrpcServerFactory(properties.getInprocess().getName(), - builderCustomizers); + builderCustomizers, serviceFilter); serviceDiscoverer.findServices() .stream() .map((serviceSpec) -> serviceConfigurer.configure(serviceSpec, 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 2f7fd6c3..82b68bf7 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 @@ -45,10 +45,12 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; +import org.springframework.grpc.server.DefaultGrpcServerFactory; import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.grpc.server.InProcessGrpcServerFactory; import org.springframework.grpc.server.NettyGrpcServerFactory; import org.springframework.grpc.server.ServerBuilderCustomizer; +import org.springframework.grpc.server.ServerServiceDefinitionFilter; import org.springframework.grpc.server.ShadedNettyGrpcServerFactory; import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; import org.springframework.grpc.server.service.DefaultGrpcServiceConfigurer; @@ -68,6 +70,7 @@ * Tests for {@link GrpcServerAutoConfiguration}. * * @author Chris Bono + * @author Andrey Litvitski */ class GrpcServerAutoConfigurationTests { 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 8c7ab4a6..8173d853 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 @@ -34,9 +34,11 @@ import org.springframework.grpc.client.InProcessGrpcChannelFactory; import org.springframework.grpc.server.InProcessGrpcServerFactory; import org.springframework.grpc.server.ServerBuilderCustomizer; +import org.springframework.grpc.server.ServerServiceDefinitionFilter; import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle; import org.springframework.grpc.server.service.GrpcServiceConfigurer; import org.springframework.grpc.server.service.GrpcServiceDiscoverer; +import org.springframework.lang.Nullable; import io.grpc.BindableService; import io.grpc.ChannelCredentials; @@ -56,8 +58,9 @@ public class InProcessTestAutoConfiguration { @Order(Ordered.HIGHEST_PRECEDENCE) TestInProcessGrpcServerFactory testInProcessGrpcServerFactory(GrpcServiceDiscoverer serviceDiscoverer, GrpcServiceConfigurer serviceConfigurer, - List> customizers) { - var factory = new TestInProcessGrpcServerFactory(address, customizers); + List> customizers, + @Nullable ServerServiceDefinitionFilter serviceFilter) { + var factory = new TestInProcessGrpcServerFactory(address, customizers, serviceFilter); serviceDiscoverer.findServices() .stream() .map((serviceSpec) -> serviceConfigurer.configure(serviceSpec, factory)) @@ -86,8 +89,9 @@ GrpcServerLifecycle inProcessGrpcServerLifecycle(InProcessGrpcServerFactory fact public static class TestInProcessGrpcServerFactory extends InProcessGrpcServerFactory { public TestInProcessGrpcServerFactory(String address, - List> serverBuilderCustomizers) { - super(address, serverBuilderCustomizers); + List> serverBuilderCustomizers, + @Nullable ServerServiceDefinitionFilter serviceFilter) { + super(address, serverBuilderCustomizers, serviceFilter); } } From 4cd7430bfb25d69a590c77b4d6267d5b24bf6e6c Mon Sep 17 00:00:00 2001 From: Chris Bono Date: Tue, 1 Jul 2025 20:17:48 -0500 Subject: [PATCH 3/3] Polish "Allow filtering of services" This commit updates some tests and the contract slightly of the previous commit to allow a serverFactory parameter as part of the inputs to the service filter and tests all variants of server factories via `@ParameterizedTest` See #207 Signed-off-by: Chris Bono --- .../grpc/server/DefaultGrpcServerFactory.java | 2 +- .../server/ServerServiceDefinitionFilter.java | 11 ++- .../server/DefaultGrpcServerFactoryTests.java | 81 +++++++++++++++++ .../grpc/server/GrpcServerFactoryTests.java | 30 ------ .../GrpcServerAutoConfigurationTests.java | 91 ++++++++++++++++++- .../test/InProcessTestAutoConfiguration.java | 3 +- 6 files changed, 176 insertions(+), 42 deletions(-) create mode 100644 spring-grpc-core/src/test/java/org/springframework/grpc/server/DefaultGrpcServerFactoryTests.java delete mode 100644 spring-grpc-core/src/test/java/org/springframework/grpc/server/GrpcServerFactoryTests.java diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/DefaultGrpcServerFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/DefaultGrpcServerFactory.java index 982f15f5..a66e6340 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/server/DefaultGrpcServerFactory.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/DefaultGrpcServerFactory.java @@ -100,7 +100,7 @@ public Server createServer() { @Override public void addService(ServerServiceDefinition service) { - if (this.serviceFilter == null || this.serviceFilter.filter(service)) { + if (this.serviceFilter == null || this.serviceFilter.filter(service, this)) { this.serviceList.add(service); } } diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/ServerServiceDefinitionFilter.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/ServerServiceDefinitionFilter.java index bf294471..b805f253 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/server/ServerServiceDefinitionFilter.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/ServerServiceDefinitionFilter.java @@ -18,8 +18,8 @@ import io.grpc.ServerServiceDefinition; /** - * Strategy to determine whether a {@link ServerServiceDefinition} should be included for - * the {@link GrpcServerFactory server factory}. + * Strategy to determine whether a {@link ServerServiceDefinition} should be added to a + * {@link GrpcServerFactory server factory}. * * @author Andrey Litvitski */ @@ -27,11 +27,12 @@ public interface ServerServiceDefinitionFilter { /** - * Determine whether the given {@link ServerServiceDefinition} should be included for - * the {@link GrpcServerFactory server factory}. + * Determine whether the given {@link ServerServiceDefinition} should be added to the + * given {@link GrpcServerFactory server factory}. * @param serviceDefinition the gRPC service definition under consideration. + * @param serverFactory the server factory in use. * @return {@code true} if the service should be included; {@code false} otherwise. */ - boolean filter(ServerServiceDefinition serviceDefinition); + boolean filter(ServerServiceDefinition serviceDefinition, GrpcServerFactory serverFactory); } diff --git a/spring-grpc-core/src/test/java/org/springframework/grpc/server/DefaultGrpcServerFactoryTests.java b/spring-grpc-core/src/test/java/org/springframework/grpc/server/DefaultGrpcServerFactoryTests.java new file mode 100644 index 00000000..20b1f792 --- /dev/null +++ b/spring-grpc-core/src/test/java/org/springframework/grpc/server/DefaultGrpcServerFactoryTests.java @@ -0,0 +1,81 @@ +/* + * 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.server; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import java.util.List; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import io.grpc.ServerServiceDefinition; + +/** + * Tests for {@link DefaultGrpcServerFactory}. + */ +class DefaultGrpcServerFactoryTests { + + @Nested + class WithServiceFilter { + + @Test + void whenNoFilterThenAllServicesAdded() { + ServerServiceDefinition serviceDef1 = mock(); + ServerServiceDefinition serviceDef2 = mock(); + DefaultGrpcServerFactory serverFactory = new DefaultGrpcServerFactory("myhost:5150", List.of(), null, null, + null, null); + serverFactory.addService(serviceDef2); + serverFactory.addService(serviceDef1); + assertThat(serverFactory) + .extracting("serviceList", InstanceOfAssertFactories.list(ServerServiceDefinition.class)) + .containsExactly(serviceDef2, serviceDef1); + } + + @Test + void whenFilterAllowsAllThenAllServicesAdded() { + ServerServiceDefinition serviceDef1 = mock(); + ServerServiceDefinition serviceDef2 = mock(); + ServerServiceDefinitionFilter serviceFilter = (serviceDef, serviceFactory) -> true; + DefaultGrpcServerFactory serverFactory = new DefaultGrpcServerFactory("myhost:5150", List.of(), null, null, + null, serviceFilter); + serverFactory.addService(serviceDef2); + serverFactory.addService(serviceDef1); + assertThat(serverFactory) + .extracting("serviceList", InstanceOfAssertFactories.list(ServerServiceDefinition.class)) + .containsExactly(serviceDef2, serviceDef1); + } + + @Test + void whenFilterAllowsOneThenOneServiceAdded() { + ServerServiceDefinition serviceDef1 = mock(); + ServerServiceDefinition serviceDef2 = mock(); + ServerServiceDefinitionFilter serviceFilter = (serviceDef, serviceFactory) -> serviceDef == serviceDef1; + DefaultGrpcServerFactory serverFactory = new DefaultGrpcServerFactory("myhost:5150", List.of(), null, null, + null, serviceFilter); + serverFactory.addService(serviceDef2); + serverFactory.addService(serviceDef1); + assertThat(serverFactory) + .extracting("serviceList", InstanceOfAssertFactories.list(ServerServiceDefinition.class)) + .containsExactly(serviceDef1); + } + + } + +} diff --git a/spring-grpc-core/src/test/java/org/springframework/grpc/server/GrpcServerFactoryTests.java b/spring-grpc-core/src/test/java/org/springframework/grpc/server/GrpcServerFactoryTests.java deleted file mode 100644 index 76265feb..00000000 --- a/spring-grpc-core/src/test/java/org/springframework/grpc/server/GrpcServerFactoryTests.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.server; - -import org.junit.jupiter.api.Test; - -/** - * Tests for {@link GrpcServerFactory gRPC server factories}. - */ -class GrpcServerFactoryTests { - - @Test - void placeholderTest() { - } - -} 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 82b68bf7..6a980e46 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 @@ -17,6 +17,7 @@ package org.springframework.grpc.autoconfigure.server; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.inOrder; @@ -26,11 +27,16 @@ import java.time.Duration; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.stream.Stream; 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.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.InOrder; import org.mockito.MockedStatic; import org.mockito.Mockito; @@ -45,7 +51,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; -import org.springframework.grpc.server.DefaultGrpcServerFactory; import org.springframework.grpc.server.GrpcServerFactory; import org.springframework.grpc.server.InProcessGrpcServerFactory; import org.springframework.grpc.server.NettyGrpcServerFactory; @@ -83,13 +88,23 @@ void prepareForTest() { when(service.bindService()).thenReturn(serviceDefinition); } - private AbstractApplicationContextRunner contextRunner() { + private ApplicationContextRunner contextRunner() { // NOTE: we use noop server lifecycle to avoid startup ApplicationContextRunner runner = new ApplicationContextRunner(); return contextRunner(runner); } - private AbstractApplicationContextRunner contextRunner(AbstractApplicationContextRunner runner) { + private ApplicationContextRunner contextRunner(ApplicationContextRunner runner) { + return runner + .withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class, + GrpcServerFactoryAutoConfiguration.class, SslAutoConfiguration.class)) + .withBean("shadedNettyGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean("nettyGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean("inProcessGrpcServerLifecycle", GrpcServerLifecycle.class, Mockito::mock) + .withBean(BindableService.class, () -> service); + } + + private WebApplicationContextRunner webContextRunner(WebApplicationContextRunner runner) { return runner .withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class, GrpcServerFactoryAutoConfiguration.class, SslAutoConfiguration.class)) @@ -294,7 +309,7 @@ void shadedNettyServerFactoryAutoConfiguredAsExpected() { @Test void serverFactoryAutoConfiguredInWebAppWhenServletDisabled() { serverFactoryAutoConfiguredAsExpected( - this.contextRunner(new WebApplicationContextRunner()) + this.webContextRunner(new WebApplicationContextRunner()) .withPropertyValues("spring.grpc.server.host=myhost", "spring.grpc.server.port=6160") .withPropertyValues("spring.grpc.server.servlet.enabled=false"), GrpcServerFactory.class, "myhost:6160", "shadedNettyGrpcServerLifecycle"); @@ -395,6 +410,74 @@ void nettyServerFactoryAutoConfiguredWithSsl() { NettyGrpcServerFactory.class, "myhost:6160", "nettyGrpcServerLifecycle"); } + @Nested + class WithAllFactoriesServiceFilterAutoConfig { + + static Stream serverFactoryProvider() { + return Stream.of(arguments( + (Function) (contextRunner) -> contextRunner, + ShadedNettyGrpcServerFactory.class), + arguments( + (Function) ( + contextRunner) -> contextRunner.withClassLoader(new FilteredClassLoader( + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)), + NettyGrpcServerFactory.class), + arguments( + (Function) ( + contextRunner) -> contextRunner + .withPropertyValues("spring.grpc.server.inprocess.name=foo") + .withClassLoader(new FilteredClassLoader(NettyServerBuilder.class, + io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder.class)), + InProcessGrpcServerFactory.class)); + } + + @ParameterizedTest(name = "whenNoServiceFilterThenFactoryUsesNoFilter w/ factory {1}") + @MethodSource("serverFactoryProvider") + void whenNoServiceFilterThenFactoryUsesNoFilter( + Function serverFactoryContextCustomizer, + Class expectedServerFactoryType) { + GrpcServerAutoConfigurationTests.this.contextRunner() + .withPropertyValues("spring.grpc.server.port=0") + .with(serverFactoryContextCustomizer) + .run((context) -> assertThat(context).getBean(GrpcServerFactory.class) + .isInstanceOf(expectedServerFactoryType) + .extracting("serviceFilter") + .isNull()); + } + + @ParameterizedTest(name = "whenUniqueServiceFilterThenFactoryUsesFilter w/ factory {1}") + @MethodSource("serverFactoryProvider") + void whenUniqueServiceFilterThenFactoryUsesFilter( + Function serverFactoryContextCustomizer, + Class expectedServerFactoryType) { + ServerServiceDefinitionFilter serviceFilter = mock(); + GrpcServerAutoConfigurationTests.this.contextRunner() + .withPropertyValues("spring.grpc.server.port=0") + .withBean(ServerServiceDefinitionFilter.class, () -> serviceFilter) + .with(serverFactoryContextCustomizer) + .run((context) -> assertThat(context).getBean(GrpcServerFactory.class) + .isInstanceOf(expectedServerFactoryType) + .extracting("serviceFilter") + .isSameAs(serviceFilter)); + } + + @ParameterizedTest(name = "whenMultipleServiceFiltersThenThrowsException w/ factory {1}") + @MethodSource("serverFactoryProvider") + void whenMultipleServiceFiltersThenThrowsException( + Function serverFactoryContextCustomizer, + Class ignored) { + GrpcServerAutoConfigurationTests.this.contextRunnerWithLifecyle() + .withPropertyValues("spring.grpc.server.port=0") + .withBean("filter1", ServerServiceDefinitionFilter.class, Mockito::mock) + .withBean("filter2", ServerServiceDefinitionFilter.class, Mockito::mock) + .with(serverFactoryContextCustomizer) + .run((context) -> assertThat(context).hasFailed() + .getFailure() + .hasMessageContaining("expected single matching bean but found 2: filter1,filter2")); + } + + } + @Nested class WithGrpcServiceConfigurerAutoConfig { 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 8173d853..10be2b01 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 @@ -57,8 +57,7 @@ public class InProcessTestAutoConfiguration { @ConditionalOnBean(BindableService.class) @Order(Ordered.HIGHEST_PRECEDENCE) TestInProcessGrpcServerFactory testInProcessGrpcServerFactory(GrpcServiceDiscoverer serviceDiscoverer, - GrpcServiceConfigurer serviceConfigurer, - List> customizers, + GrpcServiceConfigurer serviceConfigurer, List> customizers, @Nullable ServerServiceDefinitionFilter serviceFilter) { var factory = new TestInProcessGrpcServerFactory(address, customizers, serviceFilter); serviceDiscoverer.findServices()