From 8f706665df0a33c4dfb147126590f032488f2d9e Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Mon, 12 May 2025 17:04:03 +0100 Subject: [PATCH] Change contract of StubFactory Instead of an instance method we now use a static supports() method to determine if a factory matches a given stub type. This is better for the lifecycle since bean definitions have to be created before and factory can actually be created and autowired. It's a slightly unusual contract, but it makes it much harder to make a mistake with lifecycle, and much easier to install custom StubFactory instances (as @Beans) --- .../grpc/client/AbstractStubFactory.java | 15 +- .../grpc/client/BlockingStubFactory.java | 19 +- .../grpc/client/BlockingV2StubFactory.java | 19 +- .../grpc/client/FutureStubFactory.java | 13 +- .../grpc/client/GrpcClientFactory.java | 227 ++++++++++++------ .../GrpcClientFactoryPostProcessor.java | 4 +- .../grpc/client/ImportGrpcClients.java | 4 +- .../grpc/client/ReactorStubFactory.java | 19 +- .../grpc/client/SimpleStubFactory.java | 22 +- .../grpc/client/StubFactory.java | 7 +- .../ClientBeanRegistrationsAotProcessor.java | 20 +- .../grpc/internal/ClasspathScanner.java | 44 ++-- .../grpc/client/GrpcClientFactoryTests.java | 148 ++++++++++++ .../grpc/internal/ClasspathScannerTests.java | 77 ++++++ 14 files changed, 453 insertions(+), 185 deletions(-) create mode 100644 spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcClientFactoryTests.java create mode 100644 spring-grpc-core/src/test/java/org/springframework/grpc/internal/ClasspathScannerTests.java diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/AbstractStubFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/AbstractStubFactory.java index ff2d5b5..a0eaada 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/AbstractStubFactory.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/AbstractStubFactory.java @@ -23,20 +23,13 @@ import io.grpc.stub.AbstractStub; public abstract class AbstractStubFactory> implements StubFactory { - private final Class> baseType; - - @SuppressWarnings("unchecked") - protected AbstractStubFactory(Class baseType) { - this.baseType = (Class>) baseType; + protected static > boolean supports(Class baseType, Class type) { + return baseType.isAssignableFrom(type); } @Override - public boolean supports(Class type) { - return this.baseType.isAssignableFrom(type); - } - - @Override - public T create(Supplier channel, Class> type) { + public T create(Supplier channel, Class type) { + // All the generated stubs are static inner classes of the service Class factory = type.getEnclosingClass(); @SuppressWarnings("unchecked") T stub = (T) createStub(channel, factory, methodName()); diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/BlockingStubFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/BlockingStubFactory.java index 6fb4b47..684cc24 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/BlockingStubFactory.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/BlockingStubFactory.java @@ -15,24 +15,13 @@ */ package org.springframework.grpc.client; -import org.springframework.core.Ordered; - import io.grpc.stub.AbstractBlockingStub; -public class BlockingStubFactory extends AbstractStubFactory> implements Ordered { +public class BlockingStubFactory extends AbstractStubFactory> { - public BlockingStubFactory() { - super(AbstractBlockingStub.class); - } - - @Override - public boolean supports(Class type) { - return super.supports(type) && !type.getSimpleName().contains("BlockingV2"); - } - - @Override - public int getOrder() { - return SimpleStubFactory.SIMPLE_STUB_ORDER - 30; + public static boolean supports(Class type) { + return AbstractStubFactory.supports(AbstractBlockingStub.class, type) + && !type.getSimpleName().contains("BlockingV2"); } @Override diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/BlockingV2StubFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/BlockingV2StubFactory.java index d2ce4e2..e85355f 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/BlockingV2StubFactory.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/BlockingV2StubFactory.java @@ -15,24 +15,13 @@ */ package org.springframework.grpc.client; -import org.springframework.core.Ordered; - import io.grpc.stub.AbstractBlockingStub; -public class BlockingV2StubFactory extends AbstractStubFactory> implements Ordered { +public class BlockingV2StubFactory extends AbstractStubFactory> { - public BlockingV2StubFactory() { - super(AbstractBlockingStub.class); - } - - @Override - public boolean supports(Class type) { - return super.supports(type) && type.getSimpleName().contains("BlockingV2"); - } - - @Override - public int getOrder() { - return SimpleStubFactory.SIMPLE_STUB_ORDER - 30; + public static boolean supports(Class type) { + return AbstractStubFactory.supports(AbstractBlockingStub.class, type) + && type.getSimpleName().contains("BlockingV2"); } @Override diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/FutureStubFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/FutureStubFactory.java index eec737d..851d7b2 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/FutureStubFactory.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/FutureStubFactory.java @@ -15,20 +15,13 @@ */ package org.springframework.grpc.client; -import org.springframework.core.Ordered; - import io.grpc.stub.AbstractBlockingStub; import io.grpc.stub.AbstractFutureStub; -public class FutureStubFactory extends AbstractStubFactory> implements Ordered { +public class FutureStubFactory extends AbstractStubFactory> { - public FutureStubFactory() { - super(AbstractFutureStub.class); - } - - @Override - public int getOrder() { - return SimpleStubFactory.SIMPLE_STUB_ORDER - 20; + public static boolean supports(Class type) { + return AbstractStubFactory.supports(AbstractFutureStub.class, type); } @Override 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 index 77b2c85..4b0749b 100644 --- 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 @@ -15,21 +15,28 @@ */ package org.springframework.grpc.client; +import java.io.IOException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Supplier; -import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; 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.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.TypeFilter; import org.springframework.grpc.internal.ClasspathScanner; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -48,34 +55,36 @@ import io.grpc.stub.AbstractStub; */ public class GrpcClientFactory { - private static final Set> DEFAULT_FACTORIES = new HashSet<>(); + private static final Set> DEFAULT_FACTORIES = new LinkedHashSet<>(); - private Map, StubFactory> factories = new HashMap<>(); + private static final String FACTORIES_BEAN_DEFINITION_NAME = GrpcClientFactory.class.getName() + ".factories"; + + private Map, StubFactory> factories = new LinkedHashMap<>(); private final ApplicationContext context; private Map> options = new HashMap<>(); static { - stubs(BlockingStubFactory.class); - stubs(BlockingV2StubFactory.class); - stubs(FutureStubFactory.class); - stubs(ReactorStubFactory.class); - stubs(SimpleStubFactory.class); + DEFAULT_FACTORIES.add((Class>) BlockingStubFactory.class); + DEFAULT_FACTORIES.add((Class>) BlockingV2StubFactory.class); + DEFAULT_FACTORIES.add((Class>) FutureStubFactory.class); + DEFAULT_FACTORIES.add((Class>) ReactorStubFactory.class); + DEFAULT_FACTORIES.add((Class>) SimpleStubFactory.class); } public GrpcClientFactory(ApplicationContext context) { this.context = context; } - public > T getClient(String target, Class type, Class factory) { - StubFactory stubs = findFactory(factory, type); + public T getClient(String target, Class type, Class factory) { + @SuppressWarnings("unchecked") + StubFactory stubs = (StubFactory) 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; } @@ -90,14 +99,17 @@ public class GrpcClientFactory { this.options.put(target, () -> channels().createChannel(target, options)); } - private static void stubs(Class> factory) { - DEFAULT_FACTORIES.add(factory); - } - private StubFactory findFactory(Class factoryType, Class type) { if (this.factories.isEmpty()) { + List> factories = new ArrayList<>(); for (StubFactory factory : this.context.getBeansOfType(StubFactory.class).values()) { - this.factories.put(factory.getClass(), factory); + factories.add(factory); + } + AnnotationAwareOrderComparator.sort(factories); + for (StubFactory factory : factories) { + if (supports(factory.getClass(), type)) { + this.factories.put(factory.getClass(), factory); + } } for (Class factory : DEFAULT_FACTORIES) { if (this.factories.containsKey(factory)) { @@ -107,14 +119,20 @@ public class GrpcClientFactory { (StubFactory) this.context.getAutowireCapableBeanFactory().createBean(factory)); } } - return findFactory(this.factories, factoryType, type); + StubFactory factory = findFactory(this.factories, factoryType, type); + if (factory == null) { + throw new IllegalStateException( + "Cannot find a suitable factory for " + type.getName() + " with factory " + factoryType); + } + return factory; } - private static Class findDefaultFactory(Class factoryType, Class type) { + private static Class findDefaultFactory(BeanDefinitionRegistry registry, Class factoryType, Class type) { if (factoryType != null && factoryType != UnspecifiedStubFactory.class) { return supports(factoryType, type) ? factoryType : null; } - for (Class factory : DEFAULT_FACTORIES) { + Set> factories = locateFactoryTypes(registry); + for (Class factory : factories) { if (supports(factory, type)) { return factory; } @@ -122,41 +140,78 @@ public class GrpcClientFactory { return null; } + private static Set> locateFactoryTypes(BeanDefinitionRegistry registry) { + AbstractBeanDefinition beanDefinition; + if (!registry.containsBeanDefinition(FACTORIES_BEAN_DEFINITION_NAME)) { + // Stash the factories in a bean definition so we can find them later + beanDefinition = BeanDefinitionBuilder.rootBeanDefinition(StubFactoryProvider.class).getBeanDefinition(); + beanDefinition.setAttribute("factories", findStubFactoryTypes(registry)); + registry.registerBeanDefinition(FACTORIES_BEAN_DEFINITION_NAME, beanDefinition); + } + beanDefinition = (AbstractBeanDefinition) registry.getBeanDefinition(FACTORIES_BEAN_DEFINITION_NAME); + @SuppressWarnings("unchecked") + Set> factories = (Set>) beanDefinition.getAttribute("factories"); + return factories; + + } + + public static HashSet> findStubFactoryTypes(BeanDefinitionRegistry registry) { + HashSet> factories = new HashSet<>(); + for (String name : registry.getBeanDefinitionNames()) { + BeanDefinition beanDefinition = registry.getBeanDefinition(name); + Class factory = resolveBeanClass(beanDefinition); + if (factory != null && StubFactory.class.isAssignableFrom(factory)) { + factories.add(factory); + } + } + for (Class factory : DEFAULT_FACTORIES) { + if (!factories.contains(factory)) { + factories.add(factory); + } + } + return factories; + } + + private static Class resolveBeanClass(BeanDefinition beanDefinition) { + if (beanDefinition instanceof AbstractBeanDefinition rootBeanDefinition) { + if (rootBeanDefinition.hasBeanClass()) { + return rootBeanDefinition.getBeanClass(); + } + } + return null; + } + private static boolean supports(Class factory, Class type) { + // To avoid needing to instantiate the factory we use reflection to check for a + // static supports() method. If it exists we call it. Method method = ReflectionUtils.findMethod(factory, "supports", Class.class); boolean supports = false; if (method != null) { + ReflectionUtils.makeAccessible(method); try { supports = (boolean) ReflectionUtils.invokeMethod(method, null, type); } catch (Exception e) { - try { - // TODO: drop support for non-static methods - supports = (boolean) ReflectionUtils.invokeMethod(method, BeanUtils.instantiateClass(factory), - type); - } - catch (Exception ex) { - // Ignore - } + // Ignore } } + else { + // If the factory is not one of the default factories, and doesn't have a + // supports() method we assume it supports the supplied type + supports = !DEFAULT_FACTORIES.contains(factory); + } return supports; } private static StubFactory findFactory(Map, StubFactory> values, Class factoryType, Class type) { - StubFactory> factory = null; + StubFactory factory = null; if (factoryType != null && factoryType != UnspecifiedStubFactory.class) { factory = values.get(factoryType); - if (!factory.supports(type)) { - factory = null; - } } else { - List> factories = new ArrayList<>(values.values()); - AnnotationAwareOrderComparator.sort(factories); - for (StubFactory> value : factories) { - if (value.supports(type)) { + for (StubFactory value : values.values()) { + if (supports(value.getClass(), type)) { factory = value; break; } @@ -170,8 +225,9 @@ public class GrpcClientFactory { } public static void register(BeanDefinitionRegistry registry, GrpcClientRegistrationSpec spec) { + spec = spec.prepare(registry); for (Class type : spec.types()) { - if (GrpcClientFactory.findDefaultFactory(spec.factory(), type) == null) { + if (GrpcClientFactory.findDefaultFactory(registry, spec.factory(), type) == null) { continue; } RootBeanDefinition beanDef = (RootBeanDefinition) BeanDefinitionBuilder.rootBeanDefinition(type) @@ -192,7 +248,7 @@ public class GrpcClientFactory { } public record GrpcClientRegistrationSpec(String prefix, Class> factory, String target, - Class[] types) { + Class[] types, String[] packages) { private static ClasspathScanner SCANNER = new ClasspathScanner(); @@ -200,48 +256,73 @@ public class GrpcClientFactory { 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) { + private GrpcClientRegistrationSpec prepare(BeanDefinitionRegistry registry) { Set> allTypes = new HashSet<>(); allTypes.addAll(Set.of(this.types)); - for (String basePackage : packages) { - for (Class type : SCANNER.scan(basePackage, AbstractStub.class)) { - if (findDefaultFactory(this.factory, type) != null) { + for (String basePackage : this.packages) { + TypeFilter filter = new TypeFilter() { + @Override + public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) + throws IOException { + Class type = ClassUtils.resolveClassName(metadataReader.getClassMetadata().getClassName(), + ClasspathScanner.class.getClassLoader()); + return supports(GrpcClientRegistrationSpec.this.factory, type); + } + }; + for (Class type : SCANNER.scan(basePackage, filter)) { + if (findDefaultFactory(registry, this.factory, type) != null) { allTypes.add(type); } } } @SuppressWarnings("unchecked") Class>[] newTypes = allTypes.toArray(new Class[0]); - return new GrpcClientRegistrationSpec(this.prefix, this.factory, this.target, newTypes); + return new GrpcClientRegistrationSpec(this.prefix, this.factory, this.target, newTypes, new String[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, new String[0]); + } + + public GrpcClientRegistrationSpec(String prefix, String target, Class[] types) { + this(prefix, UnspecifiedStubFactory.class, target, types, new String[0]); + } + + public GrpcClientRegistrationSpec factory(Class> factory) { + return new GrpcClientRegistrationSpec(this.prefix, factory, this.target, this.types, this.packages); + } + + public GrpcClientRegistrationSpec types(Class... types) { + return new GrpcClientRegistrationSpec(this.prefix, this.factory, this.target, types, this.packages); + } + + public GrpcClientRegistrationSpec prefix(String prefix) { + if (StringUtils.hasText(prefix)) { + return new GrpcClientRegistrationSpec(prefix, this.factory, this.target, this.types, this.packages); + } + else { + return new GrpcClientRegistrationSpec("", this.factory, this.target, this.types, this.packages); + } + } + + public GrpcClientRegistrationSpec packages(String... packages) { + Set allPackages = new HashSet<>(); + for (String pkg : packages) { + if (StringUtils.hasText(pkg)) { + allPackages.add(pkg); + } + } + for (String pkg : this.packages) { + if (StringUtils.hasText(pkg)) { + allPackages.add(pkg); + } + } + return new GrpcClientRegistrationSpec(this.prefix, this.factory, this.target, this.types, + allPackages.toArray(new String[0])); } public GrpcClientRegistrationSpec packageClasses(Class... packageClasses) { @@ -256,4 +337,8 @@ public class GrpcClientFactory { } } + static class StubFactoryProvider { + + } + } diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientFactoryPostProcessor.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientFactoryPostProcessor.java index c5c9581..df307eb 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientFactoryPostProcessor.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/GrpcClientFactoryPostProcessor.java @@ -24,8 +24,6 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import io.grpc.stub.AbstractStub; - /** * Post processor for {@link GrpcClientFactory} that applies the customizers and provides * a factory for client instances at runtime. @@ -54,7 +52,7 @@ public class GrpcClientFactoryPostProcessor implements ApplicationContextAware { } } - > T getClient(String target, Class type, Class factory) { + T getClient(String target, Class type, Class factory) { initialize(this.context); return this.registry.getClient(target, (Class) type, factory); } 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 a97a700..730d7f8 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 @@ -24,8 +24,6 @@ import java.lang.annotation.Target; import org.springframework.context.annotation.Import; -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 @@ -60,7 +58,7 @@ public @interface ImportGrpcClients { * Concrete types of the stubs to create. * @return the types of the stubs */ - Class>[] types() default {}; + Class[] types() default {}; /** * The factory type to use to create the stubs. Only needed if you are scanning (with diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/ReactorStubFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/ReactorStubFactory.java index 3390df6..c2bab4d 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/ReactorStubFactory.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/ReactorStubFactory.java @@ -15,25 +15,12 @@ */ package org.springframework.grpc.client; -import org.springframework.core.Ordered; - -import io.grpc.stub.AbstractBlockingStub; import io.grpc.stub.AbstractStub; -public class ReactorStubFactory extends AbstractStubFactory> implements Ordered { +public class ReactorStubFactory extends AbstractStubFactory> { - public ReactorStubFactory() { - super(AbstractStub.class); - } - - @Override - public int getOrder() { - return SimpleStubFactory.SIMPLE_STUB_ORDER - 10; - } - - @Override - public boolean supports(Class type) { - return super.supports(type) && type.getSimpleName().startsWith("Reactor"); + public static boolean supports(Class type) { + return AbstractStubFactory.supports(AbstractStub.class, type) && type.getSimpleName().startsWith("Reactor"); } @Override diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/SimpleStubFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/SimpleStubFactory.java index f4c4acd..4a2db2c 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/SimpleStubFactory.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/SimpleStubFactory.java @@ -15,26 +15,16 @@ */ package org.springframework.grpc.client; -import org.springframework.core.Ordered; +import org.springframework.util.ReflectionUtils; -import io.grpc.stub.AbstractBlockingStub; import io.grpc.stub.AbstractStub; -public class SimpleStubFactory extends AbstractStubFactory> implements Ordered { +public class SimpleStubFactory extends AbstractStubFactory> { - /** - * Constant used to specify the order in which the factory should be considered or - * applied. A lower value indicates higher priority. - */ - public static final int SIMPLE_STUB_ORDER = 0; - - public SimpleStubFactory() { - super(AbstractStub.class); - } - - @Override - public int getOrder() { - return SimpleStubFactory.SIMPLE_STUB_ORDER; + public static boolean supports(Class type) { + Class factory = type.getEnclosingClass(); + return AbstractStubFactory.supports(AbstractStub.class, type) && factory != null + && ReflectionUtils.findMethod(factory, "newStub", (Class[]) null) != null; } @Override diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/StubFactory.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/StubFactory.java index 2cbda19..32e2885 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/StubFactory.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/StubFactory.java @@ -18,12 +18,9 @@ package org.springframework.grpc.client; import java.util.function.Supplier; import io.grpc.ManagedChannel; -import io.grpc.stub.AbstractStub; -public interface StubFactory> { +public interface StubFactory { - boolean supports(Class type); - - T create(Supplier channel, Class> type); + T create(Supplier channel, Class type); } diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/client/aot/ClientBeanRegistrationsAotProcessor.java b/spring-grpc-core/src/main/java/org/springframework/grpc/client/aot/ClientBeanRegistrationsAotProcessor.java index 1979ecd..5e1e1a9 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/client/aot/ClientBeanRegistrationsAotProcessor.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/client/aot/ClientBeanRegistrationsAotProcessor.java @@ -31,8 +31,10 @@ import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContrib import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.core.MethodParameter; +import org.springframework.grpc.client.GrpcClientFactory; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -63,7 +65,14 @@ public class ClientBeanRegistrationsAotProcessor implements BeanFactoryInitializ if (registrations.isEmpty()) { return null; } - return new ClientBeanRegistrationsAotContribution(registrations, resources); + + Set factories = new HashSet<>(); + if (beanFactory instanceof DefaultListableBeanFactory listable) { + HashSet> types = GrpcClientFactory.findStubFactoryTypes(listable); + factories.addAll(types); + } + + return new ClientBeanRegistrationsAotContribution(registrations, factories, resources); } private Collection findMessageTypes(Class beanClass) { @@ -106,8 +115,11 @@ public class ClientBeanRegistrationsAotProcessor implements BeanFactoryInitializ private Set> resources; - ClientBeanRegistrationsAotContribution(Set types, Set> resources) { + private Set factories; + + ClientBeanRegistrationsAotContribution(Set types, Set factories, Set> resources) { this.types = types; + this.factories = factories; this.resources = resources; } @@ -120,6 +132,10 @@ public class ClientBeanRegistrationsAotProcessor implements BeanFactoryInitializ for (Type type : this.types) { hints.registerType(TypeReference.of(type.getTypeName()), MemberCategory.INVOKE_PUBLIC_METHODS); } + for (Type type : this.factories) { + hints.registerType(TypeReference.of(type.getTypeName()), MemberCategory.INVOKE_PUBLIC_METHODS, + MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); + } ResourceHints resources = generationContext.getRuntimeHints().resources(); // We only really need this if we are scanning. Some stubs are not scanned // anyway, and scanning should be unnecessary for AOT, but this works and can diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/internal/ClasspathScanner.java b/spring-grpc-core/src/main/java/org/springframework/grpc/internal/ClasspathScanner.java index 0c29664..706b2ff 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/internal/ClasspathScanner.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/internal/ClasspathScanner.java @@ -17,6 +17,7 @@ package org.springframework.grpc.internal; import java.io.FileNotFoundException; import java.io.IOException; +import java.lang.annotation.Annotation; import java.util.LinkedHashSet; import java.util.Set; @@ -34,6 +35,9 @@ import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import org.springframework.core.type.classreading.ClassFormatException; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.filter.AnnotationTypeFilter; +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.core.type.filter.TypeFilter; import org.springframework.util.ClassUtils; public class ClasspathScanner implements ResourceLoaderAware { @@ -55,12 +59,30 @@ public class ClasspathScanner implements ResourceLoaderAware { } public Set> scan(String basePackage, Class type) { - Set> candidates = new LinkedHashSet<>(); boolean debugEnabled = logger.isDebugEnabled(); - boolean traceEnabled = logger.isTraceEnabled(); if (debugEnabled) { logger.debug("Scanning " + basePackage + " for classes of type " + type.getName()); } + return scan(basePackage, new AssignableTypeFilter(type)); + } + + public Set> annotated(String basePackage, Class type) { + boolean debugEnabled = logger.isDebugEnabled(); + if (debugEnabled) { + logger.debug("Scanning " + basePackage + " for annotations of type " + type.getName()); + } + @SuppressWarnings("unchecked") + Class annotationType = (Class) type; + return scan(basePackage, new AnnotationTypeFilter(annotationType)); + } + + public Set> scan(String basePackage, TypeFilter filter) { + Set> candidates = new LinkedHashSet<>(); + if (filter == null) { + return candidates; + } + boolean debugEnabled = logger.isDebugEnabled(); + boolean traceEnabled = logger.isTraceEnabled(); try { String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + resolveBasePackage(basePackage) + '/' + this.resourcePattern; @@ -76,14 +98,14 @@ public class ClasspathScanner implements ResourceLoaderAware { } try { MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource); - if (isCandidateComponent(metadataReader, type)) { + if (filter.match(metadataReader, getMetadataReaderFactory())) { Class sbd = ClassUtils.forName(metadataReader.getClassMetadata().getClassName(), null); logger.debug("Identified candidate component class: " + resource); candidates.add(sbd); } else { if (debugEnabled) { - logger.debug("Ignored because not a concrete top-level class: " + resource); + logger.debug("Ignored because not a candidate class: " + resource); } } } @@ -108,20 +130,6 @@ public class ClasspathScanner implements ResourceLoaderAware { return candidates; } - private boolean isCandidateComponent(MetadataReader metadataReader, Class type) { - try { - if (metadataReader.getClassMetadata().isConcrete()) { - if (type.isAssignableFrom( - ClassUtils.resolveClassName(metadataReader.getClassMetadata().getClassName(), null))) { - return true; - } - } - } - catch (Exception ex) { - } - return false; - } - private MetadataReaderFactory getMetadataReaderFactory() { if (this.metadataReaderFactory == null) { this.metadataReaderFactory = new CachingMetadataReaderFactory(); diff --git a/spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcClientFactoryTests.java b/spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcClientFactoryTests.java new file mode 100644 index 0000000..64ca83f --- /dev/null +++ b/spring-grpc-core/src/test/java/org/springframework/grpc/client/GrpcClientFactoryTests.java @@ -0,0 +1,148 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchIllegalStateException; + +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.grpc.client.GrpcClientFactory.GrpcClientRegistrationSpec; +import org.springframework.grpc.client.GrpcClientFactoryTests.MyProto.MyStub; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ManagedChannel; +import io.grpc.stub.AbstractStub; + +public class GrpcClientFactoryTests { + + private GrpcChannelFactory channelFactory = Mockito.mock(GrpcChannelFactory.class); + + private StaticApplicationContext context = new StaticApplicationContext(); + + private GrpcClientFactory factory; + + GrpcClientFactoryTests() { + Mockito.when(channelFactory.createChannel(Mockito.anyString(), Mockito.any())) + .thenReturn(Mockito.mock(ManagedChannel.class)); + context.registerBean(GrpcChannelFactory.class, () -> channelFactory); + factory = new GrpcClientFactory(context); + } + + @Test + void testRegisterAndCreate() { + GrpcClientFactory.register(context, new GrpcClientRegistrationSpec("local", new Class[] { MyStub.class })); + assertThat(factory.getClient("local", MyStub.class, null)).isNotNull(); + } + + @Test + void testNoStubFactory() { + GrpcClientFactory.register(context, new GrpcClientRegistrationSpec("local", new Class[] { OtherStub.class })); + catchIllegalStateException(() -> factory.getClient("local", OtherStub.class, null)); + } + + @Test + void testCustomStubFactory() { + context.registerBean(OtherStubFactory.class, () -> new OtherStubFactory()); + GrpcClientFactory.register(context, new GrpcClientRegistrationSpec("local", new Class[] { OtherStub.class })); + assertThat(factory.getClient("local", OtherStub.class, OtherStubFactory.class)).isNotNull(); + } + + @Test + void testWithExplicitStubFactory() { + context.registerBean(OtherStubFactory.class, () -> new OtherStubFactory()); + GrpcClientFactory.register(context, new GrpcClientRegistrationSpec("local", new Class[] { OtherStub.class }) + .factory(OtherStubFactory.class)); + assertThat(factory.getClient("local", OtherStub.class, null)).isNotNull(); + } + + @Test + void testAnnotationConfig() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.registerBean(MyConfiguration.class); + context.registerBean(GrpcChannelFactory.class, () -> channelFactory); + GrpcClientFactory.register(context, new GrpcClientRegistrationSpec("local", new Class[] { OtherStub.class })); + context.refresh(); + factory = new GrpcClientFactory(context); + assertThat(factory.getClient("local", OtherStub.class, null)).isNotNull(); + } + + static class OtherStubFactory implements StubFactory { + + @Override + public OtherStub create(Supplier channel, Class type) { + return new OtherStub(channel.get()); + } + + static boolean supports(Class type) { + return OtherStub.class.isAssignableFrom(type); + } + + } + + static class OtherStub extends AbstractStub { + + OtherStub(Channel channel) { + super(channel); + } + + @Override + protected OtherStub build(Channel channel, CallOptions callOptions) { + return new OtherStub(channel); + } + + } + + static class MyProto { + + public static MyStub newStub(Channel channel) { + return new MyStub(channel); + } + + static class MyStub extends AbstractStub { + + MyStub(Channel channel) { + super(channel); + } + + @Override + protected MyStub build(Channel channel, CallOptions callOptions) { + return new MyStub(channel); + } + + } + + } + + @Configuration(proxyBeanMethods = false) + static class MyConfiguration { + + @Bean + OtherStubFactory otherStubFactory() { + return new OtherStubFactory(); + } + + } + +} diff --git a/spring-grpc-core/src/test/java/org/springframework/grpc/internal/ClasspathScannerTests.java b/spring-grpc-core/src/test/java/org/springframework/grpc/internal/ClasspathScannerTests.java new file mode 100644 index 0000000..0dbbd79 --- /dev/null +++ b/spring-grpc-core/src/test/java/org/springframework/grpc/internal/ClasspathScannerTests.java @@ -0,0 +1,77 @@ +/* + * 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.internal; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.type.filter.AssignableTypeFilter; +import org.springframework.core.type.filter.TypeFilter; + +public class ClasspathScannerTests { + + @Test + void testScan() { + ClasspathScanner scanner = new ClasspathScanner(); + Set> classes = scanner.scan("org.springframework.grpc.internal", ClasspathScannerTests.class); + assertThat(classes).isNotEmpty(); + assertThat(classes).contains(ClasspathScannerTests.class); + } + + @Test + void testFilter() { + ClasspathScanner scanner = new ClasspathScanner(); + Set> classes = scanner.scan("org.springframework.grpc.internal", new AssignableTypeFilter(Foo.class)); + assertThat(classes).isNotEmpty(); + assertThat(classes).contains(Foo.class); + } + + @Test + void testNoFilter() { + ClasspathScanner scanner = new ClasspathScanner(); + Set> classes = scanner.scan("org.springframework.grpc.internal", (TypeFilter) null); + assertThat(classes).isEmpty(); + } + + @Test + void testAnnotation() { + ClasspathScanner scanner = new ClasspathScanner(); + Set> classes = scanner.annotated("org.springframework.grpc.internal", FooMarker.class); + assertThat(classes).isNotEmpty(); + assertThat(classes).contains(Foo.class); + } + + @FooMarker + interface Foo { + + } + + @Target({ ElementType.TYPE, ElementType.METHOD }) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @interface FooMarker { + + } + +}