Skip to content

Commit 161ad69

Browse files
therepaniconobc
authored andcommitted
Add client interceptor filter support
This allows a user to define a ClientInterceptorFilter bean that decides which client interceptors to apply to which GrpcChannelFactory. Resolves #195 Signed-off-by: Andrey Litvitski <[email protected]>
1 parent be6318f commit 161ad69

File tree

5 files changed

+114
-10
lines changed

5 files changed

+114
-10
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright 2023-2025 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+
package org.springframework.grpc.client;
17+
18+
import io.grpc.ClientInterceptor;
19+
20+
/**
21+
* Strategy to determine whether a {@link ClientInterceptor} should be included for a
22+
* given {@link GrpcChannelFactory}.
23+
*
24+
* @author Andrey Litvitski
25+
*/
26+
@FunctionalInterface
27+
public interface ClientInterceptorFilter {
28+
29+
/**
30+
* Determine whether the given {@link ClientInterceptor} should be included for the
31+
* provided {@link GrpcChannelFactory}.
32+
* @param interceptor the client interceptor under consideration.
33+
* @param channelFactory the channel factory in use.
34+
* @return {@code true} if the interceptor should be included; {@code false}
35+
* otherwise.
36+
*/
37+
boolean filter(ClientInterceptor interceptor, GrpcChannelFactory channelFactory);
38+
39+
}

spring-grpc-core/src/main/java/org/springframework/grpc/client/ClientInterceptorsConfigurer.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2024-2024 the original author or authors.
2+
* Copyright 2024-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
2121
import java.util.List;
2222

2323
import org.springframework.beans.factory.InitializingBean;
24+
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
2425
import org.springframework.context.ApplicationContext;
2526
import org.springframework.grpc.internal.ApplicationContextBeanLookupUtils;
2627

@@ -31,13 +32,16 @@
3132
* Configure a {@link ManagedChannelBuilder} with client interceptors.
3233
*
3334
* @author Chris Bono
35+
* @author Andrey Litvitski
3436
*/
3537
public class ClientInterceptorsConfigurer implements InitializingBean {
3638

3739
private final ApplicationContext applicationContext;
3840

3941
private List<ClientInterceptor> globalInterceptors;
4042

43+
private ClientInterceptorFilter interceptorFilter;
44+
4145
public ClientInterceptorsConfigurer(ApplicationContext applicationContext) {
4246
this.applicationContext = applicationContext;
4347
}
@@ -48,13 +52,18 @@ public ClientInterceptorsConfigurer(ApplicationContext applicationContext) {
4852
* @param interceptors the non-null list of interceptors to be applied to the channel
4953
* @param mergeWithGlobalInterceptors whether the provided interceptors should be
5054
* blended with the global interceptors.
55+
* @param factory the channel factory used to filter global interceptors
5156
*/
5257
protected void configureInterceptors(ManagedChannelBuilder<?> builder, List<ClientInterceptor> interceptors,
53-
boolean mergeWithGlobalInterceptors) {
58+
boolean mergeWithGlobalInterceptors, GrpcChannelFactory factory) {
5459
// Add global interceptors first
5560
List<ClientInterceptor> allInterceptors = new ArrayList<>(this.globalInterceptors);
5661
// Add specific interceptors
5762
allInterceptors.addAll(interceptors);
63+
// Filter all interceptors
64+
if (this.interceptorFilter != null) {
65+
allInterceptors.removeIf(interceptor -> !this.interceptorFilter.filter(interceptor, factory));
66+
}
5867
if (mergeWithGlobalInterceptors) {
5968
ApplicationContextBeanLookupUtils.sortBeansIncludingOrderAnnotation(this.applicationContext,
6069
ClientInterceptor.class, allInterceptors);
@@ -66,11 +75,21 @@ protected void configureInterceptors(ManagedChannelBuilder<?> builder, List<Clie
6675
@Override
6776
public void afterPropertiesSet() {
6877
this.globalInterceptors = findGlobalInterceptors();
78+
this.interceptorFilter = findInterceptorFilter();
6979
}
7080

7181
private List<ClientInterceptor> findGlobalInterceptors() {
7282
return ApplicationContextBeanLookupUtils.getBeansWithAnnotation(this.applicationContext,
7383
ClientInterceptor.class, GlobalClientInterceptor.class);
7484
}
7585

86+
private ClientInterceptorFilter findInterceptorFilter() {
87+
try {
88+
return this.applicationContext.getBean(ClientInterceptorFilter.class);
89+
}
90+
catch (NoSuchBeanDefinitionException ignored) {
91+
return null;
92+
}
93+
}
94+
7695
}

spring-grpc-core/src/main/java/org/springframework/grpc/client/DefaultGrpcChannelFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ public ManagedChannel createChannel(String target, ChannelBuilderOptions options
9696
T builder = newChannelBuilder(targetUri, this.credentials.getChannelCredentials(target));
9797
// Handle interceptors
9898
this.interceptorsConfigurer.configureInterceptors(builder, options.interceptors(),
99-
options.mergeWithGlobalInterceptors());
99+
options.mergeWithGlobalInterceptors(), this);
100100
// Handle customizers
101101
this.globalCustomizers.forEach((c) -> c.customize(target, builder));
102102
var customizer = options.<T>customizer();

spring-grpc-core/src/test/java/org/springframework/grpc/client/ClientInterceptorsConfigurerTests.java

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 the original author or authors.
2+
* Copyright 2023-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -43,6 +43,7 @@
4343
* Tests for {@link ClientInterceptorsConfigurer}.
4444
*
4545
* @author Chris Bono
46+
* @author Andrey Litvitski
4647
*/
4748
class ClientInterceptorsConfigurerTests {
4849

@@ -67,8 +68,9 @@ private void customizeContextAndRunConfigurer(
6768
List<ClientInterceptor> clientSpecificInterceptors, List<ClientInterceptor> expectedInterceptors) {
6869
ManagedChannelBuilder<?> builder = Mockito.mock();
6970
this.contextRunner().with(contextCustomizer).run((context) -> {
71+
var factory = Mockito.mock(GrpcChannelFactory.class);
7072
var configurer = context.getBean(ClientInterceptorsConfigurer.class);
71-
configurer.configureInterceptors(builder, clientSpecificInterceptors, true);
73+
configurer.configureInterceptors(builder, clientSpecificInterceptors, true, factory);
7274
// NOTE: the interceptors are called in reverse order per builder contract
7375
var expectedInterceptorsReversed = new ArrayList<>(expectedInterceptors);
7476
Collections.reverse(expectedInterceptorsReversed);
@@ -129,13 +131,14 @@ void whenBlendInterceptorsFalseThenGlobalInterceptorsAddedFirst() {
129131
ClientInterceptorsConfigurerTests.this.contextRunner()
130132
.withUserConfiguration(GlobalClientInterceptorsConfig.class, ClientSpecificInterceptorsConfig.class)
131133
.run((context) -> {
134+
var factory = Mockito.mock(GrpcChannelFactory.class);
132135
var interceptorA = context.getBean("interceptorA", ClientInterceptor.class);
133136
var interceptorB = context.getBean("interceptorB", ClientInterceptor.class);
134137
var clientSpecificInterceptors = List.of(interceptorB, interceptorA);
135138
var expectedInterceptors = List.of(GlobalClientInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
136139
GlobalClientInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO, interceptorB, interceptorA);
137140
var configurer = context.getBean(ClientInterceptorsConfigurer.class);
138-
configurer.configureInterceptors(builder, clientSpecificInterceptors, false);
141+
configurer.configureInterceptors(builder, clientSpecificInterceptors, false, factory);
139142
// NOTE: the interceptors are called in reverse order per builder
140143
// contract
141144
var expectedInterceptorsReversed = new ArrayList<>(expectedInterceptors);
@@ -151,13 +154,55 @@ void whenBlendInterceptorsTrueThenGlobalInterceptorsBlended() {
151154
ClientInterceptorsConfigurerTests.this.contextRunner()
152155
.withUserConfiguration(GlobalClientInterceptorsConfig.class, ClientSpecificInterceptorsConfig.class)
153156
.run((context) -> {
157+
var factory = Mockito.mock(GrpcChannelFactory.class);
154158
var interceptorA = context.getBean("interceptorA", ClientInterceptor.class);
155159
var interceptorB = context.getBean("interceptorB", ClientInterceptor.class);
156160
var clientSpecificInterceptors = List.of(interceptorB, interceptorA);
157161
var expectedInterceptors = List.of(GlobalClientInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
158162
interceptorB, GlobalClientInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO, interceptorA);
159163
var configurer = context.getBean(ClientInterceptorsConfigurer.class);
160-
configurer.configureInterceptors(builder, clientSpecificInterceptors, true);
164+
configurer.configureInterceptors(builder, clientSpecificInterceptors, true, factory);
165+
// NOTE: the interceptors are called in reverse order per builder
166+
// contract
167+
var expectedInterceptorsReversed = new ArrayList<>(expectedInterceptors);
168+
Collections.reverse(expectedInterceptorsReversed);
169+
verify(builder).intercept(expectedInterceptorsReversed);
170+
});
171+
}
172+
173+
}
174+
175+
@Nested
176+
class WithInterceptorFilters {
177+
178+
@Test
179+
void whenFilterExcludesOneGlobalInterceptor_thenBuilderGetsOnlyAllowedOnes() {
180+
ManagedChannelBuilder<?> builder = Mockito.mock();
181+
ClientInterceptorsConfigurerTests.this.contextRunner()
182+
.withUserConfiguration(GlobalClientInterceptorsConfig.class)
183+
.withBean(ClientInterceptorFilter.class,
184+
() -> (interceptor, __) -> interceptor == GlobalClientInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR)
185+
.run(context -> {
186+
var factory = Mockito.mock(GrpcChannelFactory.class);
187+
var configurer = context.getBean(ClientInterceptorsConfigurer.class);
188+
configurer.configureInterceptors(builder, List.of(), true, factory);
189+
var expectedInterceptors = List.of(GlobalClientInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR);
190+
verify(builder).intercept(expectedInterceptors);
191+
});
192+
}
193+
194+
@Test
195+
void whenFilterIncludesAllGlobalInterceptors_thenBuilderGetsOnlyAllowedOnes() {
196+
ManagedChannelBuilder<?> builder = Mockito.mock();
197+
ClientInterceptorsConfigurerTests.this.contextRunner()
198+
.withUserConfiguration(GlobalClientInterceptorsConfig.class)
199+
.withBean(ClientInterceptorFilter.class, () -> (interceptor, __) -> true)
200+
.run(context -> {
201+
var factory = Mockito.mock(GrpcChannelFactory.class);
202+
var configurer = context.getBean(ClientInterceptorsConfigurer.class);
203+
configurer.configureInterceptors(builder, List.of(), true, factory);
204+
var expectedInterceptors = List.of(GlobalClientInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
205+
GlobalClientInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO);
161206
// NOTE: the interceptors are called in reverse order per builder
162207
// contract
163208
var expectedInterceptorsReversed = new ArrayList<>(expectedInterceptors);

spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcChannelFactoryTests.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2023-2024 the original author or authors.
2+
* Copyright 2023-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -110,7 +110,7 @@ void whenOptionsContainNoInterceptorThenConfigurerInvokedWithNoInterceptor() {
110110
channel = channelFactory.createChannel(channelName);
111111
assertThat(channel).isNotNull();
112112
verify(configurer).configureInterceptors(any(ManagedChannelBuilder.class),
113-
assertArg((interceptors) -> assertThat(interceptors).isEmpty()), eq(false));
113+
assertArg((interceptors) -> assertThat(interceptors).isEmpty()), eq(false), eq(channelFactory));
114114
}
115115

116116
@Test
@@ -126,7 +126,8 @@ void whenOptionsContainInterceptorThenConfigurerInvokedWithInterceptor() {
126126
.withInterceptorsMerge(true));
127127
assertThat(channel).isNotNull();
128128
verify(configurer).configureInterceptors(any(ManagedChannelBuilder.class),
129-
assertArg((interceptors) -> assertThat(interceptors).containsExactly(interceptor)), eq(true));
129+
assertArg((interceptors) -> assertThat(interceptors).containsExactly(interceptor)), eq(true),
130+
eq(channelFactory));
130131
}
131132

132133
}

0 commit comments

Comments
 (0)