Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix metric disable when using the constructor injection of the Grpc Client #907

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -193,4 +196,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) {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -57,7 +76,8 @@
* @author Michael ([email protected])
* @author Daniel Theuke ([email protected])
*/
public class GrpcClientBeanPostProcessor implements BeanPostProcessor {
public class GrpcClientBeanPostProcessor
implements InstantiationAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor {

private final ApplicationContext applicationContext;

Expand All @@ -70,25 +90,26 @@ public class GrpcClientBeanPostProcessor implements BeanPostProcessor {
// For bean registration via @GrpcClientBean
private ConfigurableListableBeanFactory configurableBeanFactory;

private final Set<Class<? extends Annotation>> grpcClientAnnotationTypes = new LinkedHashSet<>(4);

private final Map<String, InjectionMetadata> 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<GrpcClientConstructorInjection.Registry> registries;
try {
registries = getConfigurableBeanFactory().getBean(GrpcClientConstructorInjection.class).getRegistries();
Expand Down Expand Up @@ -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);
}
Expand All @@ -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.
*
Expand Down Expand Up @@ -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<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
Class<?> targetClass = clazz;

do {
final List<InjectionMetadata.InjectedElement> 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<? extends Annotation> 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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<? extends AbstractStub<?>> stubType) {
return CustomStub.class.isAssignableFrom(stubType);
}

@Override
protected String getFactoryMethodName() {
return "custom";
}

};
}
}
}
Loading