Add client interceptors to channel factory
This commit adds support for global and channel-specific client interceptors. Resolves #52 Signed-off-by: Chris Bono <chris.bono@gmail.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.grpc.internal.ApplicationContextBeanLookupUtils;
|
||||
|
||||
import io.grpc.ClientInterceptor;
|
||||
import io.grpc.ManagedChannelBuilder;
|
||||
|
||||
/**
|
||||
* Configure a {@link ManagedChannelBuilder} with client interceptors.
|
||||
*
|
||||
* @author Chris Bono
|
||||
*/
|
||||
public class ClientInterceptorsConfigurer implements InitializingBean {
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
private List<ClientInterceptor> globalInterceptors;
|
||||
|
||||
public ClientInterceptorsConfigurer(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a {@link ManagedChannelBuilder} with client interceptors.
|
||||
* @param builder the builder to configure
|
||||
* @param interceptors the non-null list of interceptors to be applied to the channel
|
||||
* @param mergeWithGlobalInterceptors whether the provided interceptors should be
|
||||
* blended with the global interceptors.
|
||||
*/
|
||||
protected void configureInterceptors(ManagedChannelBuilder<?> builder, List<ClientInterceptor> interceptors,
|
||||
boolean mergeWithGlobalInterceptors) {
|
||||
// Add global interceptors first
|
||||
List<ClientInterceptor> allInterceptors = new ArrayList<>(this.globalInterceptors);
|
||||
// Add specific interceptors
|
||||
allInterceptors.addAll(interceptors);
|
||||
if (mergeWithGlobalInterceptors) {
|
||||
ApplicationContextBeanLookupUtils.sortBeansIncludingOrderAnnotation(this.applicationContext,
|
||||
ClientInterceptor.class, allInterceptors);
|
||||
}
|
||||
Collections.reverse(allInterceptors);
|
||||
builder.intercept(allInterceptors);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
this.globalInterceptors = findGlobalInterceptors();
|
||||
}
|
||||
|
||||
private List<ClientInterceptor> findGlobalInterceptors() {
|
||||
return ApplicationContextBeanLookupUtils.getBeansWithAnnotation(this.applicationContext,
|
||||
ClientInterceptor.class, GlobalClientInterceptor.class);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import io.grpc.ChannelCredentials;
|
||||
import io.grpc.ClientInterceptor;
|
||||
import io.grpc.ForwardingChannelBuilder2;
|
||||
import io.grpc.Grpc;
|
||||
import io.grpc.ManagedChannel;
|
||||
@@ -47,17 +48,18 @@ public class DefaultGrpcChannelFactory implements GrpcChannelFactory, Disposable
|
||||
|
||||
private final List<GrpcChannelBuilderCustomizer> customizers = new ArrayList<>();
|
||||
|
||||
private final ClientInterceptorsConfigurer interceptorsConfigurer;
|
||||
|
||||
private ChannelCredentialsProvider credentials = ChannelCredentialsProvider.INSECURE;
|
||||
|
||||
private VirtualTargets targets = VirtualTargets.DEFAULT;
|
||||
|
||||
public DefaultGrpcChannelFactory() {
|
||||
this(List.of());
|
||||
}
|
||||
|
||||
public DefaultGrpcChannelFactory(List<GrpcChannelBuilderCustomizer> customizers) {
|
||||
public DefaultGrpcChannelFactory(List<GrpcChannelBuilderCustomizer> customizers,
|
||||
ClientInterceptorsConfigurer interceptorsConfigurer) {
|
||||
Assert.notNull(customizers, () -> "customizers must not be null");
|
||||
Assert.notNull(interceptorsConfigurer, () -> "interceptorsConfigurer must not be null");
|
||||
this.customizers.addAll(customizers);
|
||||
this.interceptorsConfigurer = interceptorsConfigurer;
|
||||
}
|
||||
|
||||
public void setVirtualTargets(VirtualTargets targets) {
|
||||
@@ -70,13 +72,20 @@ public class DefaultGrpcChannelFactory implements GrpcChannelFactory, Disposable
|
||||
|
||||
@Override
|
||||
public ManagedChannelBuilder<?> createChannel(String authority) {
|
||||
ManagedChannelBuilder<?> target = this.builders.computeIfAbsent(authority, path -> {
|
||||
ManagedChannelBuilder<?> builder = newChannel(this.targets.getTarget(path),
|
||||
return this.createChannel(authority, List.of(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ManagedChannelBuilder<?> createChannel(String authority, List<ClientInterceptor> interceptors,
|
||||
boolean mergeWithGlobalInterceptors) {
|
||||
Assert.notNull(interceptors, () -> "interceptors must not be null");
|
||||
return this.builders.computeIfAbsent(authority, path -> {
|
||||
ManagedChannelBuilder<?> builder = newChannelBuilder(this.targets.getTarget(path),
|
||||
this.credentials.getChannelCredentials(path));
|
||||
this.interceptorsConfigurer.configureInterceptors(builder, interceptors, mergeWithGlobalInterceptors);
|
||||
this.customizers.forEach((c) -> c.customize(path, builder));
|
||||
return builder;
|
||||
return new DisposableChannelBuilder(authority, builder);
|
||||
});
|
||||
return new DisposableChannelBuilder(authority, target);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,15 +95,17 @@ public class DefaultGrpcChannelFactory implements GrpcChannelFactory, Disposable
|
||||
* @param creds the credentials for the channel
|
||||
* @return a new {@link ManagedChannelBuilder} for the given path and credentials
|
||||
*/
|
||||
protected ManagedChannelBuilder<?> newChannel(String path, ChannelCredentials creds) {
|
||||
protected ManagedChannelBuilder<?> newChannelBuilder(String path, ChannelCredentials creds) {
|
||||
return Grpc.newChannelBuilder(path, creds);
|
||||
}
|
||||
|
||||
private ManagedChannel buildAndRegisterChannel(String channelName, ManagedChannelBuilder<?> channelBuilder) {
|
||||
return this.channels.computeIfAbsent(channelName, (__) -> channelBuilder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
for (ManagedChannel channel : this.channels.values()) {
|
||||
channel.shutdown();
|
||||
}
|
||||
this.channels.values().forEach(ManagedChannel::shutdown);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,10 +114,10 @@ public class DefaultGrpcChannelFactory implements GrpcChannelFactory, Disposable
|
||||
*/
|
||||
class DisposableChannelBuilder extends ForwardingChannelBuilder2<DisposableChannelBuilder> {
|
||||
|
||||
private final ManagedChannelBuilder<?> delegate;
|
||||
|
||||
private final String authority;
|
||||
|
||||
private final ManagedChannelBuilder<?> delegate;
|
||||
|
||||
DisposableChannelBuilder(String authority, ManagedChannelBuilder<?> delegate) {
|
||||
this.authority = authority;
|
||||
this.delegate = delegate;
|
||||
@@ -119,7 +130,7 @@ public class DefaultGrpcChannelFactory implements GrpcChannelFactory, Disposable
|
||||
|
||||
@Override
|
||||
public ManagedChannel build() {
|
||||
return DefaultGrpcChannelFactory.this.channels.computeIfAbsent(this.authority, name -> super.build());
|
||||
return DefaultGrpcChannelFactory.this.buildAndRegisterChannel(this.authority, this.delegate);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
* Partial copy from net.devh:grpc-spring-boot-starter.
|
||||
*/
|
||||
|
||||
package org.springframework.grpc.client;
|
||||
|
||||
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.core.annotation.Order;
|
||||
|
||||
import io.grpc.ClientInterceptor;
|
||||
|
||||
/**
|
||||
* Annotation that can be specified on a gRPC {@link ClientInterceptor} bean which will
|
||||
* result in the interceptor being applied globally to channels.
|
||||
* <p>
|
||||
* The bean interceptor {@link Order} will be respected.
|
||||
*
|
||||
* @author Daniel Theuke (daniel.theuke@heuboe.de)
|
||||
* @author Chris Bono
|
||||
*/
|
||||
@Target({ ElementType.TYPE, ElementType.METHOD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
public @interface GlobalClientInterceptor {
|
||||
|
||||
}
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.grpc.client;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import io.grpc.ClientInterceptor;
|
||||
import io.grpc.ManagedChannelBuilder;
|
||||
|
||||
/**
|
||||
@@ -34,4 +37,16 @@ public interface GrpcChannelFactory {
|
||||
*/
|
||||
ManagedChannelBuilder<?> createChannel(String authority);
|
||||
|
||||
/**
|
||||
* Creates a {@link ManagedChannelBuilder} for the given authority and the provided
|
||||
* client interceptors.
|
||||
* @param authority the target authority for the channel
|
||||
* @param interceptors the non-null list of interceptors to be applied to the channel
|
||||
* @param mergeWithGlobalInterceptors whether the provided interceptors should be
|
||||
* blended with the global interceptors.
|
||||
* @return a channel builder conifgured with the provided values
|
||||
*/
|
||||
ManagedChannelBuilder<?> createChannel(String authority, List<ClientInterceptor> interceptors,
|
||||
boolean mergeWithGlobalInterceptors);
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
|
||||
import io.grpc.ClientInterceptor;
|
||||
import io.grpc.ManagedChannelBuilder;
|
||||
|
||||
/**
|
||||
* Tests for {@link ClientInterceptorsConfigurer}.
|
||||
*
|
||||
* @author Chris Bono
|
||||
*/
|
||||
class ClientInterceptorsConfigurerTests {
|
||||
|
||||
private ApplicationContextRunner contextRunner() {
|
||||
return new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(ClientInterceptorsConfigurerConfig.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void globalClientInterceptorsAreFoundInProperOrder() {
|
||||
this.contextRunner()
|
||||
.withUserConfiguration(GlobalClientInterceptorsConfig.class)
|
||||
.run((context) -> Assertions.assertThat(context)
|
||||
.getBean(ClientInterceptorsConfigurer.class)
|
||||
.extracting("globalInterceptors", InstanceOfAssertFactories.LIST)
|
||||
.containsExactly(GlobalClientInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
|
||||
GlobalClientInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO));
|
||||
}
|
||||
|
||||
private void customizeContextAndRunConfigurer(
|
||||
Function<ApplicationContextRunner, ApplicationContextRunner> contextCustomizer,
|
||||
List<ClientInterceptor> clientSpecificInterceptors, List<ClientInterceptor> expectedInterceptors) {
|
||||
ManagedChannelBuilder<?> builder = Mockito.mock();
|
||||
this.contextRunner().with(contextCustomizer).run((context) -> {
|
||||
var configurer = context.getBean(ClientInterceptorsConfigurer.class);
|
||||
configurer.configureInterceptors(builder, clientSpecificInterceptors, true);
|
||||
// NOTE: the interceptors are called in reverse order per builder contract
|
||||
var expectedInterceptorsReversed = new ArrayList<>(expectedInterceptors);
|
||||
Collections.reverse(expectedInterceptorsReversed);
|
||||
verify(builder).intercept(expectedInterceptorsReversed);
|
||||
});
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithOnlyGlobalInterceptors {
|
||||
|
||||
@Test
|
||||
void whenNoGlobalInterceptorsRegisteredThenBuilderGetsNoInterceptors() {
|
||||
customizeContextAndRunConfigurer(Function.identity(), List.of(), List.of());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenGlobalInterceptorsRegisteredThenBuilderGetsGlobalInterceptors() {
|
||||
customizeContextAndRunConfigurer(
|
||||
(contextRunner) -> contextRunner.withUserConfiguration(GlobalClientInterceptorsConfig.class),
|
||||
List.of(), List.of(GlobalClientInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
|
||||
GlobalClientInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithOnlyClientSpecificInterceptors {
|
||||
|
||||
@Test
|
||||
void whenSingleInterceptorSpecifiedThenItIsUsed() {
|
||||
var clientSpecificInterceptors = List
|
||||
.<ClientInterceptor>of(ClientSpecificInterceptorsConfig.SVC_INTERCEPTOR_A);
|
||||
var expectedInterceptors = List.<ClientInterceptor>of(ClientSpecificInterceptorsConfig.SVC_INTERCEPTOR_A);
|
||||
customizeContextAndRunConfigurer(
|
||||
(contextRunner) -> contextRunner.withUserConfiguration(ClientSpecificInterceptorsConfig.class),
|
||||
clientSpecificInterceptors, expectedInterceptors);
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenMultipleInterceptorsSpecifiedThenTheyAreUsed() {
|
||||
var clientSpecificInterceptors = List.of(ClientSpecificInterceptorsConfig.SVC_INTERCEPTOR_B,
|
||||
ClientSpecificInterceptorsConfig.SVC_INTERCEPTOR_A);
|
||||
var expectedInterceptors = List.of(ClientSpecificInterceptorsConfig.SVC_INTERCEPTOR_B,
|
||||
ClientSpecificInterceptorsConfig.SVC_INTERCEPTOR_A);
|
||||
customizeContextAndRunConfigurer(
|
||||
(contextRunner) -> contextRunner.withUserConfiguration(ClientSpecificInterceptorsConfig.class),
|
||||
clientSpecificInterceptors, expectedInterceptors);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithClientSpecificCombinedWithGlobalInterceptors {
|
||||
|
||||
@Test
|
||||
void whenBlendInterceptorsFalseThenGlobalInterceptorsAddedFirst() {
|
||||
ManagedChannelBuilder<?> builder = Mockito.mock();
|
||||
ClientInterceptorsConfigurerTests.this.contextRunner()
|
||||
.withUserConfiguration(GlobalClientInterceptorsConfig.class, ClientSpecificInterceptorsConfig.class)
|
||||
.run((context) -> {
|
||||
var interceptorA = context.getBean("interceptorA", ClientInterceptor.class);
|
||||
var interceptorB = context.getBean("interceptorB", ClientInterceptor.class);
|
||||
var clientSpecificInterceptors = List.of(interceptorB, interceptorA);
|
||||
var expectedInterceptors = List.of(GlobalClientInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
|
||||
GlobalClientInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO, interceptorB, interceptorA);
|
||||
var configurer = context.getBean(ClientInterceptorsConfigurer.class);
|
||||
configurer.configureInterceptors(builder, clientSpecificInterceptors, false);
|
||||
// NOTE: the interceptors are called in reverse order per builder
|
||||
// contract
|
||||
var expectedInterceptorsReversed = new ArrayList<>(expectedInterceptors);
|
||||
Collections.reverse(expectedInterceptorsReversed);
|
||||
verify(builder).intercept(expectedInterceptorsReversed);
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void whenBlendInterceptorsTrueThenGlobalInterceptorsBlended() {
|
||||
ManagedChannelBuilder<?> builder = Mockito.mock();
|
||||
ClientInterceptorsConfigurerTests.this.contextRunner()
|
||||
.withUserConfiguration(GlobalClientInterceptorsConfig.class, ClientSpecificInterceptorsConfig.class)
|
||||
.run((context) -> {
|
||||
var interceptorA = context.getBean("interceptorA", ClientInterceptor.class);
|
||||
var interceptorB = context.getBean("interceptorB", ClientInterceptor.class);
|
||||
var clientSpecificInterceptors = List.of(interceptorB, interceptorA);
|
||||
var expectedInterceptors = List.of(GlobalClientInterceptorsConfig.GLOBAL_INTERCEPTOR_BAR,
|
||||
interceptorB, GlobalClientInterceptorsConfig.GLOBAL_INTERCEPTOR_FOO, interceptorA);
|
||||
var configurer = context.getBean(ClientInterceptorsConfigurer.class);
|
||||
configurer.configureInterceptors(builder, clientSpecificInterceptors, true);
|
||||
// NOTE: the interceptors are called in reverse order per builder
|
||||
// contract
|
||||
var expectedInterceptorsReversed = new ArrayList<>(expectedInterceptors);
|
||||
Collections.reverse(expectedInterceptorsReversed);
|
||||
verify(builder).intercept(expectedInterceptorsReversed);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface TestClientInterceptorA extends ClientInterceptor {
|
||||
|
||||
}
|
||||
|
||||
interface TestClientInterceptorB extends ClientInterceptor {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ClientInterceptorsConfigurerConfig {
|
||||
|
||||
@Bean
|
||||
ClientInterceptorsConfigurer clientInterceptorsConfigurer(ApplicationContext applicationContext) {
|
||||
return new ClientInterceptorsConfigurer(applicationContext);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class GlobalClientInterceptorsConfig {
|
||||
|
||||
static ClientInterceptor GLOBAL_INTERCEPTOR_FOO = Mockito.mock();
|
||||
|
||||
static ClientInterceptor GLOBAL_INTERCEPTOR_IGNORED = Mockito.mock();
|
||||
|
||||
static ClientInterceptor GLOBAL_INTERCEPTOR_BAR = Mockito.mock();
|
||||
|
||||
@Bean
|
||||
@Order(200)
|
||||
@GlobalClientInterceptor
|
||||
ClientInterceptor globalInterceptorFoo() {
|
||||
return GLOBAL_INTERCEPTOR_FOO;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(150)
|
||||
ClientInterceptor globalInterceptorIgnored() {
|
||||
return GLOBAL_INTERCEPTOR_IGNORED;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(100)
|
||||
@GlobalClientInterceptor
|
||||
ClientInterceptor globalInterceptorBar() {
|
||||
return GLOBAL_INTERCEPTOR_BAR;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ClientSpecificInterceptorsConfig {
|
||||
|
||||
static TestClientInterceptorB SVC_INTERCEPTOR_B = Mockito.mock();
|
||||
|
||||
static TestClientInterceptorA SVC_INTERCEPTOR_A = Mockito.mock();
|
||||
|
||||
@Bean
|
||||
@Order(150)
|
||||
TestClientInterceptorB interceptorB() {
|
||||
return SVC_INTERCEPTOR_B;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Order(225)
|
||||
TestClientInterceptorA interceptorA() {
|
||||
return SVC_INTERCEPTOR_A;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 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.client;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.grpc.client.DefaultGrpcChannelFactory.DisposableChannelBuilder;
|
||||
|
||||
import io.grpc.ClientInterceptor;
|
||||
|
||||
/**
|
||||
* Tests for {@link DefaultGrpcChannelFactory}.
|
||||
*/
|
||||
class DefaultGrpcChannelFactoryTests {
|
||||
|
||||
@Test
|
||||
void createChannelWithoutClientSpecificInterceptorsInvokesInterceptorConfigurer() {
|
||||
ClientInterceptorsConfigurer configurer = mock();
|
||||
var channelName = "testChannel";
|
||||
var channelFactory = new DefaultGrpcChannelFactory(List.of(), configurer);
|
||||
channelFactory.createChannel(channelName);
|
||||
// Get the actual builder that should be passed into the configurer
|
||||
assertThat(channelFactory)
|
||||
.extracting("builders", InstanceOfAssertFactories.map(String.class, DisposableChannelBuilder.class))
|
||||
.hasEntrySatisfying(channelName,
|
||||
(builder) -> verify(configurer).configureInterceptors(builder.delegate(), List.of(), false));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createChannelWithClientSpecificInterceptorsInvokesInterceptorConfigurer() {
|
||||
ClientInterceptorsConfigurer configurer = mock();
|
||||
var channelName = "testChannel";
|
||||
var channelFactory = new DefaultGrpcChannelFactory(List.of(), configurer);
|
||||
var interceptor = mock(ClientInterceptor.class);
|
||||
var interceptors = List.of(interceptor);
|
||||
channelFactory.createChannel(channelName, interceptors, true);
|
||||
// Get the actual builder that should be passed into the configurer
|
||||
assertThat(channelFactory)
|
||||
.extracting("builders", InstanceOfAssertFactories.map(String.class, DisposableChannelBuilder.class))
|
||||
.hasEntrySatisfying(channelName,
|
||||
(builder) -> verify(configurer).configureInterceptors(builder.delegate(), interceptors, true));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -100,3 +100,34 @@ SimpleGrpc.SimpleBlockingStub stub(GrpcChannelFactory channels, @LocalGrpcPort i
|
||||
return SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:" + port).build());
|
||||
}
|
||||
----
|
||||
|
||||
[[client-interceptor]]
|
||||
== Client Interceptors
|
||||
|
||||
=== Global
|
||||
To add a client interceptor to be applied to all created channels you can simply register a client interceptor bean and then annotate it with `@GlobalClientInterceptor`.
|
||||
The interceptors are ordered according to their bean natural ordering (i.e. `@Order`).
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
@Order(100)
|
||||
@GlobalClientInterceptor
|
||||
ClientInterceptor myGlobalLoggingInterceptor() {
|
||||
return new MyLoggingInterceptor();
|
||||
}
|
||||
----
|
||||
|
||||
=== Per-Channel
|
||||
To add one or more client interceptors to be applied to a single client channel you can simply pass in the interceptor instance(s) when invoking the channel factory to create the channel.
|
||||
|
||||
The interceptors are ordered according to their position in the specified list.
|
||||
|
||||
=== Blended
|
||||
When a channel is constructed with both global and per-client 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 `mergeWithGlobalInterceptors` parameter on the channel factory 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` or `Ordered` interface).
|
||||
|
||||
You can use this option if you want to add a per-client interceptor between global interceptors.
|
||||
|
||||
IMPORTANT: The per-channel interceptors you pass in must either be bean instances marked with `@Order` or regular objects that implement the `Ordered` interface to be properly merged/ordered with the global interceptors.
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
This section covers the changes made from version 0.2.0 to version 0.3.0.
|
||||
|
||||
=== Client Interceptors
|
||||
You can now add xref:client.adoc#client-interceptor[Client Interceptors] to created gRPC channels.
|
||||
|
||||
=== Breaking Changes
|
||||
|
||||
|
||||
@@ -21,12 +21,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.grpc.autoconfigure.client.GrpcClientProperties.NamedChannel;
|
||||
import org.springframework.grpc.autoconfigure.common.codec.GrpcCodecConfiguration;
|
||||
import org.springframework.grpc.client.ChannelCredentialsProvider;
|
||||
import org.springframework.grpc.client.ClientInterceptorsConfigurer;
|
||||
import org.springframework.grpc.client.DefaultGrpcChannelFactory;
|
||||
import org.springframework.grpc.client.GrpcChannelBuilderCustomizer;
|
||||
import org.springframework.grpc.client.GrpcChannelFactory;
|
||||
@@ -40,11 +42,18 @@ import io.grpc.DecompressorRegistry;
|
||||
@Import(GrpcCodecConfiguration.class)
|
||||
public class GrpcClientAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
ClientInterceptorsConfigurer clientInterceptorsConfigurer(ApplicationContext applicationContext) {
|
||||
return new ClientInterceptorsConfigurer(applicationContext);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(GrpcChannelFactory.class)
|
||||
public DefaultGrpcChannelFactory defaultGrpcChannelFactory(List<GrpcChannelBuilderCustomizer> customizers,
|
||||
ChannelCredentialsProvider credentials, GrpcClientProperties channels, SslBundles ignored) {
|
||||
DefaultGrpcChannelFactory factory = new DefaultGrpcChannelFactory(customizers);
|
||||
ClientInterceptorsConfigurer interceptorsConfigurer, ChannelCredentialsProvider credentials,
|
||||
GrpcClientProperties channels, SslBundles ignored) {
|
||||
DefaultGrpcChannelFactory factory = new DefaultGrpcChannelFactory(customizers, interceptorsConfigurer);
|
||||
factory.setCredentialsProvider(credentials);
|
||||
factory.setVirtualTargets(new NamedChannelVirtualTargets(channels));
|
||||
return factory;
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
*/
|
||||
package org.springframework.grpc.test;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.grpc.client.ClientInterceptorsConfigurer;
|
||||
import org.springframework.grpc.client.DefaultGrpcChannelFactory;
|
||||
|
||||
import io.grpc.ChannelCredentials;
|
||||
@@ -23,8 +26,12 @@ import io.grpc.inprocess.InProcessChannelBuilder;
|
||||
|
||||
public class InProcessGrpcChannelFactory extends DefaultGrpcChannelFactory {
|
||||
|
||||
public InProcessGrpcChannelFactory(ClientInterceptorsConfigurer interceptorsConfigurer) {
|
||||
super(List.of(), interceptorsConfigurer);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ManagedChannelBuilder<?> newChannel(String path, ChannelCredentials creds) {
|
||||
protected ManagedChannelBuilder<?> newChannelBuilder(String path, ChannelCredentials creds) {
|
||||
return InProcessChannelBuilder.forName(path);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,11 +20,14 @@ import java.util.List;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnNotWebApplication;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.grpc.autoconfigure.client.GrpcClientAutoConfiguration;
|
||||
import org.springframework.grpc.autoconfigure.server.GrpcServerFactoryAutoConfiguration;
|
||||
import org.springframework.grpc.client.ClientInterceptorsConfigurer;
|
||||
import org.springframework.grpc.server.ServerBuilderCustomizer;
|
||||
import org.springframework.grpc.server.service.GrpcServiceDiscoverer;
|
||||
|
||||
@@ -37,7 +40,7 @@ import io.grpc.inprocess.InProcessServerBuilder;
|
||||
@ConditionalOnNotWebApplication
|
||||
public class InProcessGrpcServerFactoryAutoConfiguration {
|
||||
|
||||
private String address = InProcessServerBuilder.generateName();
|
||||
private final String address = InProcessServerBuilder.generateName();
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(BindableService.class)
|
||||
@@ -49,8 +52,14 @@ public class InProcessGrpcServerFactoryAutoConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
InProcessGrpcChannelFactory grpcChannelFactory() {
|
||||
InProcessGrpcChannelFactory factory = new InProcessGrpcChannelFactory();
|
||||
@ConditionalOnMissingBean
|
||||
ClientInterceptorsConfigurer inProcessClientInterceptorsConfigurer(ApplicationContext applicationContext) {
|
||||
return new ClientInterceptorsConfigurer(applicationContext);
|
||||
}
|
||||
|
||||
@Bean
|
||||
InProcessGrpcChannelFactory grpcChannelFactory(ClientInterceptorsConfigurer interceptorsConfigurer) {
|
||||
InProcessGrpcChannelFactory factory = new InProcessGrpcChannelFactory(interceptorsConfigurer);
|
||||
factory.setVirtualTargets(path -> address);
|
||||
return factory;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user