diff --git a/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java b/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java index 95e4a75f9..c77ac9952 100644 --- a/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java +++ b/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java @@ -29,11 +29,14 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; +import org.springframework.context.weaving.LoadTimeWeaverAware; +import org.springframework.instrument.classloading.LoadTimeWeaver; import io.grpc.CompressorRegistry; import io.grpc.DecompressorRegistry; import io.grpc.NameResolverProvider; import io.grpc.NameResolverRegistry; +import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import net.devh.boot.grpc.client.channelfactory.GrpcChannelConfigurer; import net.devh.boot.grpc.client.channelfactory.GrpcChannelFactory; @@ -224,4 +227,17 @@ GrpcChannelFactory inProcessGrpcChannelFactory( return new InProcessChannelFactory(properties, globalClientInterceptorRegistry, channelConfigurers); } + @Configuration(proxyBeanMethods = false) + static class GrpcClientConstructorInjectionConfiguration implements LoadTimeWeaverAware { + @Autowired + private GrpcClientBeanPostProcessor grpcClientBeanPostProcessor; + + @PostConstruct + public void init() { + grpcClientBeanPostProcessor.initGrpClientConstructorInjections(); + } + + @Override + public void setLoadTimeWeaver(LoadTimeWeaver ltw) {} + } } diff --git a/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/inject/GrpcClientBeanPostProcessor.java b/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/inject/GrpcClientBeanPostProcessor.java index 8fa1f4efa..9b646f0a2 100644 --- a/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/inject/GrpcClientBeanPostProcessor.java +++ b/grpc-client-spring-boot-starter/src/main/java/net/devh/boot/grpc/client/inject/GrpcClientBeanPostProcessor.java @@ -18,33 +18,52 @@ import static java.util.Objects.requireNonNull; +import java.beans.PropertyDescriptor; +import java.lang.annotation.Annotation; +import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; import org.springframework.beans.InvalidPropertyException; +import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.InjectionMetadata; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; +import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; +import org.springframework.core.BridgeMethodResolver; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; import com.google.common.collect.Lists; import io.grpc.Channel; import io.grpc.ClientInterceptor; import io.grpc.stub.AbstractStub; -import jakarta.annotation.PostConstruct; import net.devh.boot.grpc.client.channelfactory.GrpcChannelFactory; import net.devh.boot.grpc.client.nameresolver.NameResolverRegistration; import net.devh.boot.grpc.client.stubfactory.FallbackStubFactory; @@ -57,7 +76,8 @@ * @author Michael (yidongnan@gmail.com) * @author Daniel Theuke (daniel.theuke@heuboe.de) */ -public class GrpcClientBeanPostProcessor implements BeanPostProcessor { +public class GrpcClientBeanPostProcessor + implements InstantiationAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor { private final ApplicationContext applicationContext; @@ -70,25 +90,26 @@ public class GrpcClientBeanPostProcessor implements BeanPostProcessor { // For bean registration via @GrpcClientBean private ConfigurableListableBeanFactory configurableBeanFactory; + private final Set> grpcClientAnnotationTypes = new LinkedHashSet<>(4); + + private final Map injectionMetadataCache = new ConcurrentHashMap<>(256); + /** - * Creates a new GrpcClientBeanPostProcessor with the given ApplicationContext. + * Creates a new GrpcClientBeanPostProcessor with the given ApplicationContext for GrpcClient standard + * {@link GrpcClient @GrpcClient} annotation. * * @param applicationContext The application context that will be used to get lazy access to the * {@link GrpcChannelFactory} and {@link StubTransformer}s. */ public GrpcClientBeanPostProcessor(final ApplicationContext applicationContext) { this.applicationContext = requireNonNull(applicationContext, "applicationContext"); - } - - @PostConstruct - public void init() { - initGrpClientConstructorInjections(); + this.grpcClientAnnotationTypes.add(GrpcClient.class); } /** * Triggers registering grpc client beans from GrpcClientConstructorInjection. */ - private void initGrpClientConstructorInjections() { + public void initGrpClientConstructorInjections() { Iterable registries; try { registries = getConfigurableBeanFactory().getBean(GrpcClientConstructorInjection.class).getRegistries(); @@ -120,9 +141,6 @@ private void initGrpClientConstructorInjections() { public Object postProcessBeforeInitialization(final Object bean, final String beanName) throws BeansException { Class clazz = bean.getClass(); do { - processFields(clazz, bean); - processMethods(clazz, bean); - if (isAnnotatedWithConfiguration(clazz)) { processGrpcClientBeansAnnotations(clazz); } @@ -132,6 +150,19 @@ public Object postProcessBeforeInitialization(final Object bean, final String be return bean; } + @Override + public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) { + InjectionMetadata metadata = findGrpcClientMetadata(beanName, bean.getClass(), pvs); + try { + metadata.inject(bean, beanName, pvs); + } catch (BeanCreationException ex) { + throw ex; + } catch (Throwable ex) { + throw new BeanCreationException(beanName, "Injection of gRPC client stub failed", ex); + } + return pvs; + } + /** * Processes the bean's fields in the given class. * @@ -398,4 +429,110 @@ private boolean isAnnotatedWithConfiguration(final Class clazz) { return configurationAnnotation != null; } + @Override + public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class beanType, String beanName) { + InjectionMetadata metadata = findGrpcClientMetadata(beanName, beanType, null); + metadata.checkConfigMembers(beanDefinition); + } + + private InjectionMetadata findGrpcClientMetadata(String beanName, Class clazz, @Nullable PropertyValues pvs) { + // Fall back to class name as cache key, for backwards compatibility with custom callers. + String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName()); + // Quick check on the concurrent map first, with minimal locking. + InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey); + if (InjectionMetadata.needsRefresh(metadata, clazz)) { + synchronized (this.injectionMetadataCache) { + metadata = this.injectionMetadataCache.get(cacheKey); + if (InjectionMetadata.needsRefresh(metadata, clazz)) { + if (metadata != null) { + metadata.clear(pvs); + } + metadata = buildGrpcClientMetadata(clazz); + this.injectionMetadataCache.put(cacheKey, metadata); + } + } + } + return metadata; + } + + private InjectionMetadata buildGrpcClientMetadata(Class clazz) { + if (!AnnotationUtils.isCandidateClass(clazz, this.grpcClientAnnotationTypes)) { + return InjectionMetadata.EMPTY; + } + + List elements = new ArrayList<>(); + Class targetClass = clazz; + + do { + final List currElements = new ArrayList<>(); + + ReflectionUtils.doWithLocalFields(targetClass, field -> { + MergedAnnotation ann = findGrpcClientAnnotation(field); + if (ann != null) { + if (Modifier.isStatic(field.getModifiers())) { + throw new IllegalStateException( + "GrpcClient annotation is not supported on static fields: " + field); + } + currElements.add(new GrpcClientMemberElement(field, null)); + } + }); + + ReflectionUtils.doWithLocalMethods(targetClass, method -> { + Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method); + if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) { + return; + } + MergedAnnotation ann = findGrpcClientAnnotation(bridgedMethod); + if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { + if (Modifier.isStatic(method.getModifiers())) { + throw new IllegalStateException( + "GrpcClient annotation is not supported on static method: " + method); + } + if (method.getParameterCount() == 0) { + throw new IllegalStateException( + "GrpcClient annotation should only be used on methods with parameters: " + method); + } + PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); + currElements.add(new GrpcClientMemberElement(method, pd)); + } + }); + + elements.addAll(0, currElements); + targetClass = targetClass.getSuperclass(); + } while (targetClass != null && targetClass != Object.class); + + return InjectionMetadata.forElements(elements, clazz); + } + + private MergedAnnotation findGrpcClientAnnotation(AccessibleObject ao) { + MergedAnnotations annotations = MergedAnnotations.from(ao); + for (Class type : this.grpcClientAnnotationTypes) { + MergedAnnotation annotation = annotations.get(type); + if (annotation.isPresent()) { + return annotation; + } + } + return null; + } + + /** + * Class representing injection information about an annotated member. + */ + private class GrpcClientMemberElement extends InjectionMetadata.InjectedElement { + + public GrpcClientMemberElement(Member member, @Nullable PropertyDescriptor pd) { + super(member, pd); + } + + @Override + protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable { + Class clazz = bean.getClass(); + do { + processFields(clazz, bean); + processMethods(clazz, bean); + + clazz = clazz.getSuperclass(); + } while (clazz != null); + } + } } diff --git a/tests/src/test/java/net/devh/boot/grpc/test/inject/GrpcClientMetricsTest.java b/tests/src/test/java/net/devh/boot/grpc/test/inject/GrpcClientMetricsTest.java new file mode 100644 index 000000000..420f94748 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/inject/GrpcClientMetricsTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2016-2023 The gRPC-Spring 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 + * + * http://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 net.devh.boot.grpc.test.inject; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.autoconfigure.metrics.JvmMetricsAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import io.grpc.stub.AbstractStub; +import io.micrometer.core.instrument.MeterRegistry; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.client.stubfactory.StandardJavaGrpcStubFactory; +import net.devh.boot.grpc.client.stubfactory.StubFactory; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.InProcessConfiguration; +import net.devh.boot.grpc.test.config.MetricConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.proto.TestServiceGrpc; + +@SpringBootTest +@SpringJUnitConfig( + classes = { + GrpcClientMetricsTest.TestConfig.class, + GrpcClientMetricsTest.GrpcClientConstructorInjectionBean.class, + InProcessConfiguration.class, + ServiceConfiguration.class, + BaseAutoConfiguration.class, + MetricConfiguration.class + }) +@DirtiesContext +public class GrpcClientMetricsTest { + @Autowired + MeterRegistry registry; + + @Test + void jvmMetricsTest() { + assertThat(registry.getMeters()) + .filteredOn(meter -> meter.getId().getName().contains("jvm")) + .isNotEmpty(); + } + + @Component + public static class GrpcClientConstructorInjectionBean { + public TestServiceGrpc.TestServiceBlockingStub blockingStub; + public TestServiceGrpc.TestServiceFutureStub futureStubForClientTest; + public TestServiceGrpc.TestServiceBlockingStub anotherBlockingStub; + public TestServiceGrpc.TestServiceBlockingStub unnamedTestServiceBlockingStub; + public CustomGrpc.FactoryMethodAccessibleStub anotherServiceClientBean; + + public GrpcClientConstructorInjectionBean( + @GrpcClient("test") TestServiceGrpc.TestServiceBlockingStub blockingStub, + @GrpcClient("test") TestServiceGrpc.TestServiceFutureStub futureStubForClientTest, + @GrpcClient("anotherTest") TestServiceGrpc.TestServiceBlockingStub anotherBlockingStub, + @GrpcClient("unnamed") TestServiceGrpc.TestServiceBlockingStub unnamedTestServiceBlockingStub, + @GrpcClient("test") CustomGrpc.FactoryMethodAccessibleStub anotherServiceClientBean) { + this.blockingStub = blockingStub; + this.futureStubForClientTest = futureStubForClientTest; + this.anotherBlockingStub = anotherBlockingStub; + this.unnamedTestServiceBlockingStub = unnamedTestServiceBlockingStub; + this.anotherServiceClientBean = anotherServiceClientBean; + } + } + + @Configuration + @ImportAutoConfiguration({MetricsAutoConfiguration.class, JvmMetricsAutoConfiguration.class}) + public static class TestConfig { + @Bean + StubFactory customStubFactory() { + return new StandardJavaGrpcStubFactory() { + + @Override + public boolean isApplicable(final Class> stubType) { + return CustomStub.class.isAssignableFrom(stubType); + } + + @Override + protected String getFactoryMethodName() { + return "custom"; + } + + }; + } + } +}