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)
This commit is contained in:
Dave Syer
2025-05-12 17:04:03 +01:00
parent 08957115bd
commit 8f706665df
14 changed files with 453 additions and 185 deletions

View File

@@ -23,20 +23,13 @@ import io.grpc.stub.AbstractStub;
public abstract class AbstractStubFactory<T extends AbstractStub<?>> implements StubFactory<T> {
private final Class<? extends AbstractStub<?>> baseType;
@SuppressWarnings("unchecked")
protected AbstractStubFactory(Class<?> baseType) {
this.baseType = (Class<? extends AbstractStub<?>>) baseType;
protected static <S extends AbstractStub<?>> boolean supports(Class<S> baseType, Class<?> type) {
return baseType.isAssignableFrom(type);
}
@Override
public boolean supports(Class<?> type) {
return this.baseType.isAssignableFrom(type);
}
@Override
public T create(Supplier<ManagedChannel> channel, Class<? extends AbstractStub<?>> type) {
public T create(Supplier<ManagedChannel> channel, Class<? extends T> 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());

View File

@@ -15,24 +15,13 @@
*/
package org.springframework.grpc.client;
import org.springframework.core.Ordered;
import io.grpc.stub.AbstractBlockingStub;
public class BlockingStubFactory extends AbstractStubFactory<AbstractBlockingStub<?>> implements Ordered {
public class BlockingStubFactory extends AbstractStubFactory<AbstractBlockingStub<?>> {
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

View File

@@ -15,24 +15,13 @@
*/
package org.springframework.grpc.client;
import org.springframework.core.Ordered;
import io.grpc.stub.AbstractBlockingStub;
public class BlockingV2StubFactory extends AbstractStubFactory<AbstractBlockingStub<?>> implements Ordered {
public class BlockingV2StubFactory extends AbstractStubFactory<AbstractBlockingStub<?>> {
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

View File

@@ -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<AbstractBlockingStub<?>> implements Ordered {
public class FutureStubFactory extends AbstractStubFactory<AbstractBlockingStub<?>> {
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

View File

@@ -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<Class<?>> DEFAULT_FACTORIES = new HashSet<>();
private static final Set<Class<?>> DEFAULT_FACTORIES = new LinkedHashSet<>();
private Map<Class<?>, StubFactory<?>> factories = new HashMap<>();
private static final String FACTORIES_BEAN_DEFINITION_NAME = GrpcClientFactory.class.getName() + ".factories";
private Map<Class<?>, StubFactory<?>> factories = new LinkedHashMap<>();
private final ApplicationContext context;
private Map<String, Supplier<ManagedChannel>> options = new HashMap<>();
static {
stubs(BlockingStubFactory.class);
stubs(BlockingV2StubFactory.class);
stubs(FutureStubFactory.class);
stubs(ReactorStubFactory.class);
stubs(SimpleStubFactory.class);
DEFAULT_FACTORIES.add((Class<? extends StubFactory<?>>) BlockingStubFactory.class);
DEFAULT_FACTORIES.add((Class<? extends StubFactory<?>>) BlockingV2StubFactory.class);
DEFAULT_FACTORIES.add((Class<? extends StubFactory<?>>) FutureStubFactory.class);
DEFAULT_FACTORIES.add((Class<? extends StubFactory<?>>) ReactorStubFactory.class);
DEFAULT_FACTORIES.add((Class<? extends StubFactory<?>>) SimpleStubFactory.class);
}
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);
public <T> T getClient(String target, Class<T> type, Class<?> factory) {
@SuppressWarnings("unchecked")
StubFactory<T> stubs = (StubFactory<T>) 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;
}
@@ -90,14 +99,17 @@ public class GrpcClientFactory {
this.options.put(target, () -> channels().createChannel(target, options));
}
private static void stubs(Class<? extends StubFactory<?>> factory) {
DEFAULT_FACTORIES.add(factory);
}
private StubFactory<?> findFactory(Class<?> factoryType, Class<?> type) {
if (this.factories.isEmpty()) {
List<StubFactory<?>> 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<Class<?>> 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<Class<?>> 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<Class<?>> factories = (Set<Class<?>>) beanDefinition.getAttribute("factories");
return factories;
}
public static HashSet<Class<?>> findStubFactoryTypes(BeanDefinitionRegistry registry) {
HashSet<Class<?>> 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<Class<?>, StubFactory<?>> values, Class<?> factoryType,
Class<?> type) {
StubFactory<? extends AbstractStub<?>> factory = null;
StubFactory<?> factory = null;
if (factoryType != null && factoryType != UnspecifiedStubFactory.class) {
factory = values.get(factoryType);
if (!factory.supports(type)) {
factory = null;
}
}
else {
List<StubFactory<?>> factories = new ArrayList<>(values.values());
AnnotationAwareOrderComparator.sort(factories);
for (StubFactory<? extends AbstractStub<?>> 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<? extends StubFactory<?>> 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<? 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) {
private GrpcClientRegistrationSpec prepare(BeanDefinitionRegistry registry) {
Set<Class<?>> 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<? extends AbstractStub<?>>[] 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<? extends StubFactory<?>> 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<String> 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 {
}
}

View File

@@ -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 extends AbstractStub<T>> T getClient(String target, Class<T> type, Class<?> factory) {
<T> T getClient(String target, Class<T> type, Class<?> factory) {
initialize(this.context);
return this.registry.getClient(target, (Class<T>) type, factory);
}

View File

@@ -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<? extends AbstractStub<?>>[] types() default {};
Class<?>[] types() default {};
/**
* The factory type to use to create the stubs. Only needed if you are scanning (with

View File

@@ -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<AbstractBlockingStub<?>> implements Ordered {
public class ReactorStubFactory extends AbstractStubFactory<AbstractStub<?>> {
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

View File

@@ -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<AbstractBlockingStub<?>> implements Ordered {
public class SimpleStubFactory extends AbstractStubFactory<AbstractStub<?>> {
/**
* 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

View File

@@ -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<T extends AbstractStub<?>> {
public interface StubFactory<T> {
boolean supports(Class<?> type);
T create(Supplier<ManagedChannel> channel, Class<? extends AbstractStub<?>> type);
T create(Supplier<ManagedChannel> channel, Class<? extends T> type);
}

View File

@@ -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<Type> factories = new HashSet<>();
if (beanFactory instanceof DefaultListableBeanFactory listable) {
HashSet<Class<?>> types = GrpcClientFactory.findStubFactoryTypes(listable);
factories.addAll(types);
}
return new ClientBeanRegistrationsAotContribution(registrations, factories, resources);
}
private Collection<Type> findMessageTypes(Class<?> beanClass) {
@@ -106,8 +115,11 @@ public class ClientBeanRegistrationsAotProcessor implements BeanFactoryInitializ
private Set<Class<?>> resources;
ClientBeanRegistrationsAotContribution(Set<Type> types, Set<Class<?>> resources) {
private Set<Type> factories;
ClientBeanRegistrationsAotContribution(Set<Type> types, Set<Type> factories, Set<Class<?>> 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

View File

@@ -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<Class<?>> scan(String basePackage, Class<?> type) {
Set<Class<?>> 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<Class<?>> annotated(String basePackage, Class<?> type) {
boolean debugEnabled = logger.isDebugEnabled();
if (debugEnabled) {
logger.debug("Scanning " + basePackage + " for annotations of type " + type.getName());
}
@SuppressWarnings("unchecked")
Class<? extends Annotation> annotationType = (Class<? extends Annotation>) type;
return scan(basePackage, new AnnotationTypeFilter(annotationType));
}
public Set<Class<?>> scan(String basePackage, TypeFilter filter) {
Set<Class<?>> 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();

View File

@@ -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<OtherStub> {
@Override
public OtherStub create(Supplier<ManagedChannel> channel, Class<? extends OtherStub> type) {
return new OtherStub(channel.get());
}
static boolean supports(Class<?> type) {
return OtherStub.class.isAssignableFrom(type);
}
}
static class OtherStub extends AbstractStub<OtherStub> {
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> {
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();
}
}
}

View File

@@ -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<Class<?>> 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<Class<?>> 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<Class<?>> classes = scanner.scan("org.springframework.grpc.internal", (TypeFilter) null);
assertThat(classes).isEmpty();
}
@Test
void testAnnotation() {
ClasspathScanner scanner = new ClasspathScanner();
Set<Class<?>> 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 {
}
}