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

@@ -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<?>> {
}