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:
@@ -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());
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user