diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/service/ApplicationContextBeanLookupUtils.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/service/ApplicationContextBeanLookupUtils.java index ca7ed35..f33625d 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/server/service/ApplicationContextBeanLookupUtils.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/service/ApplicationContextBeanLookupUtils.java @@ -20,6 +20,9 @@ import java.util.LinkedHashMap; import java.util.List; import org.springframework.context.ApplicationContext; +import org.springframework.core.OrderComparator; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.Order; import org.springframework.util.Assert; @@ -93,4 +96,33 @@ final class ApplicationContextBeanLookupUtils { return applicationContext.getBeanProvider(beanType).orderedStream().filter(beanToNameMap::containsKey).toList(); } + /** + * Sorts the supplied list in place using an {@link OrderComparator} that takes the + * {@link Order @Order} annotation on bean factory methods in configuration classes + * into account. + * @param applicationContext the application context + * @param beanType the type of beans in the list + * @param beans the list of beans to sort + */ + static void sortBeansIncludingOrderAnnotation(ApplicationContext applicationContext, Class beanType, + List beans) { + var beanToNameMap = new LinkedHashMap(); + applicationContext.getBeansOfType(beanType).forEach((name, bean) -> beanToNameMap.put(bean, name)); + beans.sort(OrderComparator.INSTANCE.withSourceProvider(bean -> { + Integer priority = AnnotationAwareOrderComparator.INSTANCE.getPriority(bean); + if (priority != null) { + return (Ordered) () -> priority; + } + // Consult the bean factory method for annotations + String beanName = beanToNameMap.get(bean); + if (beanName != null) { + Order order = applicationContext.findAnnotationOnBean(beanName, Order.class); + if (order != null) { + return (Ordered) order::value; + } + } + return null; + })); + } + } 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 1e8c030..ed75e49 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 @@ -76,7 +76,10 @@ public class DefaultGrpcServiceConfigurer implements GrpcServiceConfigurer, Init Arrays.stream(serviceInfo.interceptorNames()) .forEachOrdered((interceptorBeanName) -> allInterceptors .add(this.applicationContext.getBean(interceptorBeanName, ServerInterceptor.class))); - // TODO handle blend + if (serviceInfo.blendWithGlobalInterceptors()) { + ApplicationContextBeanLookupUtils.sortBeansIncludingOrderAnnotation(this.applicationContext, + ServerInterceptor.class, allInterceptors); + } return ServerInterceptors.interceptForward(serviceDef, allInterceptors); } 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 8c36e12..13a4018 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 @@ -25,7 +25,6 @@ import java.util.function.Function; import org.assertj.core.api.Assertions; 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; @@ -256,7 +255,6 @@ class DefaultGrpcServiceConfigurerTests { } @SuppressWarnings("unchecked") - @Disabled("Needs 'blend interceptors' to be implemented") @Test void whenBlendInterceptorsTrueThenGlobalInterceptorsBlended() { GrpcServiceInfo serviceInfo = new GrpcServiceInfo( 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 c3852a4..a44c2a7 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 @@ -128,6 +128,16 @@ BindableService myService() { } ---- +[[server-interceptor-blending]] +[TIP] +==== +When a service is configured with both global and per-service interceptors, the global interceptors are first applied in their sorted order followed by the per-service interceptors in their sorted order. + +However, by setting the `blendWithGlobalInterceptors` attribute on the `@GrpcService` annotation to `"true"` you can change this behavior so that the interceptors are all combined and then sorted according to their bean natural ordering (i.e. `@Order`). + +You can use this option if you want to add a per-service interceptor between global interceptors. +==== + == Observability Spring gRPC provides an autoconfigured interceptor that can be used to provide observability to your gRPC services.