diff --git a/docs/en/client/configuration.md b/docs/en/client/configuration.md index c025ea09a..acfa90ed8 100644 --- a/docs/en/client/configuration.md +++ b/docs/en/client/configuration.md @@ -9,6 +9,7 @@ This section describes how you can configure your grpc-spring-boot-starter clien - [Configuration via Properties](#configuration-via-properties) - [Choosing the Target](#choosing-the-target) - [Configuration via Beans](#configuration-via-beans) + - [GrpcClientBean](#grpcclientbean) - [GrpcChannelConfigurer](#grpcchannelconfigurer) - [ClientInterceptor](#clientinterceptor) - [StubFactory](#stubfactory) @@ -115,6 +116,50 @@ First of all most of the beans can be replaced by custom ones, that you can conf If you don't wish to go that far, you can use classes such as `GrpcChannelConfigurer` and `StubTransformer` to configure the channels, stubs and other components without losing the features provided by this library. +### GrpcClientBean + +This annotation is used to create injectable beans from your otherwise non-injectable `@GrpcClient` instances. +The annotation can be repeatedly added to any of your `@Configuration` classes. + +> **Note:** We recommend using either `@GrpcClientBean`s or fields annotated with `@GrpcClient` throughout your +> application, as mixing the two might cause confusion for future developers. + +````java +@Configuration +@GrpcClientBean( + clazz = TestServiceBlockingStub.class, + beanName = "blockingStub", + client = @GrpcClient("test") +) +@GrpcClientBean( + clazz = FactoryMethodAccessibleStub.class, + beanName = "accessibleStub", + client = @GrpcClient("test")) +public class YourCustomConfiguration { + + @Bean + FooService fooServiceBean(@Autowired TestServiceGrpc.TestServiceBlockingStub blockingStub) { + return new FooService(blockingStub); + } + +} + +@Service +@AllArgsConsturtor +public class BarService { + + private FactoryMethodAccessibleStub accessibleStub; + + public String receiveGreeting(String name) { + HelloRequest request = HelloRequest.newBuilder() + .setName(name) + .build(); + return accessibleStub.sayHello(request).getMessage(); + } + +} +```` + ### GrpcChannelConfigurer The grpc client configurer allows you to add your custom configuration to grpc's `ManagedChannelBuilder`s. diff --git a/docs/en/client/getting-started.md b/docs/en/client/getting-started.md index 7b0d42a83..3d9f6f057 100644 --- a/docs/en/client/getting-started.md +++ b/docs/en/client/getting-started.md @@ -106,10 +106,14 @@ If you don't wish to use any advanced features, then the first element is probab The annotation that marks fields and setters for auto injection of clients. Supports `Channel`s, and all kinds of `Stub`s. Do not use `@GrpcClient` in conjunction with `@Autowired` or `@Inject`. - Currently it isn't supported for constructor and `@Bean` method parameters. \ + Currently, it isn't supported for constructor and `@Bean` method parameters. For this case look below + to the `@GrpcClientBean`. \ **Note:** Services provided by the same application can only be accessed/called in/after the `ApplicationStartedEvent`. Stubs connecting to services outside of the application can be used earlier; starting with `@PostConstruct` / `InitializingBean#afterPropertiesSet()`. +- [`@GrpcClientBean`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/inject/GrpcClientBean.html): + The annotation helps to register `@GrpcClient` beans in the Spring context to be used with `@Autowired` and + `@Qualifier`. The annotation can be repeatedly added to any of your `@Configuration` classes. - [`Channel`](https://javadoc.io/page/io.grpc/grpc-all/latest/io/grpc/Channel.html): The Channel is a connection pool for a single address. The target servers might serve multiple grpc-services though. The address will be resolved using a `NameResolver` and might point to a fixed or dynamic number of servers. @@ -167,13 +171,51 @@ public class FoobarService { public String receiveGreeting(String name) { HelloRequest request = HelloRequest.newBuilder() .setName(name) - .build() + .build(); return myServiceStub.sayHello(request).getMessage(); } } ```` +Also you can feel free to inject stub with `@GrpcClientBean` with `@Configuration` for more wide usage in +another services. + +> **Note:** We recommend using either `@GrpcClientBean`s or fields annotated with `@GrpcClient` throughout your +> application, as mixing the two can cause confusion for future developers. + +````java +@Configuration +@GrpcClientBean( + clazz = TestServiceGrpc.TestServiceBlockingStub.class, + beanName = "blockingStub", + client = @GrpcClient("test") +) +public class YourCustomConfiguration { + + @Bean + FoobarService foobarService(@Autowired TestServiceGrpc.TestServiceBlockingStub blockingStub) { + return new FoobarService(blockingStub); + } + +} + +@Service +@AllArgsConsturtor +public class FoobarService { + + private TestServiceBlockingStub blockingStub; + + public String receiveGreeting(String name) { + HelloRequest request = HelloRequest.newBuilder() + .setName(name) + .build(); + return blockingStub.sayHello(request).getMessage(); + } + +} +```` + ## Additional Topics - *Getting Started* diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClient.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClient.java index b01330909..f9ebe17a2 100644 --- a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClient.java +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClient.java @@ -27,6 +27,7 @@ import javax.inject.Inject; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; import io.grpc.CallCredentials; import io.grpc.CallOptions; @@ -39,7 +40,7 @@ /** * An annotation for fields of type {@link Channel} or subclasses of {@link AbstractStub}/gRPC client services. Also - * works for annotated methods that only take a single parameter of the same types. Annotated fields/methods will be + * works for annotated methods that only take a single parameter of these types. Annotated fields/methods will be * automatically populated/invoked by Spring. * *

@@ -59,8 +60,7 @@ * interceptors and applied using {@link ClientInterceptors#interceptForward(Channel, ClientInterceptor...)}. *

* - * @author Michael (yidongnan@gmail.com) - * @since 2016/12/7 + * @see GrpcClientBean Add as bean to the {@link ApplicationContext}. */ @Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClientBean.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClientBean.java new file mode 100644 index 000000000..651916789 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClientBean.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2016-2021 Michael Zhang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.devh.boot.grpc.client.inject; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; + +/** + * Annotation that can be added to {@link Configuration} classes to add a {@link GrpcClient} bean to the + * {@link ApplicationContext}. + */ +@Target(ElementType.TYPE) +@Repeatable(GrpcClientBeans.class) +@Retention(RetentionPolicy.RUNTIME) +public @interface GrpcClientBean { + + /** + * The type of the bean to create. + * + * @return The type of the bean. + */ + Class clazz(); + + /** + * The name of the bean to create. If empty, a name will be generated automatically based on the bean class and the + * client name. + * + * @return The optional name of the bean. + */ + String beanName() default ""; + + /** + * The client definition used to create the channel and grab all properties. + * + * @return The client definition to use. + */ + GrpcClient client(); + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClientBeanPostProcessor.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClientBeanPostProcessor.java index eaa0e6049..3681777d3 100644 --- a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClientBeanPostProcessor.java +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClientBeanPostProcessor.java @@ -32,7 +32,10 @@ import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.ReflectionUtils; @@ -63,6 +66,9 @@ public class GrpcClientBeanPostProcessor implements BeanPostProcessor { private List stubTransformers = null; private List stubFactories = null; + // For bean registration via @GrpcClientBean + private ConfigurableListableBeanFactory configurableBeanFactory; + /** * Creates a new GrpcClientBeanPostProcessor with the given ApplicationContext. * @@ -77,31 +83,80 @@ public GrpcClientBeanPostProcessor(final ApplicationContext applicationContext) public Object postProcessBeforeInitialization(final Object bean, final String beanName) throws BeansException { Class clazz = bean.getClass(); do { - for (final Field field : clazz.getDeclaredFields()) { - final GrpcClient annotation = AnnotationUtils.findAnnotation(field, GrpcClient.class); - if (annotation != null) { - ReflectionUtils.makeAccessible(field); - ReflectionUtils.setField(field, bean, processInjectionPoint(field, field.getType(), annotation)); - } - } - for (final Method method : clazz.getDeclaredMethods()) { - final GrpcClient annotation = AnnotationUtils.findAnnotation(method, GrpcClient.class); - if (annotation != null) { - final Class[] paramTypes = method.getParameterTypes(); - if (paramTypes.length != 1) { - throw new BeanDefinitionStoreException( - "Method " + method + " doesn't have exactly one parameter."); - } - ReflectionUtils.makeAccessible(method); - ReflectionUtils.invokeMethod(method, bean, - processInjectionPoint(method, paramTypes[0], annotation)); - } + processFields(clazz, bean); + processMethods(clazz, bean); + + if (isAnnotatedWithConfiguration(clazz)) { + processGrpcClientBeansAnnotations(clazz); } + clazz = clazz.getSuperclass(); } while (clazz != null); return bean; } + /** + * Processes the bean's fields in the given class. + * + * @param clazz The class to process. + * @param bean The bean to process. + */ + private void processFields(final Class clazz, final Object bean) { + for (final Field field : clazz.getDeclaredFields()) { + final GrpcClient annotation = AnnotationUtils.findAnnotation(field, GrpcClient.class); + if (annotation != null) { + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, bean, processInjectionPoint(field, field.getType(), annotation)); + } + } + } + + + /** + * Processes the bean's methods in the given class. + * + * @param clazz The class to process. + * @param bean The bean to process. + */ + private void processMethods(final Class clazz, final Object bean) { + for (final Method method : clazz.getDeclaredMethods()) { + final GrpcClient annotation = AnnotationUtils.findAnnotation(method, GrpcClient.class); + if (annotation != null) { + final Class[] paramTypes = method.getParameterTypes(); + if (paramTypes.length != 1) { + throw new BeanDefinitionStoreException( + "Method " + method + " doesn't have exactly one parameter."); + } + ReflectionUtils.makeAccessible(method); + ReflectionUtils.invokeMethod(method, bean, + processInjectionPoint(method, paramTypes[0], annotation)); + } + } + } + + + /** + * Processes the given class's {@link GrpcClientBean} annotations. + * + * @param clazz The class to process. + */ + private void processGrpcClientBeansAnnotations(final Class clazz) { + for (final GrpcClientBean annotation : clazz.getAnnotationsByType(GrpcClientBean.class)) { + + final String beanNameToCreate = getBeanName(annotation); + try { + final ConfigurableListableBeanFactory beanFactory = getConfigurableBeanFactory(); + final Object beanValue = + processInjectionPoint(null, annotation.clazz(), annotation.client()); + beanFactory.registerSingleton(beanNameToCreate, beanValue); + } catch (final Exception e) { + throw new BeanCreationException(annotation + " on class " + clazz.getName(), beanNameToCreate, + "Unexpected exception while creating and registering bean", + e); + } + } + } + /** * Processes the given injection point and computes the appropriate value for the injection. * @@ -130,6 +185,7 @@ protected T processInjectionPoint(final Member injectionTarget, final Class< throw new IllegalStateException( "Injection value is null unexpectedly for " + name + " at " + injectionTarget); } + return value; } @@ -223,8 +279,12 @@ protected T valueForMember(final String name, final Member injectionTarget, } return injectionType.cast(stub); } else { - throw new InvalidPropertyException(injectionTarget.getDeclaringClass(), injectionTarget.getName(), - "Unsupported type " + injectionType.getName()); + if (injectionTarget != null) { + throw new InvalidPropertyException(injectionTarget.getDeclaringClass(), injectionTarget.getName(), + "Unsupported type " + injectionType.getName()); + } else { + throw new BeanInstantiationException(injectionType, "Unsupported grpc stub or channel type"); + } } } @@ -233,9 +293,9 @@ protected T valueForMember(final String name, final Member injectionTarget, * * @param stubClass The stub class that needs to be created. * @param channel The gRPC channel associated with the created stub, passed as a parameter to the stub factory. + * @return A newly created gRPC stub. * @throws BeanInstantiationException If the stub couldn't be created, either because the type isn't supported or * because of a failure in creation. - * @return A newly created gRPC stub. */ private AbstractStub createStub(final Class> stubClass, final Channel channel) { final StubFactory factory = getStubFactories().stream() @@ -265,4 +325,41 @@ private List getStubFactories() { return this.stubFactories; } + /** + * Lazy factory getter from the context for bean registration with {@link GrpcClientBean} annotations. + * + * @return configurable bean factory + */ + private ConfigurableListableBeanFactory getConfigurableBeanFactory() { + if (this.configurableBeanFactory == null) { + this.configurableBeanFactory = ((ConfigurableApplicationContext) this.applicationContext).getBeanFactory(); + } + return this.configurableBeanFactory; + } + + /** + * Gets the bean name from the given annotation. + * + * @param grpcClientBean The annotation to extract it from. + * @return The extracted name. + */ + private String getBeanName(final GrpcClientBean grpcClientBean) { + if (!grpcClientBean.beanName().isEmpty()) { + return grpcClientBean.beanName(); + } else { + return grpcClientBean.client().value() + grpcClientBean.clazz().getSimpleName(); + } + } + + /** + * Checks whether the given class is annotated with {@link Configuration}. + * + * @param clazz The class to check. + * @return True, if the given class is annotated with {@link Configuration}. False otherwise. + */ + private boolean isAnnotatedWithConfiguration(final Class clazz) { + final Configuration configurationAnnotation = AnnotationUtils.findAnnotation(clazz, Configuration.class); + return configurationAnnotation != null; + } + } diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClientBeans.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClientBeans.java new file mode 100644 index 000000000..a6b6aefb3 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClientBeans.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2016-2021 Michael Zhang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.devh.boot.grpc.client.inject; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Configuration; + +/** + * Annotation that can be added to {@link Configuration} classes to add {@link GrpcClient} beans to the + * {@link ApplicationContext}. + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface GrpcClientBeans { + + /** + * Helper field containing multiple {@link GrpcClientBean} definitions. + * + * @return An array with bean definitions to create. + */ + GrpcClientBean[] value(); + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/inject/GrpcClientBeanInjectionNegativeTest.java b/tests/src/test/java/net/devh/boot/grpc/test/inject/GrpcClientBeanInjectionNegativeTest.java new file mode 100644 index 000000000..0f9b1fc03 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/inject/GrpcClientBeanInjectionNegativeTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2016-2021 Michael Zhang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.devh.boot.grpc.test.inject; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.client.inject.GrpcClientBean; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.InProcessConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.proto.TestServiceGrpc; + +/** + * Test case covering probable conflicting situations with {@link GrpcClientBean} and {@link GrpcClient} usage. + */ +class GrpcClientBeanInjectionNegativeTest { + + @Test + void duplicateGrpcClientBeansTest() { + final SpringApplication app = new SpringApplication( + TwoSameGrpcClientBeansConfig.class, + InProcessConfiguration.class, + ServiceConfiguration.class, + BaseAutoConfiguration.class); + + final BeanCreationException error = assertThrows(BeanCreationException.class, app::run); + + final BeanCreationException cause = (BeanCreationException) error.getCause(); + assertEquals("duplicateStub", cause.getBeanName()); + assertThat(cause).hasMessageContaining(TwoSameGrpcClientBeansConfig.class.getName()); + } + + @Test + void badGrpcClientBeanTest() { + final SpringApplication app = new SpringApplication( + BadGrpcClientBeanConfig.class, + InProcessConfiguration.class, + ServiceConfiguration.class, + BaseAutoConfiguration.class); + + final BeanCreationException error = assertThrows(BeanCreationException.class, app::run); + + final BeanCreationException cause = (BeanCreationException) error.getCause(); + assertEquals("badStub", cause.getBeanName()); + assertThat(cause) + .hasMessageContaining(BadGrpcClientBeanConfig.class.getName()) + .hasMessageContaining(String.class.getName()); + } + + @Test + @Disabled("This does not fail unexpectedly") + void mixedGrpcClientBeanAndFieldTest() { + final SpringApplication app = new SpringApplication( + MixedBeanConfig.class, + InProcessConfiguration.class, + ServiceConfiguration.class, + BaseAutoConfiguration.class); + + assertThrows(BeanCreationException.class, app::run); + } + + @TestConfiguration + @GrpcClientBean( + clazz = TestServiceGrpc.TestServiceBlockingStub.class, + beanName = "duplicateStub", + client = @GrpcClient("test")) + @GrpcClientBean( + clazz = TestServiceGrpc.TestServiceBlockingStub.class, + beanName = "duplicateStub", + client = @GrpcClient("test")) + public static class TwoSameGrpcClientBeansConfig { + } + + @TestConfiguration + @GrpcClientBean( + clazz = String.class, + beanName = "badStub", + client = @GrpcClient("test")) + public static class BadGrpcClientBeanConfig { + } + + @TestConfiguration + @GrpcClientBean( + clazz = TestServiceGrpc.TestServiceFutureStub.class, + beanName = "mixedStub", + client = @GrpcClient("test")) + public static class MixedBeanConfig { + + @GrpcClient("test") + TestServiceGrpc.TestServiceBlockingStub stub; + + @Bean("mixedStub") + public TestServiceGrpc.TestServiceBlockingStub mixedStub() { + return this.stub; + } + + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/inject/GrpcClientBeanInjectionTest.java b/tests/src/test/java/net/devh/boot/grpc/test/inject/GrpcClientBeanInjectionTest.java new file mode 100644 index 000000000..2f17f0210 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/inject/GrpcClientBeanInjectionTest.java @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2016-2021 Michael Zhang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the + * Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE + * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR + * OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package net.devh.boot.grpc.test.inject; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import io.grpc.stub.AbstractStub; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.client.inject.GrpcClientBean; +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.ServiceConfiguration; +import net.devh.boot.grpc.test.proto.TestServiceGrpc; + +/** + * Test case should cover auto wiring with field and method injection + */ +@SpringBootTest +@SpringJUnitConfig( + classes = { + GrpcClientBeanInjectionTest.TestConfig.class, + GrpcClientBeanInjectionTest.CustomConfig.class, + InProcessConfiguration.class, + ServiceConfiguration.class, + BaseAutoConfiguration.class + }) +@DirtiesContext +class GrpcClientBeanInjectionTest { + + @Autowired + TestServiceGrpc.TestServiceBlockingStub blockingStub; + + @Autowired + TestServiceGrpc.TestServiceFutureStub futureStubForClientTest; + + @Autowired + TestServiceGrpc.TestServiceBlockingStub anotherBlockingStub; + + @Autowired + TestServiceGrpc.TestServiceBlockingStub unnamedTestServiceBlockingStub; + + @Autowired + CustomGrpc.FactoryMethodAccessibleStub anotherServiceClientBean; + + @Autowired + String aboutMethodInjectedBlockingStubBean; + + @Autowired + TestServiceGrpc.TestServiceBlockingStub stubFromSpringConfiguration; + + /** + * Test should cover bean simple single bean creation with {@link GrpcClientBean} annotation with + * {@link TestConfiguration} + */ + @Test + void singleContextInjectionFromTestConfigurationTest() { + assertNotNull(this.blockingStub, "blockingStub"); + } + + /** + * Test should cover bean simple single bean creation with {@link GrpcClientBean} annotation with + * {@link Configuration} + */ + @Test + void singleContextInjectionFromConfigurationTest() { + assertNotNull(this.stubFromSpringConfiguration, "stubFromSpringConfiguration"); + } + + /** + * Test should cover creation of another bean with different stub class and same grpc client definition + */ + @Test + void anotherSubTypeAndSameClientDefinitionTest() { + assertNotNull(this.futureStubForClientTest, "futureStubForClientTest"); + } + + /** + * Test should cover creation of another bean same different stub class, but different grpc client definition + */ + @Test + void twoDifferentClientDefinitionsTest() { + assertNotNull(this.anotherBlockingStub, "blockingStub"); + } + + /** + * Test should cover creation of another bean with different service and stub class with same grpc client definition + */ + @Test + void anotherGrpcServiceAndSameGrpcClientDefinitionTest() { + assertNotNull(this.anotherServiceClientBean, "anotherServiceClientBean"); + } + + /** + * Test should cover creation of bean without defined bean name + */ + @Test + void unnamedBeanContextInjectionTest() { + assertNotNull(this.unnamedTestServiceBlockingStub, "unnamedTestServiceBlockingStub"); + } + + /** + * Test should cover bean method injection via {@link Autowired} and {@link Qualifier} from context + */ + @Test + void autoWiringQualifierMethodInjectionFromContextTest() { + assertNotNull(this.aboutMethodInjectedBlockingStubBean, "aboutBlockingStubBean"); + } + + @TestConfiguration + @GrpcClientBean( + clazz = TestServiceGrpc.TestServiceBlockingStub.class, + beanName = "blockingStub", + client = @GrpcClient("test")) + @GrpcClientBean( + clazz = TestServiceGrpc.TestServiceFutureStub.class, + beanName = "futureStubForClientTest", + client = @GrpcClient("test")) + @GrpcClientBean( + clazz = TestServiceGrpc.TestServiceBlockingStub.class, + beanName = "anotherBlockingStub", + client = @GrpcClient("anotherTest")) + @GrpcClientBean( + clazz = TestServiceGrpc.TestServiceBlockingStub.class, + client = @GrpcClient("unnamed")) + @GrpcClientBean( + clazz = CustomGrpc.FactoryMethodAccessibleStub.class, + beanName = "anotherServiceClientBean", + client = @GrpcClient("test")) + public static class TestConfig { + + @Bean + String aboutMethodInjectedBlockingStubBean( + @Qualifier("anotherBlockingStub") final TestServiceGrpc.TestServiceBlockingStub blockingStub) { + return blockingStub.toString(); + } + + @Bean + StubFactory customStubFactory() { + return new StandardJavaGrpcStubFactory() { + + @Override + public boolean isApplicable(final Class> stubType) { + return CustomStub.class.isAssignableFrom(stubType); + } + + @Override + protected String getFactoryMethodName() { + return "custom"; + } + + }; + } + } + + @Configuration + @GrpcClientBean( + clazz = TestServiceGrpc.TestServiceBlockingStub.class, + beanName = "stubFromSpringConfiguration", + client = @GrpcClient("test2")) + public static class CustomConfig { + } + +}