Skip to content

Commit 4daf413

Browse files
onobcdsyer
authored andcommitted
Add GlobalServerInterceptor to service discovery
This commit builds upon the previously added service discoverer by adding support for finding all server interceptor beans that are marked w/ the newly added `@GlobalServerInterceptor` annotation. Resolves #4 Signed-off-by: Chris Bono <[email protected]>
1 parent daf8e04 commit 4daf413

File tree

5 files changed

+255
-9
lines changed

5 files changed

+255
-9
lines changed

spring-grpc-docs/src/main/antora/modules/ROOT/pages/server.adoc

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,24 @@ dependencies {
8585

8686
The `spring.grpc.server.*` properties will be ignored in facour of the regular `server.*` properties in this case.
8787
The servlet that is created is mapped to process HTTP POST requests to the paths defined by the registered services, as `/<service-name>/*`.
88-
Clients can connect to the server using that path, which is what any gRPC client library will do.
88+
Clients can connect to the server using that path, which is what any gRPC client library will do.
89+
90+
[[server-interceptor]]
91+
== Server Interceptors
92+
93+
=== Global
94+
To add a server interceptor to be applied to all services you can simply register a server interceptor bean and then annotate it with `@GlobalServerInterceptor`.
95+
The interceptors are ordered according to their bean natural ordering (i.e. `@Order`).
96+
97+
[source,java]
98+
----
99+
@Bean
100+
@Order(100)
101+
@GlobalServerInterceptor
102+
ServerInterceptor myGlobalLoggingInterceptor() {
103+
return new MyLoggingInterceptor();
104+
}
105+
----
106+
107+
=== Per-Service
108+
This is a **WIP** and will be available soon.

spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/DefaultGrpcServiceDiscoverer.java

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,35 +16,58 @@
1616

1717
package org.springframework.grpc.autoconfigure.server;
1818

19-
import java.util.ArrayList;
19+
import java.util.HashMap;
2020
import java.util.List;
2121

2222
import io.grpc.BindableService;
23+
import io.grpc.ServerInterceptor;
24+
import io.grpc.ServerInterceptors;
2325
import io.grpc.ServerServiceDefinition;
2426

2527
import org.springframework.beans.factory.ObjectProvider;
28+
import org.springframework.context.ApplicationContext;
2629
import org.springframework.grpc.server.GrpcServiceDiscoverer;
2730

2831
/**
2932
* The default {@link GrpcServiceDiscoverer} that finds all {@link BindableService} beans
3033
* and configures and binds them.
3134
*
32-
* @author Michael ([email protected])
3335
* @author Chris Bono
3436
*/
3537
class DefaultGrpcServiceDiscoverer implements GrpcServiceDiscoverer {
3638

3739
private final ObjectProvider<BindableService> grpcServicesProvider;
3840

39-
DefaultGrpcServiceDiscoverer(ObjectProvider<BindableService> grpcServicesProvider) {
41+
private final ObjectProvider<ServerInterceptor> serverInterceptorsProvider;
42+
43+
private final ApplicationContext applicationContext;
44+
45+
public DefaultGrpcServiceDiscoverer(ObjectProvider<BindableService> grpcServicesProvider,
46+
ObjectProvider<ServerInterceptor> serverInterceptorsProvider, ApplicationContext applicationContext) {
4047
this.grpcServicesProvider = grpcServicesProvider;
48+
this.serverInterceptorsProvider = serverInterceptorsProvider;
49+
this.applicationContext = applicationContext;
4150
}
4251

4352
@Override
4453
public List<ServerServiceDefinition> findServices() {
45-
List<ServerServiceDefinition> list = new ArrayList<>(
46-
grpcServicesProvider.orderedStream().map(BindableService::bindService).toList());
47-
return list;
54+
List<ServerInterceptor> globalInterceptors = findGlobalInterceptors();
55+
return grpcServicesProvider.orderedStream()
56+
.map(BindableService::bindService)
57+
.map((svc) -> ServerInterceptors.interceptForward(svc, globalInterceptors))
58+
.toList();
59+
}
60+
61+
// VisibleForTesting
62+
List<ServerInterceptor> findGlobalInterceptors() {
63+
// We find unordered map of beans (keyed by name) with the annotation and then
64+
// reverse the map for easy lookup by bean.
65+
// We then get an ordered stream of all server interceptors and filter
66+
// out those that are not present in map of annotated interceptor beans.
67+
var nameToBeanMap = applicationContext.getBeansWithAnnotation(GlobalServerInterceptor.class);
68+
var beanToNameMap = new HashMap<Object, String>();
69+
nameToBeanMap.forEach((name, bean) -> beanToNameMap.put(bean, name));
70+
return this.serverInterceptorsProvider.orderedStream().filter(beanToNameMap::containsKey).toList();
4871
}
4972

5073
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2024-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.grpc.autoconfigure.server;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import io.grpc.ServerInterceptor;
26+
27+
import org.springframework.core.annotation.Order;
28+
29+
/**
30+
* Annotation that can be specified on a gRPC {@link ServerInterceptor} bean which will
31+
* result in the interceptor being applied globally to all services.
32+
* <p>
33+
* The bean interceptor {@link Order} will be respected.
34+
*
35+
* @author Daniel Theuke ([email protected])
36+
* @author Chris Bono
37+
*/
38+
@Target({ ElementType.TYPE, ElementType.METHOD })
39+
@Retention(RetentionPolicy.RUNTIME)
40+
@Documented
41+
public @interface GlobalServerInterceptor {
42+
43+
}

spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/server/GrpcServerAutoConfiguration.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2424
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2525
import org.springframework.boot.context.properties.EnableConfigurationProperties;
26+
import org.springframework.context.ApplicationContext;
2627
import org.springframework.context.ApplicationEventPublisher;
2728
import org.springframework.context.annotation.Bean;
2829
import org.springframework.context.annotation.Import;
@@ -36,6 +37,7 @@
3637
import io.grpc.CompressorRegistry;
3738
import io.grpc.DecompressorRegistry;
3839
import io.grpc.ServerBuilder;
40+
import io.grpc.ServerInterceptor;
3941

4042
/**
4143
* {@link EnableAutoConfiguration Auto-configuration} for gRPC server-side components.
@@ -75,8 +77,10 @@ ServerBuilderCustomizers serverBuilderCustomizers(ObjectProvider<ServerBuilderCu
7577

7678
@ConditionalOnMissingBean
7779
@Bean
78-
GrpcServiceDiscoverer grpcServiceDiscoverer(ObjectProvider<BindableService> bindableServicesProvider) {
79-
return new DefaultGrpcServiceDiscoverer(bindableServicesProvider);
80+
GrpcServiceDiscoverer grpcServiceDiscoverer(ObjectProvider<BindableService> bindableServicesProvider,
81+
ObjectProvider<ServerInterceptor> serverInterceptorsProvider, ApplicationContext applicationContext) {
82+
return new DefaultGrpcServiceDiscoverer(bindableServicesProvider, serverInterceptorsProvider,
83+
applicationContext);
8084
}
8185

8286
@ConditionalOnBean(CompressorRegistry.class)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/*
2+
* Copyright 2023-2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.grpc.autoconfigure.server;
18+
19+
import java.util.List;
20+
21+
import io.grpc.BindableService;
22+
import io.grpc.ServerInterceptor;
23+
import io.grpc.ServerInterceptors;
24+
import io.grpc.ServerServiceDefinition;
25+
import org.assertj.core.api.InstanceOfAssertFactories;
26+
import org.junit.jupiter.api.Test;
27+
import org.mockito.ArgumentCaptor;
28+
import org.mockito.MockedStatic;
29+
import org.mockito.Mockito;
30+
import org.mockito.stubbing.Answer;
31+
32+
import org.springframework.boot.autoconfigure.AutoConfigurations;
33+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
34+
import org.springframework.context.annotation.Bean;
35+
import org.springframework.context.annotation.Configuration;
36+
import org.springframework.core.annotation.Order;
37+
import org.springframework.grpc.server.lifecycle.GrpcServerLifecycle;
38+
39+
import static org.assertj.core.api.Assertions.assertThat;
40+
import static org.mockito.ArgumentMatchers.any;
41+
import static org.mockito.ArgumentMatchers.anyList;
42+
import static org.mockito.Mockito.mock;
43+
import static org.mockito.Mockito.times;
44+
import static org.mockito.Mockito.when;
45+
46+
/**
47+
* Tests for {@link DefaultGrpcServiceDiscoverer}.
48+
*
49+
* @author Chris Bono
50+
*/
51+
class DefaultGrpcServiceDiscovererTests {
52+
53+
private ApplicationContextRunner contextRunner() {
54+
// NOTE: we use noop server lifecycle to avoid startup
55+
return new ApplicationContextRunner()
56+
.withConfiguration(AutoConfigurations.of(GrpcServerAutoConfiguration.class))
57+
.withBean("noopServerLifecycle", GrpcServerLifecycle.class, Mockito::mock);
58+
}
59+
60+
@Test
61+
void globalServerInterceptorsAreFoundInProperOrder() {
62+
this.contextRunner()
63+
.withUserConfiguration(GlobalServerInterceptorsConfig.class)
64+
.run((context) -> assertThat(context).getBean(DefaultGrpcServiceDiscoverer.class)
65+
.extracting(DefaultGrpcServiceDiscoverer::findGlobalInterceptors, InstanceOfAssertFactories.LIST)
66+
.containsExactly(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
67+
GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO));
68+
}
69+
70+
@Test
71+
void servicesAreFoundInProperOrderWithGlobalInterceptorsApplied() {
72+
// It gets difficult to verify interceptors are added properly to mocked services.
73+
// To make it easier, we just static mock ServerInterceptors.interceptForward to
74+
// echo back the service def. This way we can verify the interceptors were passed
75+
// in the proper order as we rely/trust that ServerInterceptors.interceptForward
76+
// is
77+
// tested well in grpc-java.
78+
try (MockedStatic<ServerInterceptors> serverInterceptorsMocked = Mockito.mockStatic(ServerInterceptors.class)) {
79+
serverInterceptorsMocked
80+
.when(() -> ServerInterceptors.interceptForward(any(ServerServiceDefinition.class), anyList()))
81+
.thenAnswer((Answer<ServerServiceDefinition>) invocation -> invocation.getArgument(0));
82+
this.contextRunner().withUserConfiguration(GlobalServerInterceptorsConfig.class).run((context) -> {
83+
assertThat(context).getBean(DefaultGrpcServiceDiscoverer.class)
84+
.extracting(DefaultGrpcServiceDiscoverer::findServices, InstanceOfAssertFactories.LIST)
85+
.containsExactly(GlobalServerInterceptorsConfig.SERVICE_DEF_B,
86+
GlobalServerInterceptorsConfig.SERVICE_DEF_A);
87+
ArgumentCaptor<ServerServiceDefinition> serviceDefArg = ArgumentCaptor.captor();
88+
ArgumentCaptor<List<ServerInterceptor>> interceptorsArg = ArgumentCaptor.captor();
89+
serverInterceptorsMocked.verify(
90+
() -> ServerInterceptors.interceptForward(serviceDefArg.capture(), interceptorsArg.capture()),
91+
times(2));
92+
assertThat(serviceDefArg.getAllValues()).containsExactly(GlobalServerInterceptorsConfig.SERVICE_DEF_B,
93+
GlobalServerInterceptorsConfig.SERVICE_DEF_A);
94+
assertThat(interceptorsArg.getAllValues()).containsExactly(
95+
List.of(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
96+
GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO),
97+
List.of(GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
98+
GlobalServerInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO));
99+
});
100+
}
101+
}
102+
103+
@Configuration(proxyBeanMethods = false)
104+
static class GlobalServerInterceptorsConfig {
105+
106+
static BindableService SERVICE_A = mock();
107+
108+
static ServerServiceDefinition SERVICE_DEF_A = mock();
109+
110+
static BindableService SERVICE_B = mock();
111+
112+
static ServerServiceDefinition SERVICE_DEF_B = mock();
113+
114+
static ServerInterceptor GLOBAL_INTERCEPTOR_FOO = mock();
115+
116+
static ServerInterceptor GLOBAL_INTERCEPTOR_IGNORED = mock();
117+
118+
static ServerInterceptor GLOBAL_INTERCEPTOR_BAR = mock();
119+
120+
@Bean
121+
@Order(200)
122+
BindableService serviceA() {
123+
when(SERVICE_A.bindService()).thenReturn(SERVICE_DEF_A);
124+
return SERVICE_A;
125+
}
126+
127+
@Bean
128+
@Order(100)
129+
BindableService serviceB() {
130+
when(SERVICE_B.bindService()).thenReturn(SERVICE_DEF_B);
131+
return SERVICE_B;
132+
}
133+
134+
@Bean
135+
@Order(200)
136+
@GlobalServerInterceptor
137+
ServerInterceptor globalInterceptorFoo() {
138+
return GLOBAL_INTERCEPTOR_FOO;
139+
}
140+
141+
@Bean
142+
@Order(150)
143+
ServerInterceptor globalInterceptorIgnored() {
144+
return GLOBAL_INTERCEPTOR_IGNORED;
145+
}
146+
147+
@Bean
148+
@Order(100)
149+
@GlobalServerInterceptor
150+
ServerInterceptor globalInterceptorBar() {
151+
return GLOBAL_INTERCEPTOR_BAR;
152+
}
153+
154+
}
155+
156+
}

0 commit comments

Comments
 (0)