serverInterceptorsProvider, ApplicationContext applicationContext) {
- return new DefaultGrpcServiceDiscoverer(bindableServicesProvider, serverInterceptorsProvider,
- applicationContext);
+ GrpcServiceConfigurer grpcServiceConfigurer(ApplicationContext applicationContext) {
+ return new DefaultGrpcServiceConfigurer(applicationContext);
+ }
+
+ @ConditionalOnMissingBean
+ @Bean
+ GrpcServiceDiscoverer grpcServiceDiscoverer(GrpcServiceConfigurer grpcServiceConfigurer,
+ ApplicationContext applicationContext) {
+ return new DefaultGrpcServiceDiscoverer(grpcServiceConfigurer, applicationContext);
}
@ConditionalOnBean(CompressorRegistry.class)
diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcService.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcService.java
new file mode 100644
index 0000000..5655323
--- /dev/null
+++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcService.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2016-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.
+ *
+ * Copy from net.devh:grpc-spring-boot-starter.
+ */
+
+package org.springframework.grpc.autoconfigure.server;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.stereotype.Service;
+
+import io.grpc.BindableService;
+import io.grpc.ServerInterceptor;
+
+/**
+ * Annotation that marks gRPC services that should be registered with a gRPC server.
+ *
+ * NOTE: This annotation is optional as all {@link BindableService} beans will be
+ * registered with a gRPC server. However, this annotation allows specifying additional
+ * information about the service (e.g. interceptors).
+ *
+ * NOTE: This annotation should only be added to {@link BindableService} beans.
+ *
+ * @author Michael (yidongnan@gmail.com)
+ * @author Chris Bono
+ */
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Service
+@Bean
+public @interface GrpcService {
+
+ /**
+ * The {@link ServerInterceptor} bean types to be applied to the service.
+ * @return the interceptor bean types to be applied to the service
+ */
+ Class extends ServerInterceptor>[] interceptors() default {};
+
+ /**
+ * The {@link ServerInterceptor} bean names to be applied to the service.
+ * @return the interceptor bean names to be applied to the service
+ */
+ String[] interceptorNames() default {};
+
+ /**
+ * Whether the service specific interceptors should be blended with the global
+ * interceptors.
+ *
+ * When false, the global interceptors are applied first, followed by the service
+ * specific interceptors.
+ *
+ * When true, the global interceptors are merged and sorted (blended) with the service
+ * specific interceptors.
+ * @return whether the service specific interceptors should be blended with the global
+ * interceptors
+ */
+ boolean blendWithGlobalInterceptors() default false;
+
+}
diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServiceConfigurer.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServiceConfigurer.java
new file mode 100644
index 0000000..6a4bfac
--- /dev/null
+++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServiceConfigurer.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.grpc.autoconfigure.server;
+
+import org.springframework.lang.Nullable;
+
+import io.grpc.BindableService;
+import io.grpc.ServerServiceDefinition;
+
+/**
+ * Configures and binds a {@link BindableService gRPC Service}.
+ *
+ * @author Chris Bono
+ */
+@FunctionalInterface
+public interface GrpcServiceConfigurer {
+
+ /**
+ * Configure and bind a gRPC service.
+ * @param bindableService service to bind and configure
+ * @param serviceInfo optional additional service information
+ * @return configured service definition
+ */
+ ServerServiceDefinition configure(BindableService bindableService, @Nullable GrpcServiceInfo serviceInfo);
+
+}
diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServiceInfo.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServiceInfo.java
new file mode 100644
index 0000000..f3eaef4
--- /dev/null
+++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServiceInfo.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2024-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.grpc.autoconfigure.server;
+
+import java.util.List;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+import io.grpc.ServerInterceptor;
+
+/**
+ * Additional information about a gRPC service that can be used when configuring the
+ * service.
+ *
+ * @param interceptors list of {@link ServerInterceptor} bean types to be applied to the
+ * service
+ * @param interceptorNames list of {@link ServerInterceptor} bean names to be applied to
+ * the service
+ * @param blendWithGlobalInterceptors whether the service specific interceptors should be
+ * merged and sorted (blended) with the global interceptors
+ * @author Chris Bono
+ */
+public record GrpcServiceInfo(Class extends ServerInterceptor>[] interceptors, String[] interceptorNames,
+ boolean blendWithGlobalInterceptors) {
+
+ public GrpcServiceInfo {
+ Assert.notNull(interceptors, "interceptors must not be null");
+ Assert.notNull(interceptorNames, "interceptorNames must not be null");
+ }
+
+ /**
+ * Construct a service info from a {@link GrpcService} annotation.
+ * @param grpcService the service annotation
+ * @return the service info or null if the supplied annotation is null
+ */
+ @Nullable
+ public static GrpcServiceInfo from(@Nullable GrpcService grpcService) {
+ return grpcService != null ? new GrpcServiceInfo(grpcService.interceptors(), grpcService.interceptorNames(),
+ grpcService.blendWithGlobalInterceptors()) : null;
+ }
+
+ /**
+ * Construct a service info with the specified interceptors.
+ * @param interceptors non-null list of interceptor bean types
+ * @return the service info with the supplied interceptors
+ */
+ @SuppressWarnings("unchecked")
+ public static GrpcServiceInfo withInterceptors(List> interceptors) {
+ Assert.notNull(interceptors, "interceptors must not be null");
+ return new GrpcServiceInfo(interceptors.toArray(new Class[0]), new String[0], false);
+ }
+
+ /**
+ * Construct a service info with the specified interceptors names.
+ * @param interceptorNames non-null list of interceptor bean names
+ * @return the service info with the supplied interceptors names
+ */
+ @SuppressWarnings("unchecked")
+ public static GrpcServiceInfo withInterceptorNames(List interceptorNames) {
+ Assert.notNull(interceptorNames, "interceptorNames must not be null");
+ return new GrpcServiceInfo(new Class[0], interceptorNames.toArray(new String[0]), false);
+ }
+
+}
diff --git a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/DefaultGrpcServiceConfigurerTests.java b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/DefaultGrpcServiceConfigurerTests.java
new file mode 100644
index 0000000..e07ffe2
--- /dev/null
+++ b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/DefaultGrpcServiceConfigurerTests.java
@@ -0,0 +1,346 @@
+/*
+ * Copyright 2023-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.grpc.autoconfigure.server;
+
+import static org.assertj.core.api.Assertions.assertThat;
+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;
+
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.stubbing.Answer;
+
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
+import org.springframework.lang.Nullable;
+
+import io.grpc.BindableService;
+import io.grpc.ServerInterceptor;
+import io.grpc.ServerInterceptors;
+import io.grpc.ServerServiceDefinition;
+
+/**
+ * Tests for {@link DefaultGrpcServiceConfigurer}.
+ *
+ * @author Chris Bono
+ */
+class DefaultGrpcServiceConfigurerTests {
+
+ private ApplicationContextRunner contextRunner() {
+ // NOTE: we use noop server lifecycle to avoid startup
+ return new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class))
+ .withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock);
+ }
+
+ @Test
+ void globalServerInterceptorsAreFoundInProperOrder() {
+ this.contextRunner()
+ .withUserConfiguration(GlobalServerInterceptorsConfig.class)
+ .run((context) -> assertThat(context).getBean(DefaultGrpcServiceConfigurer.class)
+ .extracting("globalInterceptors", InstanceOfAssertFactories.LIST)
+ .containsExactly(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
+ GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO));
+ }
+
+ @Nested
+ class WithNoServiceInfoSpecified {
+
+ @Test
+ void whenNoGlobalInterceptorsRegisteredThenServiceGetsNoInterceptors() {
+ customizeContextAndRunServiceConfigurerWithServiceInfo(Function.identity(), null, List.of());
+ }
+
+ @Test
+ void whenGlobalInterceptorsRegisteredThenServiceGetsGlobalInterceptors() {
+ customizeContextAndRunServiceConfigurerWithServiceInfo(
+ (contextRunner) -> contextRunner.withUserConfiguration(GlobalServerInterceptorsConfig.class), null,
+ List.of(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
+ GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO));
+ }
+
+ }
+
+ @Nested
+ class WithServiceInfoWithSingleInterceptor {
+
+ @Test
+ void whenSingleBeanOfInterceptorTypeRegisteredThenItIsUsed() {
+ GrpcServiceInfo serviceInfo = GrpcServiceInfo.withInterceptors(List.of(TestServerInterceptorA.class));
+ List expectedInterceptors = List.of(ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A);
+ customizeContextAndRunServiceConfigurerWithServiceInfo(
+ (contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class),
+ serviceInfo, expectedInterceptors);
+ }
+
+ @Test
+ void whenMultipleBeansOfInterceptorTypeRegisteredThenThrowsException() {
+ GrpcServiceInfo serviceInfo = GrpcServiceInfo.withInterceptors(List.of(ServerInterceptor.class));
+ customizeContextAndRunServiceConfigurerWithServiceInfo(
+ (contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class),
+ serviceInfo, NoUniqueBeanDefinitionException.class);
+ }
+
+ @Test
+ void whenNoBeanOfInterceptorTypeRegisteredThenThrowsException() {
+ GrpcServiceInfo serviceInfo = GrpcServiceInfo.withInterceptors(List.of(ServerInterceptor.class));
+ customizeContextAndRunServiceConfigurerWithServiceInfo(Function.identity(), serviceInfo,
+ NoSuchBeanDefinitionException.class);
+ }
+
+ }
+
+ @Nested
+ class WithServiceInfoWithMultipleInterceptors {
+
+ @Test
+ void whenSingleBeanOfEachInterceptorTypeRegisteredThenTheyAreUsed() {
+ GrpcServiceInfo serviceInfo = GrpcServiceInfo
+ .withInterceptors(List.of(TestServerInterceptorB.class, TestServerInterceptorA.class));
+ List expectedInterceptors = List.of(ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_B,
+ ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A);
+ customizeContextAndRunServiceConfigurerWithServiceInfo(
+ (contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class),
+ serviceInfo, expectedInterceptors);
+ }
+
+ }
+
+ @Nested
+ class WithServiceInfoWithSingleInterceptorName {
+
+ @Test
+ void whenSingleBeanWithInterceptorNameRegisteredThenItIsUsed() {
+ GrpcServiceInfo serviceInfo = GrpcServiceInfo.withInterceptorNames(List.of("interceptorB"));
+ List expectedInterceptors = List.of(ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_B);
+ customizeContextAndRunServiceConfigurerWithServiceInfo(
+ (contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class),
+ serviceInfo, expectedInterceptors);
+ }
+
+ @Test
+ void whenNoBeanWithInterceptorNameRegisteredThenThrowsException() {
+ GrpcServiceInfo serviceInfo = GrpcServiceInfo.withInterceptorNames(List.of("interceptor1"));
+ customizeContextAndRunServiceConfigurerWithServiceInfo(Function.identity(), serviceInfo,
+ NoSuchBeanDefinitionException.class);
+ }
+
+ }
+
+ @Nested
+ class WithServiceInfoWithMultipleInterceptorNames {
+
+ @Test
+ void whenSingleBeanWithEachInterceptorNameRegisteredThenTheyAreUsed() {
+ GrpcServiceInfo serviceInfo = GrpcServiceInfo.withInterceptorNames(List.of("interceptorB", "interceptorA"));
+ List expectedInterceptors = List.of(ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_B,
+ ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A);
+ customizeContextAndRunServiceConfigurerWithServiceInfo(
+ (contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class),
+ serviceInfo, expectedInterceptors);
+ }
+
+ }
+
+ @Nested
+ class WithServiceInfoWithInterceptorAndInterceptorName {
+
+ @SuppressWarnings("unchecked")
+ @Test
+ void whenSingleBeanOfEachAvailableThenTheyAreBothUsed() {
+ GrpcServiceInfo serviceInfo = new GrpcServiceInfo(new Class[] { TestServerInterceptorB.class },
+ new String[] { "interceptorA" }, false);
+ List expectedInterceptors = List.of(ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_B,
+ ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A);
+ customizeContextAndRunServiceConfigurerWithServiceInfo(
+ (contextRunner) -> contextRunner.withUserConfiguration(ServiceSpecificInterceptorsConfig.class),
+ serviceInfo, expectedInterceptors);
+ }
+
+ }
+
+ @Nested
+ class WithServiceInfoCombinedWithGlobalInterceptors {
+
+ @Test
+ void whenBlendInterceptorsFalseThenGlobalInterceptorsAddedFirst() {
+ GrpcServiceInfo serviceInfo = GrpcServiceInfo
+ .withInterceptors(List.of(TestServerInterceptorB.class, TestServerInterceptorA.class));
+ List expectedInterceptors = List.of(
+ GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
+ GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO,
+ ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_B,
+ ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A);
+ customizeContextAndRunServiceConfigurerWithServiceInfo((contextRunner) -> contextRunner
+ .withUserConfiguration(GlobalServerInterceptorsConfig.class, ServiceSpecificInterceptorsConfig.class),
+ serviceInfo, expectedInterceptors);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Disabled("Needs 'blend interceptors' to be implemented")
+ @Test
+ void whenBlendInterceptorsTrueThenGlobalInterceptorsBlended() {
+ GrpcServiceInfo serviceInfo = new GrpcServiceInfo(
+ new Class[] { TestServerInterceptorB.class, TestServerInterceptorA.class }, new String[0], true);
+ List expectedInterceptors = List.of(
+ GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
+ ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_B,
+ GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO,
+ ServiceSpecificInterceptorsConfig.SVC_INTERCEPTOR_A);
+ customizeContextAndRunServiceConfigurerWithServiceInfo((contextRunner) -> contextRunner
+ .withUserConfiguration(GlobalServerInterceptorsConfig.class, ServiceSpecificInterceptorsConfig.class),
+ serviceInfo, expectedInterceptors);
+ }
+
+ }
+
+ private void customizeContextAndRunServiceConfigurerWithServiceInfo(
+ Function contextCustomizer, GrpcServiceInfo serviceInfo,
+ List expectedInterceptors) {
+ this.customizeContextAndRunServiceConfigurerWithServiceInfo(contextCustomizer, serviceInfo,
+ expectedInterceptors, null);
+ }
+
+ private void customizeContextAndRunServiceConfigurerWithServiceInfo(
+ Function contextCustomizer, GrpcServiceInfo serviceInfo,
+ Class extends Throwable> expectedExceptionType) {
+ this.customizeContextAndRunServiceConfigurerWithServiceInfo(contextCustomizer, serviceInfo, null,
+ expectedExceptionType);
+ }
+
+ private void customizeContextAndRunServiceConfigurerWithServiceInfo(
+ Function contextCustomizer, GrpcServiceInfo serviceInfo,
+ @Nullable List expectedInterceptors,
+ @Nullable Class extends Throwable> expectedExceptionType) {
+ // It gets difficult to verify interceptors are added properly to mocked services.
+ // To make it easier, we just static mock ServerInterceptors.interceptForward to
+ // echo back the service def. This way we can verify the interceptors were passed
+ // in the proper order as we rely on ServerInterceptors.interceptForward being
+ // well tested in grpc-java.
+ try (MockedStatic serverInterceptorsMocked = Mockito.mockStatic(ServerInterceptors.class)) {
+ serverInterceptorsMocked
+ .when(() -> ServerInterceptors.interceptForward(any(ServerServiceDefinition.class), anyList()))
+ .thenAnswer((Answer) invocation -> invocation.getArgument(0));
+ 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(service, serviceInfo))
+ .isInstanceOf(expectedExceptionType);
+ serverInterceptorsMocked.verifyNoInteractions();
+ }
+ else {
+ configurer.configure(service, serviceInfo);
+ serverInterceptorsMocked
+ .verify(() -> ServerInterceptors.interceptForward(serviceDef, expectedInterceptors));
+ }
+ });
+ }
+ }
+
+ interface TestServerInterceptorA extends ServerInterceptor {
+
+ }
+
+ interface TestServerInterceptorB extends ServerInterceptor {
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ static class GlobalServerInterceptorsConfig {
+
+ static BindableService SERVICE_A = mock();
+
+ static ServerServiceDefinition SERVICE_DEF_A = mock();
+
+ static ServerInterceptor GLOBAL_INTERCEPTOR_FOO = mock();
+
+ static ServerInterceptor GLOBAL_INTERCEPTOR_IGNORED = mock();
+
+ static ServerInterceptor GLOBAL_INTERCEPTOR_BAR = mock();
+
+ @Bean
+ BindableService serviceA() {
+ when(SERVICE_A.bindService()).thenReturn(SERVICE_DEF_A);
+ return SERVICE_A;
+ }
+
+ @Bean
+ @Order(200)
+ @GlobalServerInterceptor
+ ServerInterceptor globalInterceptorFoo() {
+ return GLOBAL_INTERCEPTOR_FOO;
+ }
+
+ @Bean
+ @Order(150)
+ ServerInterceptor globalInterceptorIgnored() {
+ return GLOBAL_INTERCEPTOR_IGNORED;
+ }
+
+ @Bean
+ @Order(100)
+ @GlobalServerInterceptor
+ ServerInterceptor globalInterceptorBar() {
+ return GLOBAL_INTERCEPTOR_BAR;
+ }
+
+ }
+
+ @Configuration(proxyBeanMethods = false)
+ static class ServiceSpecificInterceptorsConfig {
+
+ static TestServerInterceptorB SVC_INTERCEPTOR_B = mock();
+
+ static TestServerInterceptorA SVC_INTERCEPTOR_A = mock();
+
+ @Bean
+ @Order(150)
+ TestServerInterceptorB interceptorB() {
+ return SVC_INTERCEPTOR_B;
+ }
+
+ @Bean
+ @Order(225)
+ TestServerInterceptorA interceptorA() {
+ return SVC_INTERCEPTOR_A;
+ }
+
+ }
+
+}
diff --git a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/DefaultGrpcServiceDiscovererTests.java b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/DefaultGrpcServiceDiscovererTests.java
index 0dc5f88..c093436 100644
--- a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/DefaultGrpcServiceDiscovererTests.java
+++ b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/DefaultGrpcServiceDiscovererTests.java
@@ -16,18 +16,18 @@
package org.springframework.grpc.autoconfigure.server;
-import java.util.List;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.springframework.grpc.autoconfigure.server.DefaultGrpcServiceDiscovererTests.DefaultGrpcServiceDiscovererTestsConfig.SERVICE_A;
+import static org.springframework.grpc.autoconfigure.server.DefaultGrpcServiceDiscovererTests.DefaultGrpcServiceDiscovererTestsConfig.SERVICE_B;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
-import io.grpc.BindableService;
-import io.grpc.ServerInterceptor;
-import io.grpc.ServerInterceptors;
-import io.grpc.ServerServiceDefinition;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.MockedStatic;
import org.mockito.Mockito;
-import org.mockito.stubbing.Answer;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -36,12 +36,8 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyList;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.when;
+import io.grpc.BindableService;
+import io.grpc.ServerServiceDefinition;
/**
* Tests for {@link DefaultGrpcServiceDiscoverer}.
@@ -58,50 +54,29 @@ class DefaultGrpcServiceDiscovererTests {
}
@Test
- void globalServerInterceptorsAreFoundInProperOrder() {
+ void servicesAreFoundInProperOrderWithExpectedGrpcServiceAnnotations() {
+ TestServiceConfigurer configurer = new TestServiceConfigurer();
this.contextRunner()
- .withUserConfiguration(GlobalServerInterceptorsConfig.class)
- .run((context) -> assertThat(context).getBean(DefaultGrpcServiceDiscoverer.class)
- .extracting(DefaultGrpcServiceDiscoverer::findGlobalInterceptors, InstanceOfAssertFactories.LIST)
- .containsExactly(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
- GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO));
- }
-
- @Test
- void servicesAreFoundInProperOrderWithGlobalInterceptorsApplied() {
- // It gets difficult to verify interceptors are added properly to mocked services.
- // To make it easier, we just static mock ServerInterceptors.interceptForward to
- // echo back the service def. This way we can verify the interceptors were passed
- // in the proper order as we rely/trust that ServerInterceptors.interceptForward
- // is
- // tested well in grpc-java.
- try (MockedStatic serverInterceptorsMocked = Mockito.mockStatic(ServerInterceptors.class)) {
- serverInterceptorsMocked
- .when(() -> ServerInterceptors.interceptForward(any(ServerServiceDefinition.class), anyList()))
- .thenAnswer((Answer) invocation -> invocation.getArgument(0));
- this.contextRunner().withUserConfiguration(GlobalServerInterceptorsConfig.class).run((context) -> {
+ .withUserConfiguration(DefaultGrpcServiceDiscovererTestsConfig.class)
+ .withBean("customServiceConfigurer", GrpcServiceConfigurer.class, () -> configurer)
+ .run((context) -> {
assertThat(context).getBean(DefaultGrpcServiceDiscoverer.class)
.extracting(DefaultGrpcServiceDiscoverer::findServices, InstanceOfAssertFactories.LIST)
- .containsExactly(GlobalServerInterceptorsConfig.SERVICE_DEF_B,
- GlobalServerInterceptorsConfig.SERVICE_DEF_A);
- ArgumentCaptor serviceDefArg = ArgumentCaptor.captor();
- ArgumentCaptor> interceptorsArg = ArgumentCaptor.captor();
- serverInterceptorsMocked.verify(
- () -> ServerInterceptors.interceptForward(serviceDefArg.capture(), interceptorsArg.capture()),
- times(2));
- assertThat(serviceDefArg.getAllValues()).containsExactly(GlobalServerInterceptorsConfig.SERVICE_DEF_B,
- GlobalServerInterceptorsConfig.SERVICE_DEF_A);
- assertThat(interceptorsArg.getAllValues()).containsExactly(
- List.of(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
- GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO),
- List.of(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
- GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO));
+ .containsExactly(DefaultGrpcServiceDiscovererTestsConfig.SERVICE_DEF_B,
+ DefaultGrpcServiceDiscovererTestsConfig.SERVICE_DEF_A);
+ assertThat(configurer.invocations).hasSize(2);
+ assertThat(configurer.invocations.keySet()).containsExactly(SERVICE_B, SERVICE_A);
+ assertThat(configurer.invocations).containsEntry(SERVICE_B, null);
+ assertThat(configurer.invocations).hasEntrySatisfying(SERVICE_A, (serviceInfo) -> {
+ assertThat(serviceInfo.interceptors()).isEmpty();
+ assertThat(serviceInfo.interceptorNames()).isEmpty();
+ assertThat(serviceInfo.blendWithGlobalInterceptors()).isFalse();
+ });
});
- }
}
@Configuration(proxyBeanMethods = false)
- static class GlobalServerInterceptorsConfig {
+ static class DefaultGrpcServiceDiscovererTestsConfig {
static BindableService SERVICE_A = mock();
@@ -111,12 +86,7 @@ class DefaultGrpcServiceDiscovererTests {
static ServerServiceDefinition SERVICE_DEF_B = mock();
- static ServerInterceptor GLOBAL_INTERCEPTOR_FOO = mock();
-
- static ServerInterceptor GLOBAL_INTERCEPTOR_IGNORED = mock();
-
- static ServerInterceptor GLOBAL_INTERCEPTOR_BAR = mock();
-
+ @GrpcService
@Bean
@Order(200)
BindableService serviceA() {
@@ -131,24 +101,16 @@ class DefaultGrpcServiceDiscovererTests {
return SERVICE_B;
}
- @Bean
- @Order(200)
- @GlobalServerInterceptor
- ServerInterceptor globalInterceptorFoo() {
- return GLOBAL_INTERCEPTOR_FOO;
- }
+ }
- @Bean
- @Order(150)
- ServerInterceptor globalInterceptorIgnored() {
- return GLOBAL_INTERCEPTOR_IGNORED;
- }
+ static class TestServiceConfigurer implements GrpcServiceConfigurer {
- @Bean
- @Order(100)
- @GlobalServerInterceptor
- ServerInterceptor globalInterceptorBar() {
- return GLOBAL_INTERCEPTOR_BAR;
+ Map invocations = new LinkedHashMap<>();
+
+ @Override
+ public ServerServiceDefinition configure(BindableService bindableService, GrpcServiceInfo serviceInfo) {
+ invocations.put(bindableService, serviceInfo);
+ return bindableService.bindService();
}
}
diff --git a/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServiceInfoTests.java b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServiceInfoTests.java
new file mode 100644
index 0000000..3461af6
--- /dev/null
+++ b/spring-grpc-spring-boot-autoconfigure/src/test/java/org/springframework/grpc/autoconfigure/server/GrpcServiceInfoTests.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2024-2024 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.grpc.autoconfigure.server;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.core.annotation.AnnotationUtils;
+
+import io.grpc.ServerInterceptor;
+
+import java.util.List;
+
+/**
+ * Tests for {@link GrpcServiceInfo}.
+ */
+class GrpcServiceInfoTests {
+
+ @Nested
+ class WithInterceptorsApiInvokedWith {
+
+ @Test
+ void nullInterceptors() {
+ assertThatIllegalArgumentException().isThrownBy(() -> GrpcServiceInfo.withInterceptors(null))
+ .withMessage("interceptors must not be null");
+ }
+
+ @Test
+ void interceptorTypes() {
+ assertThat(GrpcServiceInfo.withInterceptors(List.of(ServerInterceptor.class))).satisfies((serviceInfo) -> {
+ assertThat(serviceInfo.interceptors()).containsExactly(ServerInterceptor.class);
+ assertThat(serviceInfo.interceptorNames()).isEmpty();
+ assertThat(serviceInfo.blendWithGlobalInterceptors()).isFalse();
+ });
+ }
+
+ }
+
+ @Nested
+ class WithInterceptorNamesApiInvokedWith {
+
+ @Test
+ void nullInterceptorNames() {
+ assertThatIllegalArgumentException().isThrownBy(() -> GrpcServiceInfo.withInterceptorNames(null))
+ .withMessage("interceptorNames must not be null");
+ }
+
+ @Test
+ void interceptorNames() {
+ assertThat(GrpcServiceInfo.withInterceptorNames(List.of("myInterceptor"))).satisfies((serviceInfo) -> {
+ assertThat(serviceInfo.interceptors()).isEmpty();
+ assertThat(serviceInfo.interceptorNames()).containsExactly("myInterceptor");
+ assertThat(serviceInfo.blendWithGlobalInterceptors()).isFalse();
+ });
+ }
+
+ }
+
+ @Nested
+ class FromApiInvokedWith {
+
+ @Test
+ void nullGrpcService() {
+ assertThat(GrpcServiceInfo.from(null)).isNull();
+ }
+
+ @Test
+ void grpcServiceAnnotationWithDefaults() {
+ var grpcServiceAnnotation = AnnotationUtils.findAnnotation(TestServiceMarkedWithDefaults.class,
+ GrpcService.class);
+ assertThat(GrpcServiceInfo.from(grpcServiceAnnotation)).satisfies((serviceInfo) -> {
+ assertThat(serviceInfo.interceptors()).isEmpty();
+ assertThat(serviceInfo.interceptorNames()).isEmpty();
+ assertThat(serviceInfo.blendWithGlobalInterceptors()).isFalse();
+ });
+ }
+
+ @Test
+ void grpcServiceAnnotationWithAttributes() {
+ var grpcServiceAnnotation = AnnotationUtils.findAnnotation(TestServiceMarkedWithAttributes.class,
+ GrpcService.class);
+ assertThat(GrpcServiceInfo.from(grpcServiceAnnotation)).satisfies((serviceInfo) -> {
+ assertThat(serviceInfo.interceptors()).containsExactly(ServerInterceptor.class);
+ assertThat(serviceInfo.interceptorNames()).containsExactly("myInterceptor");
+ assertThat(serviceInfo.blendWithGlobalInterceptors()).isTrue();
+ });
+ }
+
+ }
+
+ @GrpcService
+ static class TestServiceMarkedWithDefaults {
+
+ }
+
+ @GrpcService(interceptors = ServerInterceptor.class, interceptorNames = "myInterceptor",
+ blendWithGlobalInterceptors = true)
+ static class TestServiceMarkedWithAttributes {
+
+ }
+
+}