Split bean registration and creation for stubs

GrpcClientRegistry was responsible for both, which causes lifecycle
issues when users don't follow recommendations. This change
pushes the bean registration firmly down a level into an
ImportBeanDefinitionRegistrar. Also helps with AOT because the AOT
processor only runs the IBDR at build time.
This commit is contained in:
Dave Syer
2025-04-01 11:49:59 +01:00
parent 326c075b36
commit a3d7ba643d
13 changed files with 437 additions and 492 deletions

View File

@@ -22,7 +22,7 @@ import org.springframework.experimental.boot.test.context.EnableDynamicProperty;
import org.springframework.experimental.boot.test.context.OAuth2ClientProviderIssuerUri;
import org.springframework.grpc.client.ChannelBuilderOptions;
import org.springframework.grpc.client.ImportGrpcClients;
import org.springframework.grpc.client.GrpcClientRegistryCustomizer;
import org.springframework.grpc.client.GrpcClientFactoryCustomizer;
import org.springframework.grpc.client.interceptor.security.BearerTokenAuthenticationInterceptor;
import org.springframework.grpc.sample.proto.HelloReply;
import org.springframework.grpc.sample.proto.HelloRequest;
@@ -41,7 +41,7 @@ import io.grpc.reflection.v1.ServerReflectionResponse;
import io.grpc.stub.StreamObserver;
@SpringBootTest(properties = { "spring.grpc.server.port=0",
"spring.grpc.client.channels.stub.address=static://0.0.0.0:${local.grpc.port}" })
"spring.grpc.client.default-channel.address=static://0.0.0.0:${local.grpc.port}" })
@DirtiesContext
public class GrpcServerApplicationTests {
@@ -115,6 +115,7 @@ public class GrpcServerApplicationTests {
@EnableDynamicProperty
@ImportGrpcClients(target = "stub",
types = { SimpleGrpc.SimpleBlockingStub.class, ServerReflectionGrpc.ServerReflectionStub.class })
@ImportGrpcClients(target = "secure", prefix = "secure", types = { SimpleGrpc.SimpleBlockingStub.class })
static class ExtraConfiguration {
private String token;
@@ -129,13 +130,9 @@ public class GrpcServerApplicationTests {
}
@Bean
GrpcClientRegistryCustomizer stubs(ObjectProvider<ClientRegistrationRepository> context) {
return registry -> registry
.channel("stub",
ChannelBuilderOptions.defaults()
.withInterceptors(List.of(new BearerTokenAuthenticationInterceptor(() -> token(context)))))
.prefix("secure")
.register(SimpleGrpc.SimpleBlockingStub.class);
GrpcClientFactoryCustomizer stubs(ObjectProvider<ClientRegistrationRepository> context) {
return registry -> registry.channel("secure", ChannelBuilderOptions.defaults()
.withInterceptors(List.of(new BearerTokenAuthenticationInterceptor(() -> token(context)))));
}
private String token(ObjectProvider<ClientRegistrationRepository> context) {

View File

@@ -15,10 +15,9 @@ import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.grpc.client.BlockingStubFactory;
import org.springframework.grpc.client.ChannelBuilderOptions;
import org.springframework.grpc.client.GrpcClientFactoryCustomizer;
import org.springframework.grpc.client.ImportGrpcClients;
import org.springframework.grpc.client.GrpcClientRegistryCustomizer;
import org.springframework.grpc.client.interceptor.security.BasicAuthenticationInterceptor;
import org.springframework.grpc.sample.proto.HelloReply;
import org.springframework.grpc.sample.proto.HelloRequest;
@@ -109,17 +108,14 @@ public class GrpcServerApplicationTests {
@TestConfiguration(proxyBeanMethods = false)
@ImportGrpcClients(target = "stub", prefix = "unsecured", types = { SimpleGrpc.SimpleBlockingStub.class })
@ImportGrpcClients(target = "secure", types = { SimpleGrpc.SimpleBlockingStub.class })
@ImportGrpcClients(target = "default", types = { ServerReflectionGrpc.ServerReflectionStub.class })
static class ExtraConfiguration {
@Bean
GrpcClientRegistryCustomizer basicStubs() {
return registry -> registry
.channel("stub",
ChannelBuilderOptions.defaults()
.withInterceptors(List.of(new BasicAuthenticationInterceptor("user", "user"))))
.scan(BlockingStubFactory.class)
.packageClasses(SimpleGrpc.class);
GrpcClientFactoryCustomizer basicStubs() {
return registry -> registry.channel("secure", ChannelBuilderOptions.defaults()
.withInterceptors(List.of(new BasicAuthenticationInterceptor("user", "user"))));
}
}

View File

@@ -0,0 +1,40 @@
/*
* 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 org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.grpc.client.GrpcClientFactory.GrpcClientRegistrationSpec;
public abstract class AbstractGrpcClientRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public final void registerBeanDefinitions(AnnotationMetadata meta, BeanDefinitionRegistry registry) {
GrpcClientRegistrationSpec[] specs = collect(meta);
String name = GrpcClientFactoryPostProcessor.class.getName();
for (GrpcClientRegistrationSpec spec : specs) {
GrpcClientFactory.register(registry, spec);
}
if (!registry.containsBeanDefinition(name)) {
registry.registerBeanDefinition(name, new RootBeanDefinition(GrpcClientFactoryPostProcessor.class));
}
}
protected abstract GrpcClientRegistrationSpec[] collect(AnnotationMetadata meta);
}

View File

@@ -0,0 +1,60 @@
/*
* 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.HashSet;
import java.util.Set;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.grpc.client.GrpcClientFactory.GrpcClientRegistrationSpec;
import org.springframework.util.ClassUtils;
public class AnnotationGrpcClientRegistrar extends AbstractGrpcClientRegistrar {
@Override
protected GrpcClientRegistrationSpec[] collect(AnnotationMetadata meta) {
Set<AnnotationAttributes> attrs = meta.getMergedRepeatableAnnotationAttributes(ImportGrpcClients.class,
ImportGrpcClients.Container.class, false);
Set<GrpcClientRegistrationSpec> specs = new HashSet<>();
for (AnnotationAttributes attr : attrs) {
specs.add(register(meta, attr));
}
return specs.toArray(new GrpcClientRegistrationSpec[0]);
}
private GrpcClientRegistrationSpec register(AnnotationMetadata meta, AnnotationAttributes attr) {
String target = attr.getString("target");
String prefix = attr.getString("prefix");
Class<?>[] types = attr.getClassArray("types");
Class<?>[] basePackageClasses = attr.getClassArray("basePackageClasses");
String[] basePackages = attr.getStringArray("basePackages");
Class<? extends StubFactory<?>> factory = attr.getClass("factory");
if (factory == UnspecifiedStubFactory.class) {
factory = null;
}
if (types.length == 0 && basePackageClasses.length == 0 && basePackages.length == 0) {
basePackages = new String[] { ClassUtils.getPackageName(meta.getClassName()) };
}
return GrpcClientRegistrationSpec.of(target)
.factory(factory)
.types(types)
.packages(basePackages)
.packageClasses(basePackageClasses)
.prefix(prefix);
}
}

View File

@@ -1,107 +0,0 @@
/*
* 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.Set;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.ClassUtils;
public class GrpcClientConfiguration implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata meta, BeanDefinitionRegistry registry) {
Set<AnnotationAttributes> attrs = meta.getMergedRepeatableAnnotationAttributes(ImportGrpcClients.class,
ImportGrpcClients.Container.class, false);
for (AnnotationAttributes attr : attrs) {
register(registry, meta, attr, meta.getClassName() + ".");
}
String name = GrpcClientRegistryPostProcessor.class.getName();
if (!registry.containsBeanDefinition(name)) {
registry.registerBeanDefinition(name, new RootBeanDefinition(GrpcClientRegistryPostProcessor.class));
}
}
private void register(BeanDefinitionRegistry registry, AnnotationMetadata meta, AnnotationAttributes attr,
String stem) {
String target = attr.getString("target");
String prefix = attr.getString("prefix");
Class<?>[] types = attr.getClassArray("types");
Class<? extends StubFactory<?>> factory = attr.getClass("factory");
Class<?>[] basePackageClasses = attr.getClassArray("basePackageClasses");
String[] basePackages = attr.getStringArray("basePackages");
if (types.length == 0 && basePackageClasses.length == 0 && basePackages.length == 0) {
basePackages = new String[] { ClassUtils.getPackageName(meta.getClassName()) };
}
register(registry, stem, target, prefix, types, factory, basePackageClasses, basePackages);
}
private void register(BeanDefinitionRegistry registry, String stem, String target, String prefix, Class<?>[] types,
Class<? extends StubFactory<?>> factory, Class<?>[] basePackageClasses, String[] basePackages) {
RootBeanDefinition beanDef = new RootBeanDefinition(SimpleGrpcClientRegistryCustomizer.class);
beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
String name = target;
beanDef.setBeanClass(SimpleGrpcClientRegistryCustomizer.class);
ConstructorArgumentValues values = beanDef.getConstructorArgumentValues();
values.addGenericArgumentValue(name);
values.addGenericArgumentValue(prefix);
values.addGenericArgumentValue(types);
values.addGenericArgumentValue(factory);
values.addGenericArgumentValue(basePackageClasses);
values.addGenericArgumentValue(basePackages);
registry.registerBeanDefinition(stem + target, beanDef);
}
static class SimpleGrpcClientRegistryCustomizer implements GrpcClientRegistryCustomizer {
private String target;
private Class<?>[] types;
private String prefix;
private Class<?>[] basePackageClasses;
private Class<? extends StubFactory<?>> type;
private String[] basePackages;
SimpleGrpcClientRegistryCustomizer(String target, String prefix, Class<?>[] types,
Class<? extends StubFactory<?>> type, Class<?>[] basePackageClasses, String[] basePackages) {
this.target = target;
this.prefix = prefix;
this.types = types;
this.type = type;
this.basePackageClasses = basePackageClasses;
this.basePackages = basePackages;
}
@Override
public void customize(GrpcClientRegistry registry) {
registry.channel(this.target).prefix(this.prefix).register(this.types);
registry.channel(this.target).prefix(this.prefix).scan(this.type).packageClasses(this.basePackageClasses);
registry.channel(this.target).prefix(this.prefix).scan(this.type).packages(this.basePackages);
}
}
}

View File

@@ -0,0 +1,210 @@
/*
* 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.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.grpc.internal.ClasspathScanner;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import io.grpc.ManagedChannel;
import io.grpc.stub.AbstractStub;
/**
* A factory of gRPC clients that can be used to create client stubs as beans in an
* application context. The best way to interact with the factory is to declare a bean of
* type {@link GrpcClientFactoryCustomizer} in the application context. The customizer
* will be called with the factory before it is used to create the beans.
*
* @author Dave Syer
*/
public class GrpcClientFactory {
private static Map<Class<?>, StubFactory<?>> FACTORIES = new HashMap<>();
private final ApplicationContext context;
private Map<String, Supplier<ManagedChannel>> options = new HashMap<>();
static {
stubs(new BlockingStubFactory());
stubs(new BlockingV2StubFactory());
stubs(new FutureStubFactory());
stubs(new ReactorStubFactory());
stubs(new SimpleStubFactory());
SpringFactoriesLoader.loadFactories(StubFactory.class, GrpcClientFactory.class.getClassLoader())
.forEach(GrpcClientFactory::stubs);
}
public GrpcClientFactory(ApplicationContext context) {
this.context = context;
}
public <T extends AbstractStub<T>> T getClient(String target, Class<T> type, Class<?> factory) {
StubFactory<?> stubs = findFactory(factory, type);
Supplier<ManagedChannel> channel = this.options.get(target);
if (channel == null) {
channel = () -> channels().createChannel(target, ChannelBuilderOptions.defaults());
}
Supplier<ManagedChannel> finalChannel = channel;
@SuppressWarnings("unchecked")
T client = (T) stubs.create(() -> finalChannel.get(), type);
return client;
}
/**
* Register a channel factory for the given target. The channel will be created using
* the given options. If no options are provided, the default options will be used.
* @param target the name (or base url) of the target
* @param options the options to use to create the channel
*/
public void channel(String target, ChannelBuilderOptions options) {
this.options.put(target, () -> channels().createChannel(target, options));
}
private static void stubs(StubFactory<? extends AbstractStub<?>> factory) {
FACTORIES.put(factory.getClass(), factory);
}
public static StubFactory<?> findFactory(Class<?> type) {
return findFactory(null, type);
}
private static StubFactory<?> findFactory(Class<?> factoryType, Class<?> type) {
StubFactory<? extends AbstractStub<?>> factory = null;
if (factoryType != null && factoryType != UnspecifiedStubFactory.class) {
factory = FACTORIES.get(factoryType);
if (!factory.supports(type)) {
factory = null;
}
}
else {
List<StubFactory<?>> factories = new ArrayList<>(FACTORIES.values());
AnnotationAwareOrderComparator.sort(factories);
for (StubFactory<? extends AbstractStub<?>> value : factories) {
if (value.supports(type)) {
factory = value;
break;
}
}
}
return factory;
}
private GrpcChannelFactory channels() {
return this.context.getBean(GrpcChannelFactory.class);
}
public static void register(BeanDefinitionRegistry registry, GrpcClientRegistrationSpec spec) {
for (Class<?> type : spec.types()) {
StubFactory<?> factory = GrpcClientFactory.findFactory(spec.factory(), type);
if (factory == null) {
continue;
}
RootBeanDefinition beanDef = (RootBeanDefinition) BeanDefinitionBuilder.rootBeanDefinition(type)
.setLazyInit(true)
.setFactoryMethodOnBean("getClient", GrpcClientFactoryPostProcessor.class.getName())
.addConstructorArgValue(spec.target())
.addConstructorArgValue(type)
.addConstructorArgValue(spec.factory())
.getBeanDefinition();
beanDef.setTargetType(type);
String beanName = StringUtils.hasText(spec.prefix()) ? spec.prefix() + type.getSimpleName()
: StringUtils.uncapitalize(type.getSimpleName());
if (!registry.containsBeanDefinition(beanName)) {
// Avoid registering the same bean definition multiple times
registry.registerBeanDefinition(beanName, beanDef);
}
}
}
public record GrpcClientRegistrationSpec(String prefix, Class<? extends StubFactory<?>> factory, String target,
Class<?>[] types) {
private static ClasspathScanner SCANNER = new ClasspathScanner();
public static GrpcClientRegistrationSpec defaults() {
return new GrpcClientRegistrationSpec("default", new Class[0]);
}
public static GrpcClientRegistrationSpec of(String target) {
return new GrpcClientRegistrationSpec(target, new Class[0]);
}
public GrpcClientRegistrationSpec(String target, Class<?>[] types) {
this("", UnspecifiedStubFactory.class, target, types);
}
public GrpcClientRegistrationSpec(String prefix, String target, Class<?>[] types) {
this(prefix, UnspecifiedStubFactory.class, target, types);
}
public GrpcClientRegistrationSpec factory(Class<? extends StubFactory<?>> factory) {
return new GrpcClientRegistrationSpec(this.prefix, factory, this.target, this.types);
}
public GrpcClientRegistrationSpec types(Class<?>... types) {
return new GrpcClientRegistrationSpec(this.prefix, this.factory, this.target, types);
}
public GrpcClientRegistrationSpec prefix(String prefix) {
if (StringUtils.hasText(prefix)) {
return new GrpcClientRegistrationSpec(prefix, this.factory, this.target, this.types);
}
else {
return new GrpcClientRegistrationSpec("", this.factory, this.target, this.types);
}
}
public GrpcClientRegistrationSpec packages(String... packages) {
Set<Class<?>> allTypes = new HashSet<>();
allTypes.addAll(Set.of(this.types));
for (String basePackage : packages) {
// TODO: find a global factory default if scanning and only add the types
// that it supports
allTypes.addAll(SCANNER.scan(basePackage, AbstractStub.class));
}
@SuppressWarnings("unchecked")
Class<? extends AbstractStub<?>>[] newTypes = allTypes.toArray(new Class[0]);
return new GrpcClientRegistrationSpec(this.prefix, this.factory, this.target, newTypes);
}
public GrpcClientRegistrationSpec packageClasses(Class<?>... packageClasses) {
String[] packages = new String[packageClasses.length];
for (Class<?> basePackageClass : packageClasses) {
for (int i = 0; i < packageClasses.length; i++) {
String basePackage = ClassUtils.getPackageName(basePackageClass);
packages[i] = basePackage;
}
}
return packages(packages);
}
}
}

View File

@@ -20,15 +20,15 @@ import org.springframework.context.ApplicationContext;
/**
* Callback interface that can be implemented by beans wishing to customize the
* {@link GrpcClientRegistry} before it is used. The registry is used by the application
* {@link GrpcClientFactory} before it is used. The registry is used by the application
* context very early in its lifecycle, so customizers should not refer directly to other
* beans. It is better to use a lazy lookup via the {@link ApplicationContext} or an
* {@link ObjectProvider}
*
* @author Dave Syer
*/
public interface GrpcClientRegistryCustomizer {
public interface GrpcClientFactoryCustomizer {
void customize(GrpcClientRegistry registry);
void customize(GrpcClientFactory registry);
}

View File

@@ -19,41 +19,44 @@ import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
public class GrpcClientRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor, ApplicationContextAware {
import io.grpc.stub.AbstractStub;
private GenericApplicationContext context;
/**
* Post processor for {@link GrpcClientFactory} that applies the customizers and provides
* a factory for client instances at runtime.
*
* @author Dave Syer
*/
public class GrpcClientFactoryPostProcessor implements ApplicationContextAware {
private ApplicationContext context;
private boolean initialized = false;
private GrpcClientRegistry registry;
private GrpcClientFactory registry;
private void initialize(GenericApplicationContext context) {
if (this.initialized) {
private void initialize(ApplicationContext context) {
if (this.initialized || this.context == null) {
return;
}
this.initialized = true;
this.registry = new GrpcClientRegistry(context);
if (context.getBeanNamesForType(GrpcClientRegistryCustomizer.class).length > 0) {
List<GrpcClientRegistryCustomizer> values = new ArrayList<>(
context.getBeansOfType(GrpcClientRegistryCustomizer.class).values());
this.registry = new GrpcClientFactory(context);
if (context.getBeanNamesForType(GrpcClientFactoryCustomizer.class).length > 0) {
List<GrpcClientFactoryCustomizer> values = new ArrayList<>(
context.getBeansOfType(GrpcClientFactoryCustomizer.class).values());
AnnotationAwareOrderComparator.sort(values);
values.forEach(customizer -> customizer.customize(this.registry));
}
this.registry.close();
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
if (this.context != null) {
initialize(this.context);
}
<T extends AbstractStub<T>> T getClient(String target, Class<T> type, Class<?> factory) {
initialize(this.context);
return this.registry.getClient(target, (Class<T>) type, factory);
}
@Override

View File

@@ -1,296 +0,0 @@
/*
* 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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.grpc.internal.ClasspathScanner;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
import io.grpc.Channel;
import io.grpc.ManagedChannel;
import io.grpc.stub.AbstractStub;
/**
* A registry of gRPC clients that can be used to register client stubs as beans in an
* application context. The best way to interact with the registry is to declare a bean of
* type {@link GrpcClientRegistryCustomizer} in the application context. The customizer
* will be called with the registry before it is used to register the beans.
*
* @author Dave Syer
*/
public class GrpcClientRegistry {
private List<StubFactory<?>> factories = new ArrayList<>();
private Map<Class<?>, StubFactory<?>> factoriesByClass = new HashMap<>();
private Map<String, DeferredBeanDefinition<?>> beans = new HashMap<>();
private final GenericApplicationContext context;
public GrpcClientRegistry(GenericApplicationContext context) {
this.context = context;
stubs(new BlockingStubFactory());
stubs(new BlockingV2StubFactory());
stubs(new FutureStubFactory());
stubs(new ReactorStubFactory());
stubs(new SimpleStubFactory());
SpringFactoriesLoader.loadFactories(StubFactory.class, getClass().getClassLoader()).forEach(this::stubs);
}
/**
* Called internally to register the beans in the application context.
*/
void close() {
for (Map.Entry<String, DeferredBeanDefinition<?>> entry : this.beans.entrySet()) {
registerBean(entry.getKey(), entry.getValue().type(), entry.getValue().supplier());
}
}
@SuppressWarnings("unchecked")
private <T> void registerBean(String key, Class<?> type, Supplier<?> supplier) {
Supplier<T> real = (Supplier<T>) supplier;
Class<T> stub = (Class<T>) type;
this.context.registerBean(key, stub, real, bd -> {
bd.setLazyInit(true);
bd.setAttribute(BeanRegistrationAotProcessor.IGNORE_REGISTRATION_ATTRIBUTE, true);
});
}
/**
* Register a stub factory. All stub factories are consulted (in {@link Ordered}
* order) until one is found that accepts the stub type being considered.
* @param factory the stub factory to register
* @return this
*/
public GrpcClientRegistry stubs(StubFactory<? extends AbstractStub<?>> factory) {
if (this.factoriesByClass.containsKey(factory.getClass())) {
this.factories.remove(this.factoriesByClass.get(factory.getClass()));
}
this.factories.add(factory);
this.factoriesByClass.put(factory.getClass(), factory);
return this;
}
/**
* Start a group of clients that share a common channel.
* @param name the name of the channel
* @return a group of clients to be configured
*/
public GrpcClientGroup channel(String name) {
return channel(name, ChannelBuilderOptions.defaults());
}
/**
* Start a group of clients that share a common channel.
* @param name the name of the channel
* @param options the builder options to use when the channel is created
* @return a group of clients to be configured
*/
public GrpcClientGroup channel(String name, ChannelBuilderOptions options) {
return new GrpcClientGroup(() -> channels().createChannel(name, options));
}
/**
* Start a group of clients that share a common channel.
* @param channel a factory for the channel
* @return a group of clients to be configured
*/
public GrpcClientGroup channel(Supplier<ManagedChannel> channel) {
return new GrpcClientGroup(channel);
}
private <T extends AbstractStub<?>> void preRegisterBean(String beanName, Class<T> type,
Supplier<T> clientFactory) {
this.beans.put(beanName, new DeferredBeanDefinition<>(type, clientFactory));
}
private <T extends AbstractStub<?>> void preRegisterType(String beanName, Supplier<ManagedChannel> channel,
Class<? extends StubFactory<?>> factoryType, Class<T> type) {
StubFactory<? extends AbstractStub<?>> factory = null;
if (factoryType != null) {
factory = this.factoriesByClass.get(factoryType);
if (!factory.supports(type)) {
factory = null;
}
}
else {
AnnotationAwareOrderComparator.sort(this.factories);
for (StubFactory<? extends AbstractStub<?>> value : this.factories) {
if (value.supports(type)) {
factory = value;
break;
}
}
}
if (factory != null) {
StubFactory<? extends AbstractStub<?>> value = factory;
this.beans.put(beanName, new DeferredBeanDefinition<>(type, () -> type.cast(value.create(channel, type))));
return;
}
// Ignore unsupported types
}
private GrpcChannelFactory channels() {
return this.context.getBean(GrpcChannelFactory.class);
}
private static record DeferredBeanDefinition<T extends AbstractStub<?>>(Class<T> type, Supplier<T> supplier) {
}
/**
* A group of gRPC clients that share a common channel. You can use this group to scan
* for stubs or to register individual stubs. Each stub will be created as a bean when
* the application context refreshes so you can inject it into application code by
* type (if unique) or by name. The bean names are determined by a concatenation of
* the {@link GrpcClientGroup#prefix(String) prefix} and the simple name of the stub
* class.
*
* @author Dave Syer
*/
public final class GrpcClientGroup {
private ClasspathScanner scanner = new ClasspathScanner();
private final Supplier<ManagedChannel> channel;
private String prefix = "";
private Class<? extends StubFactory<?>> factory;
private GrpcClientGroup(Supplier<ManagedChannel> channel) {
this.channel = channel;
}
/**
* Register a stub with the given type. The stub will be created using the given
* factory.
* @param <T> the parametric type of the stub
* @param type the type of the stub
* @param factory the factory to use to create the stub
* @return the parent registry
*/
public <T extends AbstractStub<?>> GrpcClientRegistry register(Class<T> type, Function<Channel, T> factory) {
String beanName = type.getSimpleName();
if (StringUtils.hasText(this.prefix)) {
beanName = this.prefix + beanName;
}
else {
beanName = StringUtils.uncapitalize(beanName);
}
preRegisterBean(beanName, type, () -> factory.apply(this.channel.get()));
return GrpcClientRegistry.this;
}
/**
* Register stubs with the given types. The stub will be created using the given
* factory.
* @param <T> ignore this
* @param types the types of the stubs
* @return the parent registry
*/
public <T extends AbstractStub<?>> GrpcClientRegistry register(Class<?>... types) {
for (Class<?> type : types) {
String beanName = type.getSimpleName();
if (StringUtils.hasText(this.prefix)) {
beanName = this.prefix + beanName;
}
else {
beanName = StringUtils.uncapitalize(beanName);
}
@SuppressWarnings("unchecked")
Class<T> stub = (Class<T>) type;
preRegisterType(beanName, this.channel, this.factory, stub);
}
return GrpcClientRegistry.this;
}
/**
* Register stubs by scanning the packages (and subpackages) of the provided
* types. The stubs will be created using the factory from
* {@link GrpcClientGroup#factory factory()} if provided, otherwise a factory will
* be selected based on the stub type. N.B. it is best to provide a factory if you
* can, so that the correct stubs are created, otherwise you might get more beans
* than you need.
* @param basePackageClasses the base types of the stub packages
* @return the parent registry
*/
public GrpcClientRegistry packageClasses(Class<?>... basePackageClasses) {
String[] basePackages = new String[basePackageClasses.length];
for (int i = 0; i < basePackageClasses.length; i++) {
basePackages[i] = ClassUtils.getPackageName(basePackageClasses[i]);
}
return packages(basePackages);
}
/**
* Register stubs by scanning the provided packages (and subpackages). The stubs
* will be created using the factory from {@link GrpcClientGroup#factory
* factory()} if provided, otherwise a factory will be selected based on the stub
* type. N.B. it is best to provide a factory if you can, so that the correct
* stubs are created, otherwise you might get more beans than you need.
* @param basePackages the base packages of the stubs
* @return the parent registry
*/
public GrpcClientRegistry packages(String... basePackages) {
for (String basePackage : basePackages) {
for (Class<?> stub : this.scanner.scan(basePackage, AbstractStub.class)) {
register(stub);
}
}
return GrpcClientRegistry.this;
}
/**
* Prepare to scan for stubs using the given factory.
* @param <T> the generic type of the factory
* @param factory the factory type
* @return this
*/
public <T extends StubFactory<?>> GrpcClientGroup scan(Class<T> factory) {
GrpcClientGroup group = new GrpcClientGroup(this.channel);
group.prefix = this.prefix;
group.factory = factory;
return group;
}
/**
* Register a prefix for the generated bean definitions.
* @param prefix the provided prefix
* @return this
*/
public GrpcClientGroup prefix(String prefix) {
GrpcClientGroup group = new GrpcClientGroup(this.channel);
group.prefix = prefix;
return group;
}
}
}

View File

@@ -29,14 +29,14 @@ import io.grpc.stub.AbstractStub;
/**
* Annotation to create gRPC client beans. If you want more control over the creation of
* the clients, or you don't want to use the annotation, you can use a bean of type
* {@link GrpcClientRegistryCustomizer} instead.
* {@link GrpcClientFactoryCustomizer} instead.
*
* @author Dave Syer
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(GrpcClientConfiguration.class)
@Import(AnnotationGrpcClientRegistrar.class)
@Repeatable(ImportGrpcClients.Container.class)
public @interface ImportGrpcClients {
@@ -67,7 +67,7 @@ public @interface ImportGrpcClients {
* basePackageClasses or basePackages) and you need to customize the stub creation.
* @return the factory type, default is {@link BlockingStubFactory}
*/
Class<? extends StubFactory<?>> factory() default BlockingStubFactory.class;
Class<? extends StubFactory<?>> factory() default UnspecifiedStubFactory.class;
/**
* The base package classes to scan for annotated components. If not specified,
@@ -87,7 +87,7 @@ public @interface ImportGrpcClients {
@Documented
// In case there is more than one @ImportGrpcClients annotation we need to
// import here as well:
@Import(GrpcClientConfiguration.class)
@Import(AnnotationGrpcClientRegistrar.class)
@interface Container {
ImportGrpcClients[] value() default {};

View File

@@ -0,0 +1,22 @@
/*
* 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 io.grpc.stub.AbstractStub;
public interface UnspecifiedStubFactory extends StubFactory<AbstractStub<?>> {
}

View File

@@ -39,7 +39,7 @@ The `@ImportGrpcClients` annotation can be used to control the scan for gRPC stu
To scan a package you can specify the `basePackages` or `basePackageClasses` attribute.
Then elsewhere in the application you can `@Autowired` the generated gRPC stubs (the blocking sub-type by default).
You can change the factory used to create the stubs from `BlockingStubFactory` by setting the `factory` attribute.
There are standard factories pre-registered for common stub types, and if you want to register additional factories you can use a `GrpcClientRegistryCustomizer` (see below for details).
There are standard factories pre-registered for common stub types, and if you want to register additional factories you can use a `GrpcClientFactoryCustomizer` (see below for details).
The default behaviour in a Spring Boot application is equivalent to the following configuration on your `@SpringBootApplication` class:
@@ -53,52 +53,41 @@ class MyApplication {
}
----
You can enhance and modify the configuration by providing `spring.grpc.client.*` application properties or by defining your own `GrpcClientRegistryCustomizer` beans.
You can enhance and modify the configuration by providing `spring.grpc.client.*` application properties or by defining your own `GrpcClientFactoryCustomizer` beans.
The customizer has full control over the scanning and registration of the gRPC clients, including for example the ability to change the base type of the stubs that are registered.
=== Register Individual Stub Types
The `@ImportGrpcClients` has a `types` attribute if you want to register specific stub types instead of scanning a package.
A `GrpcClientRegistryCustomizer` can also be used to control the registration of the gRPC clients in the application context, and the API is flexible enough to allow you to add your own behaviour that would not be possible with just the `@ImportGrpcClients` annotation.
For example, to add just one client stub using the default channel:
[source,java]
----
@Bean
GrpcClientRegistryCustomizer stubs() {
return registry -> registry
.register(SimpleGrpc.SimpleBlockingStub.class);
}
----
=== More Complex Examples
A `GrpcClientRegistryCustomizer` can also control the creation of the channels and add custom behaviour to stubs (individually or via a scan).
For example, to add a custom security interceptor to only clients:
A `GrpcClientFactoryCustomizer` can also control the creation of the channels and add custom behaviour to stubs (individually or via a scan).
For example, to add a custom security interceptor to only clients using the "stub" channel:
[source,java]
----
@Bean
GrpcClientRegistryCustomizer stubs(ObjectProvider<ClientRegistrationRepository> context) {
return registry -> registry
.channel("stub",
ChannelBuilderOptions.defaults()
.withInterceptors(List.of(new BearerTokenAuthenticationInterceptor(() -> token(context)))))
.prefix("secure")
.register(SimpleGrpc.SimpleBlockingStub.class);
@ImportGrpcClients(basePackageClasses = MyApplication.class)
@Configuration
class ExtraConfiguration {
@Bean
GrpcClientFactoryCustomizer stubs() {
return registry -> registry
.channel("stub",
ChannelBuilderOptions.defaults()
.withInterceptors(List.of(new BearerTokenAuthenticationInterceptor(() -> token(context)))));
}
}
----
In this example, instead of scanning for all stubs, we register a specific stub class `SimpleGrpc.SimpleBlockingStub` with the channel named `stub`.
The prefix `secure` is used as a bean definition name prefix, so the resulting bean definition in this case is "secureSimpleBlockingStub".
This feature is useful when you want to have multiple instances of the same stub class with different configurations.
N.B. The `ClientRegistrationRepository` in the example is injected via an `ObjectProvider` which is not called directly to avoid early instantiation.
The customizer has to run very early in the application lifecycle, so you always want to follow this pattern if you need to inject dependencies to build the channel options.
The configuration of the individual client specs is done completely separately from the channel factory configuration.
This is an important distinction because, although they might be related, the two things happen at very different times in the application lifecycle.
In particular it is futile to try to inject other beans into `MyClientRegistrar` because the beans are not available yet - its role is to define a set of bean definitions to be created later.
== Create a Client Manually
Instead of using the `@ImportGrpcClients` or `GrpcClientRegistry` features, we can create a client `@Bean` manually.
Instead of using the `@ImportGrpcClients` or `GrpcClientFactory` features, we can create a client `@Bean` manually.
The most common usage of a channel is to create a client that binds to a service.
For example:
@@ -220,7 +209,7 @@ For example:
spring.grpc.client.channels.local.address=0.0.0.0:${local.grpc.port}
----
You can't use `@LocalGrpcPort` in a `GrpcClientRegistryCustomizer` because it is not available until the server starts.
You can't use `@LocalGrpcPort` in a `GrpcClientFactoryCustomizer` because it is not available until the server starts.
You can lazily resolve `local.grpc.port` in the customizer by using the `Environment` when the channel is created, either directly via its API or through placeholders like in the properties file example above.
[[client-interceptor]]

View File

@@ -18,38 +18,69 @@ package org.springframework.grpc.autoconfigure.client;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.grpc.autoconfigure.client.ClientScanConfiguration.DefaultGrpcClientRegistrations;
import org.springframework.grpc.autoconfigure.client.GrpcClientProperties.ChannelConfig;
import org.springframework.grpc.client.AbstractGrpcClientRegistrar;
import org.springframework.grpc.client.BlockingStubFactory;
import org.springframework.grpc.client.GrpcClientFactory;
import org.springframework.grpc.client.GrpcClientFactory.GrpcClientRegistrationSpec;
import org.springframework.grpc.client.GrpcClientFactoryPostProcessor;
import org.springframework.grpc.client.ImportGrpcClients;
import org.springframework.grpc.client.GrpcClientRegistryCustomizer;
import org.springframework.grpc.client.GrpcClientRegistryPostProcessor;
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(GrpcClientRegistryPostProcessor.class)
@ConditionalOnMissingBean(GrpcClientFactoryPostProcessor.class)
@ImportGrpcClients
@Import(DefaultGrpcClientRegistrations.class)
public class ClientScanConfiguration {
@Bean
public GrpcClientRegistryCustomizer defaultGrpcClientRegistryCustomizer(BeanFactory beanFactory,
Environment environment) {
List<String> packages = new ArrayList<>();
if (AutoConfigurationPackages.has(beanFactory)) {
packages.addAll(AutoConfigurationPackages.get(beanFactory));
static class DefaultGrpcClientRegistrations extends AbstractGrpcClientRegistrar
implements EnvironmentAware, BeanFactoryAware {
private Environment environment;
private BeanFactory beanFactory;
@Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
Binder binder = Binder.get(environment);
boolean hasDefaultChannel = binder.bind("spring.grpc.client.default-channel", ChannelConfig.class).isBound();
return registry -> {
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
protected GrpcClientRegistrationSpec[] collect(AnnotationMetadata meta) {
Binder binder = Binder.get(environment);
boolean hasDefaultChannel = binder.bind("spring.grpc.client.default-channel", ChannelConfig.class)
.isBound();
if (hasDefaultChannel) {
registry.channel("default").scan(BlockingStubFactory.class).packages(packages.toArray(new String[0]));
List<String> packages = new ArrayList<>();
if (AutoConfigurationPackages.has(beanFactory)) {
packages.addAll(AutoConfigurationPackages.get(beanFactory));
}
// TODO: change global default factory type in properties maybe?
return new GrpcClientRegistrationSpec[] { GrpcClientRegistrationSpec.of("default")
.factory(BlockingStubFactory.class)
.packages(packages.toArray(new String[0])) };
}
};
return new GrpcClientRegistrationSpec[0];
}
}
}