From a3d7ba643dedfd0ef1b43f00921bfddf3ff8b08b Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Tue, 1 Apr 2025 11:49:59 +0100 Subject: [PATCH] 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. --- .../sample/GrpcServerApplicationTests.java | 15 +- .../sample/GrpcServerApplicationTests.java | 14 +- .../client/AbstractGrpcClientRegistrar.java | 40 +++ .../client/AnnotationGrpcClientRegistrar.java | 60 ++++ .../grpc/client/GrpcClientConfiguration.java | 107 ------- .../grpc/client/GrpcClientFactory.java | 210 +++++++++++++ ....java => GrpcClientFactoryCustomizer.java} | 6 +- ...va => GrpcClientFactoryPostProcessor.java} | 37 ++- .../grpc/client/GrpcClientRegistry.java | 296 ------------------ .../grpc/client/ImportGrpcClients.java | 8 +- .../grpc/client/UnspecifiedStubFactory.java | 22 ++ .../antora/modules/ROOT/pages/client.adoc | 53 ++-- .../client/ClientScanConfiguration.java | 61 +++- 13 files changed, 437 insertions(+), 492 deletions(-) create mode 100644 spring-grpc-core/src/main/java/org/springframework/grpc/client/AbstractGrpcClientRegistrar.java create mode 100644 spring-grpc-core/src/main/java/org/springframework/grpc/client/AnnotationGrpcClientRegistrar.java delete mode 100644 spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientConfiguration.java create mode 100644 spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientFactory.java rename spring-grpc-core/src/main/java/org/springframework/grpc/client/{GrpcClientRegistryCustomizer.java => GrpcClientFactoryCustomizer.java} (85%) rename spring-grpc-core/src/main/java/org/springframework/grpc/client/{GrpcClientRegistryPostProcessor.java => GrpcClientFactoryPostProcessor.java} (62%) delete mode 100644 spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientRegistry.java create mode 100644 spring-grpc-core/src/main/java/org/springframework/grpc/client/UnspecifiedStubFactory.java diff --git a/samples/grpc-oauth2/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java b/samples/grpc-oauth2/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java index 6f3f852..c4b9369 100644 --- a/samples/grpc-oauth2/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java +++ b/samples/grpc-oauth2/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java @@ -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 context) { - return registry -> registry - .channel("stub", - ChannelBuilderOptions.defaults() - .withInterceptors(List.of(new BearerTokenAuthenticationInterceptor(() -> token(context))))) - .prefix("secure") - .register(SimpleGrpc.SimpleBlockingStub.class); + GrpcClientFactoryCustomizer stubs(ObjectProvider context) { + return registry -> registry.channel("secure", ChannelBuilderOptions.defaults() + .withInterceptors(List.of(new BearerTokenAuthenticationInterceptor(() -> token(context))))); } private String token(ObjectProvider context) { diff --git a/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java b/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java index a219743..a29fdda 100644 --- a/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java +++ b/samples/grpc-secure/src/test/java/org/springframework/grpc/sample/GrpcServerApplicationTests.java @@ -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")))); } } diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/AbstractGrpcClientRegistrar.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/AbstractGrpcClientRegistrar.java new file mode 100644 index 0000000..fef3c26 --- /dev/null +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/AbstractGrpcClientRegistrar.java @@ -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); + +} diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/AnnotationGrpcClientRegistrar.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/AnnotationGrpcClientRegistrar.java new file mode 100644 index 0000000..db429c6 --- /dev/null +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/AnnotationGrpcClientRegistrar.java @@ -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 attrs = meta.getMergedRepeatableAnnotationAttributes(ImportGrpcClients.class, + ImportGrpcClients.Container.class, false); + Set 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> 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); + } + +} diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientConfiguration.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientConfiguration.java deleted file mode 100644 index 0bf3ae5..0000000 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientConfiguration.java +++ /dev/null @@ -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 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> 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> 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> type; - - private String[] basePackages; - - SimpleGrpcClientRegistryCustomizer(String target, String prefix, Class[] types, - Class> 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); - } - - } - -} diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientFactory.java new file mode 100644 index 0000000..acf9c5c --- /dev/null +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientFactory.java @@ -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, StubFactory> FACTORIES = new HashMap<>(); + + private final ApplicationContext context; + + private Map> 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 getClient(String target, Class type, Class factory) { + StubFactory stubs = findFactory(factory, type); + Supplier channel = this.options.get(target); + if (channel == null) { + channel = () -> channels().createChannel(target, ChannelBuilderOptions.defaults()); + } + Supplier 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> 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> factory = null; + if (factoryType != null && factoryType != UnspecifiedStubFactory.class) { + factory = FACTORIES.get(factoryType); + if (!factory.supports(type)) { + factory = null; + } + } + else { + List> factories = new ArrayList<>(FACTORIES.values()); + AnnotationAwareOrderComparator.sort(factories); + for (StubFactory> 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> 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> 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> 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>[] 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); + } + } + +} diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientRegistryCustomizer.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientFactoryCustomizer.java similarity index 85% rename from spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientRegistryCustomizer.java rename to spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientFactoryCustomizer.java index 2da4d16..72569c0 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientRegistryCustomizer.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientFactoryCustomizer.java @@ -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); } diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientRegistryPostProcessor.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientFactoryPostProcessor.java similarity index 62% rename from spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientRegistryPostProcessor.java rename to spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientFactoryPostProcessor.java index a316d7e..c5c9581 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientRegistryPostProcessor.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientFactoryPostProcessor.java @@ -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 values = new ArrayList<>( - context.getBeansOfType(GrpcClientRegistryCustomizer.class).values()); + this.registry = new GrpcClientFactory(context); + if (context.getBeanNamesForType(GrpcClientFactoryCustomizer.class).length > 0) { + List 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 getClient(String target, Class type, Class factory) { + initialize(this.context); + return this.registry.getClient(target, (Class) type, factory); } @Override diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientRegistry.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientRegistry.java deleted file mode 100644 index f7760ce..0000000 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientRegistry.java +++ /dev/null @@ -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> factories = new ArrayList<>(); - - private Map, StubFactory> factoriesByClass = new HashMap<>(); - - private Map> 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> entry : this.beans.entrySet()) { - registerBean(entry.getKey(), entry.getValue().type(), entry.getValue().supplier()); - } - } - - @SuppressWarnings("unchecked") - private void registerBean(String key, Class type, Supplier supplier) { - Supplier real = (Supplier) supplier; - Class stub = (Class) 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> 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 channel) { - return new GrpcClientGroup(channel); - } - - private > void preRegisterBean(String beanName, Class type, - Supplier clientFactory) { - this.beans.put(beanName, new DeferredBeanDefinition<>(type, clientFactory)); - } - - private > void preRegisterType(String beanName, Supplier channel, - Class> factoryType, Class type) { - StubFactory> factory = null; - if (factoryType != null) { - factory = this.factoriesByClass.get(factoryType); - if (!factory.supports(type)) { - factory = null; - } - } - else { - AnnotationAwareOrderComparator.sort(this.factories); - for (StubFactory> value : this.factories) { - if (value.supports(type)) { - factory = value; - break; - } - } - } - if (factory != null) { - StubFactory> 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>(Class type, Supplier 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 channel; - - private String prefix = ""; - - private Class> factory; - - private GrpcClientGroup(Supplier channel) { - this.channel = channel; - } - - /** - * Register a stub with the given type. The stub will be created using the given - * factory. - * @param 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 > GrpcClientRegistry register(Class type, Function 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 ignore this - * @param types the types of the stubs - * @return the parent registry - */ - public > 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 stub = (Class) 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 the generic type of the factory - * @param factory the factory type - * @return this - */ - public > GrpcClientGroup scan(Class 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; - } - - } - -} diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/ImportGrpcClients.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/ImportGrpcClients.java index 9feb7d7..a97a700 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/ImportGrpcClients.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/ImportGrpcClients.java @@ -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> factory() default BlockingStubFactory.class; + Class> 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 {}; diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/UnspecifiedStubFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/UnspecifiedStubFactory.java new file mode 100644 index 0000000..9c3ceab --- /dev/null +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/UnspecifiedStubFactory.java @@ -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> { + +} diff --git a/spring-grpc-docs/src/main/antora/modules/ROOT/pages/client.adoc b/spring-grpc-docs/src/main/antora/modules/ROOT/pages/client.adoc index 8a3d9ab..8634d32 100644 --- a/spring-grpc-docs/src/main/antora/modules/ROOT/pages/client.adoc +++ b/spring-grpc-docs/src/main/antora/modules/ROOT/pages/client.adoc @@ -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 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]] diff --git a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/ClientScanConfiguration.java b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/ClientScanConfiguration.java index 0cc3074..4d795de 100644 --- a/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/ClientScanConfiguration.java +++ b/spring-grpc-spring-boot-autoconfigure/src/main/java/org/springframework/grpc/autoconfigure/client/ClientScanConfiguration.java @@ -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 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 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]; + } + } }