From 7735ae7029b24379ae283a9d2ab637b5ad367e3c Mon Sep 17 00:00:00 2001 From: haochencheng Date: Sat, 24 Jul 2021 22:09:21 +0800 Subject: [PATCH 1/2] add sleuth zipkin integration --- .gitattributes | 38 + .github/ISSUE_TEMPLATE/bug_report.md | 43 + .github/ISSUE_TEMPLATE/feature_request.md | 24 + .github/ISSUE_TEMPLATE/question.md | 35 + .github/workflows/build-master.yml | 44 ++ .github/workflows/pull-request.yml | 35 + .gitignore | 24 + CONTRIBUTING.md | 26 + LICENSE | 21 + README-zh-CN.md | 227 ++++++ README.md | 252 ++++++ build.gradle | 280 +++++++ crowdin.yml | 9 + deploy.gradle | 97 +++ docs/_config.yml | 1 + docs/assets/css/style.scss | 20 + docs/assets/images/client-project-setup.dot | 60 ++ docs/assets/images/client-project-setup.svg | 139 ++++ docs/assets/images/server-project-setup.dot | 60 ++ docs/assets/images/server-project-setup.svg | 139 ++++ docs/assets/images/server-security.graphml | 727 +++++++++++++++++ docs/assets/images/server-security.svg | 1 + docs/en/actuator.md | 155 ++++ docs/en/benchmarking.md | 23 + docs/en/brave.md | 82 ++ docs/en/client/configuration.md | 281 +++++++ docs/en/client/getting-started.md | 185 +++++ docs/en/client/security.md | 133 ++++ docs/en/client/testing.md | 266 +++++++ docs/en/contributions.md | 13 + docs/en/examples.md | 35 + docs/en/flavors.md | 46 ++ docs/en/index.md | 33 + docs/en/kubernetes.md | 70 ++ docs/en/server/configuration.md | 147 ++++ docs/en/server/contextual-data.md | 72 ++ docs/en/server/exception-handling.md | 119 +++ docs/en/server/getting-started.md | 349 ++++++++ docs/en/server/security.md | 290 +++++++ docs/en/server/testing.md | 379 +++++++++ docs/en/trouble-shooting.md | 358 +++++++++ docs/en/versions.md | 100 +++ docs/index.md | 6 + docs/zh-CN/actuator.md | 145 ++++ docs/zh-CN/benchmarking.md | 22 + docs/zh-CN/brave.md | 72 ++ docs/zh-CN/client/configuration.md | 114 +++ docs/zh-CN/client/getting-started.md | 144 ++++ docs/zh-CN/client/security.md | 115 +++ docs/zh-CN/contributions.md | 13 + docs/zh-CN/examples.md | 40 + docs/zh-CN/index.md | 24 + docs/zh-CN/server/configuration.md | 108 +++ docs/zh-CN/server/contextual-data.md | 64 ++ docs/zh-CN/server/getting-started.md | 302 +++++++ docs/zh-CN/server/security.md | 255 ++++++ docs/zh-CN/server/testing.md | 283 +++++++ docs/zh-CN/trouble-shooting.md | 268 +++++++ docs/zh-CN/versions.md | 90 +++ examples/README.md | 74 ++ examples/cloud-eureka-server/build.gradle | 13 + .../cloud/server/EurekaServerApplication.java | 40 + .../src/main/resources/application.yml | 27 + examples/cloud-grpc-client/build.gradle | 37 + .../cloud/client/CloudClientApplication.java | 40 + .../GlobalInterceptorConfiguration.java | 43 + .../cloud/client/GrpcClientController.java | 39 + .../cloud/client/GrpcClientService.java | 49 ++ .../cloud/client/LogGrpcInterceptor.java | 50 ++ .../src/main/resources/application-consul.yml | 7 + .../src/main/resources/application-eureka.yml | 10 + .../src/main/resources/application-nacos.yml | 6 + .../main/resources/application-zookeeper.yml | 6 + .../src/main/resources/application.yml | 12 + examples/cloud-grpc-server/build.gradle | 37 + .../cloud/server/CloudServerApplication.java | 40 + .../GlobalInterceptorConfiguration.java | 43 + .../cloud/server/GrpcServerService.java | 39 + .../cloud/server/LogGrpcInterceptor.java | 49 ++ .../src/main/resources/application-consul.yml | 7 + .../src/main/resources/application-eureka.yml | 8 + .../src/main/resources/application-nacos.yml | 6 + .../main/resources/application-zookeeper.yml | 6 + .../src/main/resources/application.yml | 10 + .../build.gradle | 42 + .../client/CloudSleuthClientApplication.java | 40 + .../cloud/client/GrpcClientController.java | 39 + .../cloud/client/GrpcClientService.java | 49 ++ .../cloud/client/GrpcSleuthClientConfig.java | 84 ++ .../src/main/resources/application-consul.yml | 7 + .../src/main/resources/application-eureka.yml | 10 + .../src/main/resources/application-nacos.yml | 6 + .../main/resources/application-zookeeper.yml | 6 + .../src/main/resources/application.yml | 19 + .../build.gradle | 42 + .../server/CloudSleuthServerApplication.java | 40 + .../cloud/server/GrpcServerService.java | 39 + .../cloud/server/GrpcSleuthServerConfig.java | 70 ++ .../src/main/resources/application-consul.yml | 7 + .../src/main/resources/application-eureka.yml | 8 + .../src/main/resources/application-nacos.yml | 6 + .../main/resources/application-zookeeper.yml | 6 + .../src/main/resources/application.yml | 15 + examples/grpc-lib/build.gradle | 56 ++ .../grpc-lib/src/main/proto/helloworld.proto | 22 + examples/local-grpc-client/build.gradle | 9 + .../GlobalClientInterceptorConfiguration.java | 35 + .../local/client/GrpcClientController.java | 40 + .../local/client/GrpcClientService.java | 47 ++ .../client/LocalGrpcClientApplication.java | 34 + .../local/client/LogGrpcInterceptor.java | 44 ++ .../src/main/resources/application.yml | 13 + examples/local-grpc-server/build.gradle | 9 + .../GlobalInterceptorConfiguration.java | 32 + .../local/server/GrpcServerService.java | 41 + .../server/LocalGrpcServerApplication.java | 34 + .../local/server/LogGrpcInterceptor.java | 43 + .../src/main/resources/application.yml | 6 + examples/security-grpc-client/build.gradle | 10 + .../security/client/GrpcClientController.java | 53 ++ .../security/client/GrpcClientService.java | 54 ++ .../client/SecurityConfiguration.java | 47 ++ .../client/SecurityGrpcClientApplication.java | 35 + .../src/main/resources/application.yml | 15 + examples/security-grpc-server/build.gradle | 10 + .../security/server/GrpcServerService.java | 45 ++ .../server/SecurityConfiguration.java | 118 +++ .../server/SecurityGrpcServerApplication.java | 35 + .../src/main/resources/application.yml | 6 + extra/eclipse/eclipse-formatter.xml | 337 ++++++++ extra/eclipse/eclipse.importorder | 6 + extra/spotless/mit-license.java | 16 + gradle.properties | 7 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 5 + gradlew | 185 +++++ gradlew.bat | 89 +++ .../build.gradle | 41 + .../GrpcClientAutoConfiguration.java | 191 +++++ .../GrpcClientHealthAutoConfiguration.java | 67 ++ .../GrpcClientMetricAutoConfiguration.java | 55 ++ .../GrpcClientSecurityAutoConfiguration.java | 69 ++ .../GrpcClientTraceAutoConfiguration.java | 56 ++ .../GrpcDiscoveryClientAutoConfiguration.java | 40 + .../client/autoconfigure/package-info.java | 5 + .../AbstractChannelFactory.java | 370 +++++++++ .../channelfactory/GrpcChannelConfigurer.java | 44 ++ .../channelfactory/GrpcChannelFactory.java | 108 +++ .../InProcessChannelFactory.java | 76 ++ .../InProcessOrAlternativeChannelFactory.java | 103 +++ .../channelfactory/NettyChannelFactory.java | 164 ++++ .../ShadedNettyChannelFactory.java | 162 ++++ .../client/channelfactory/package-info.java | 5 + .../client/config/GrpcChannelProperties.java | 742 ++++++++++++++++++ .../client/config/GrpcChannelsProperties.java | 117 +++ .../grpc/client/config/NegotiationType.java | 44 ++ .../boot/grpc/client/config/package-info.java | 5 + .../boot/grpc/client/inject/GrpcClient.java | 123 +++ .../inject/GrpcClientBeanPostProcessor.java | 268 +++++++ .../grpc/client/inject/StubTransformer.java | 49 ++ .../boot/grpc/client/inject/package-info.java | 5 + ...tionGlobalClientInterceptorConfigurer.java | 72 ++ .../GlobalClientInterceptorConfigurer.java | 40 + .../GlobalClientInterceptorRegistry.java | 99 +++ .../GrpcGlobalClientInterceptor.java | 42 + .../interceptor/OrderedClientInterceptor.java | 67 ++ .../grpc/client/interceptor/package-info.java | 5 + .../metric/MetricCollectingClientCall.java | 77 ++ .../MetricCollectingClientCallListener.java | 68 ++ .../MetricCollectingClientInterceptor.java | 123 +++ .../boot/grpc/client/metric/package-info.java | 5 + .../DiscoveryClientNameResolver.java | 297 +++++++ .../DiscoveryClientResolverFactory.java | 131 ++++ .../NameResolverRegistration.java | 79 ++ .../nameresolver/StaticNameResolver.java | 99 +++ .../StaticNameResolverProvider.java | 104 +++ .../client/nameresolver/package-info.java | 5 + .../security/CallCredentialsHelper.java | 390 +++++++++ .../grpc/client/security/package-info.java | 10 + .../client/stubfactory/AsyncStubFactory.java | 34 + .../stubfactory/BlockingStubFactory.java | 34 + .../stubfactory/FallbackStubFactory.java | 72 ++ .../client/stubfactory/FutureStubFactory.java | 34 + .../StandardJavaGrpcStubFactory.java | 52 ++ .../grpc/client/stubfactory/StubFactory.java | 51 ++ ...itional-spring-configuration-metadata.json | 161 ++++ .../services/io.grpc.NameResolverProvider | 1 + .../main/resources/META-INF/spring.factories | 8 + .../config/GrpcChannelPropertiesConfig.java | 29 + .../GrpcChannelPropertiesGivenUnitTest.java | 51 ++ .../GrpcChannelPropertiesGlobalTest.java | 56 ++ ...hannelPropertiesNegativeGivenUnitTest.java | 48 ++ ...pcChannelPropertiesNegativeNoUnitTest.java | 48 ++ .../GrpcChannelPropertiesNoUnitTest.java | 51 ++ .../GrpcClientBeanPostProcessorTest.java | 160 ++++ grpc-client-spring-boot-starter/build.gradle | 12 + grpc-common-spring-boot/build.gradle | 22 + .../GrpcCommonCodecAutoConfiguration.java | 84 ++ .../GrpcCommonTraceAutoConfiguration.java | 43 + .../common/autoconfigure/package-info.java | 6 + .../codec/AnnotationGrpcCodecDiscoverer.java | 65 ++ .../boot/grpc/common/codec/CodecType.java | 69 ++ .../boot/grpc/common/codec/GrpcCodec.java | 57 ++ .../common/codec/GrpcCodecDefinition.java | 74 ++ .../common/codec/GrpcCodecDiscoverer.java | 37 + .../boot/grpc/common/codec/package-info.java | 5 + .../AbstractMetricCollectingInterceptor.java | 216 +++++ .../grpc/common/metric/MetricConstants.java | 72 ++ .../boot/grpc/common/metric/MetricUtils.java | 75 ++ .../boot/grpc/common/metric/package-info.java | 5 + .../common/security/SecurityConstants.java | 50 ++ .../grpc/common/security/package-info.java | 5 + .../devh/boot/grpc/common/util/GrpcUtils.java | 104 +++ .../grpc/common/util/InterceptorOrder.java | 65 ++ .../boot/grpc/common/util/package-info.java | 5 + .../main/resources/META-INF/spring.factories | 4 + .../build.gradle | 42 + .../boot/grpc/server/advice/GrpcAdvice.java | 44 ++ .../server/advice/GrpcAdviceDiscoverer.java | 93 +++ .../advice/GrpcAdviceExceptionHandler.java | 117 +++ .../GrpcAdviceExceptionInterceptor.java | 67 ++ .../advice/GrpcAdviceExceptionListener.java | 108 +++ .../advice/GrpcAdviceIsPresentCondition.java | 48 ++ .../server/advice/GrpcExceptionHandler.java | 82 ++ .../GrpcExceptionHandlerMethodResolver.java | 191 +++++ .../GrpcAdviceAutoConfiguration.java | 78 ++ .../GrpcHealthServiceAutoConfiguration.java | 59 ++ .../GrpcMetadataConsulConfiguration.java | 65 ++ .../GrpcMetadataEurekaConfiguration.java | 59 ++ .../GrpcMetadataNacosConfiguration.java | 60 ++ .../GrpcMetadataZookeeperConfiguration.java | 59 ++ ...rpcReflectionServiceAutoConfiguration.java | 52 ++ .../GrpcServerAutoConfiguration.java | 137 ++++ .../GrpcServerFactoryAutoConfiguration.java | 176 +++++ .../GrpcServerMetricAutoConfiguration.java | 110 +++ .../GrpcServerSecurityAutoConfiguration.java | 110 +++ .../GrpcServerTraceAutoConfiguration.java | 56 ++ .../server/autoconfigure/package-info.java | 5 + .../boot/grpc/server/cloud/package-info.java | 5 + .../ConditionalOnInterprocessServer.java | 38 + .../boot/grpc/server/config/ClientAuth.java | 43 + .../server/config/GrpcServerProperties.java | 393 ++++++++++ .../boot/grpc/server/config/package-info.java | 5 + ...tionGlobalServerInterceptorConfigurer.java | 72 ++ .../GlobalServerInterceptorConfigurer.java | 40 + .../GlobalServerInterceptorRegistry.java | 99 +++ .../GrpcGlobalServerInterceptor.java | 42 + .../interceptor/OrderedServerInterceptor.java | 67 ++ .../grpc/server/interceptor/package-info.java | 5 + .../metric/MetricCollectingServerCall.java | 69 ++ .../MetricCollectingServerCallListener.java | 83 ++ .../MetricCollectingServerInterceptor.java | 150 ++++ .../boot/grpc/server/metric/package-info.java | 5 + .../server/nameresolver/SelfNameResolver.java | 186 +++++ .../nameresolver/SelfNameResolverFactory.java | 79 ++ .../server/nameresolver/package-info.java | 5 + .../grpc/server/scope/GrpcRequestScope.java | 266 +++++++ .../AnonymousAuthenticationReader.java | 75 ++ .../BasicGrpcAuthenticationReader.java | 87 ++ .../BearerAuthenticationReader.java | 89 +++ .../CompositeGrpcAuthenticationReader.java | 62 ++ .../GrpcAuthenticationReader.java | 62 ++ .../SSLContextGrpcAuthenticationReader.java | 77 ++ .../X509CertificateAuthentication.java | 101 +++ ...X509CertificateAuthenticationProvider.java | 146 ++++ .../security/authentication/package-info.java | 5 + .../AbstractGrpcSecurityMetadataSource.java | 47 ++ .../security/check/AccessPredicate.java | 278 +++++++ .../check/AccessPredicateConfigAttribute.java | 85 ++ .../security/check/AccessPredicateVoter.java | 69 ++ .../security/check/AccessPredicates.java | 78 ++ .../check/GrpcSecurityMetadataSource.java | 49 ++ .../ManualGrpcSecurityMetadataSource.java | 147 ++++ .../server/security/check/package-info.java | 5 + ...tractAuthenticatingServerCallListener.java | 153 ++++ .../AuthenticatingServerInterceptor.java | 51 ++ ...uthorizationCheckingServerInterceptor.java | 108 +++ ...efaultAuthenticatingServerInterceptor.java | 178 +++++ ...ExceptionTranslatingServerInterceptor.java | 134 ++++ .../security/interceptors/package-info.java | 5 + .../grpc/server/security/package-info.java | 10 + .../AbstractGrpcServerFactory.java | 187 +++++ .../serverfactory/GrpcServerConfigurer.java | 43 + .../serverfactory/GrpcServerFactory.java | 65 ++ .../serverfactory/GrpcServerLifecycle.java | 150 ++++ .../InProcessGrpcServerFactory.java | 101 +++ .../serverfactory/NettyGrpcServerFactory.java | 173 ++++ .../ShadedNettyGrpcServerFactory.java | 174 ++++ .../server/serverfactory/package-info.java | 5 + .../AnnotationGrpcServiceDiscoverer.java | 100 +++ .../boot/grpc/server/service/GrpcService.java | 84 ++ .../server/service/GrpcServiceDefinition.java | 76 ++ .../server/service/GrpcServiceDiscoverer.java | 41 + .../grpc/server/service/package-info.java | 5 + .../main/resources/META-INF/spring.factories | 14 + .../advice/GrpcAdviceDiscovererTest.java | 116 +++ .../AwaitableStreamObserver.java | 90 +++ ...thServiceDefaultAutoConfigurationTest.java | 68 ++ ...althServiceFalseAutoConfigurationTest.java | 51 ++ ...ealthServiceTrueAutoConfigurationTest.java | 32 + ...onServiceDefaultAutoConfigurationTest.java | 71 ++ ...tionServiceFalseAutoConfigurationTest.java | 51 ++ ...ctionServiceTrueAutoConfigurationTest.java | 32 + .../config/GrpcServerPropertiesConfig.java | 29 + .../GrpcServerPropertiesGivenUnitTest.java | 52 ++ ...ServerPropertiesNegativeGivenUnitTest.java | 47 ++ ...rpcServerPropertiesNegativeNoUnitTest.java | 47 ++ .../GrpcServerPropertiesNoUnitTest.java | 50 ++ .../AbstractGrpcServerFactoryTest.java | 60 ++ .../GrpcServerLifecycleTest.java | 197 +++++ .../src/test/resources/logback-test.xml | 17 + grpc-server-spring-boot-starter/build.gradle | 12 + private.key.enc | Bin 0 -> 3600 bytes settings.gradle | 21 + testExamples.sh | 207 +++++ tests/build.gradle | 83 ++ .../AbstractSimpleServerClientTest.java | 136 ++++ .../advice/AdviceExceptionHandlingTest.java | 211 +++++ .../AdviceIsPresentAutoConfigurationTest.java | 130 +++ ...AdviceNotPresentAutoConfigurationTest.java | 87 ++ .../grpc/test/advice/GrpcMetaDataUtils.java | 41 + .../grpc/test/codec/AbstractCodecTest.java | 118 +++ .../boot/grpc/test/codec/BeanCodecTest.java | 96 +++ .../boot/grpc/test/codec/CustomCodecTest.java | 98 +++ .../boot/grpc/test/codec/GzipCodecTest.java | 78 ++ .../grpc/test/codec/IdentityCodecTest.java | 78 ++ .../AnnotatedSecurityConfiguration.java | 32 + ...waitableServerClientCallConfiguration.java | 184 +++++ .../test/config/BaseAutoConfiguration.java | 35 + .../config/BeanAnnotatedServiceConfig.java | 55 ++ .../grpc/test/config/GrpcAdviceConfig.java | 166 ++++ .../test/config/InProcessConfiguration.java | 60 ++ .../config/ManualSecurityConfiguration.java | 56 ++ .../grpc/test/config/MetricConfiguration.java | 38 + ...OrderedClientInterceptorConfiguration.java | 96 +++ ...OrderedServerInterceptorConfiguration.java | 95 +++ .../config/ScopedServiceConfiguration.java | 45 ++ .../test/config/ServiceConfiguration.java | 33 + .../WithBasicAuthSecurityConfiguration.java | 120 +++ .../WithCertificateSecurityConfiguration.java | 67 ++ .../boot/grpc/test/inject/CustomGrpc.java | 87 ++ .../boot/grpc/test/inject/CustomStub.java | 40 + .../test/inject/GrpcClientInjectionTest.java | 177 +++++ .../devh/boot/grpc/test/inject/OtherStub.java | 41 + .../DefaultServerInterceptorTest.java | 76 ++ .../OrderedClientInterceptorTest.java | 75 ++ .../OrderedServerInterceptorTest.java | 75 ++ .../PickupClientInterceptorTest.java | 144 ++++ .../PickupServerInterceptorTest.java | 150 ++++ .../metric/MetricAutoConfigurationTest.java | 68 ++ ...MetricCollectingClientInterceptorTest.java | 78 ++ .../MetricCollectingInterceptorTest.java | 620 +++++++++++++++ ...MetricCollectingServerInterceptorTest.java | 79 ++ .../MetricCustomAutoConfigurationTest.java | 95 +++ .../MetricFullAutoConfigurationTest.java | 76 ++ .../grpc/test/metric/MetricTestHelper.java | 93 +++ .../grpc/test/scope/GrpcRequestScopeTest.java | 147 ++++ .../test/security/AbstractSecurityTest.java | 262 +++++++ .../AbstractSecurityWithBasicAuthTest.java | 111 +++ .../AnnotatedSecurityWithBasicAuthTest.java | 48 ++ .../AnnotatedSecurityWithCertificateTest.java | 59 ++ .../test/security/ConcurrentSecurityTest.java | 105 +++ .../ManualSecurityWithBasicAuthTest.java | 48 ++ .../ManualSecurityWithCertificateTest.java | 59 ++ .../test/server/ScopedTestServiceImpl.java | 76 ++ .../grpc/test/server/TestServiceImpl.java | 201 +++++ .../grpc/test/server/WaitingTestService.java | 81 ++ .../setup/AbstractBrokenServerClientTest.java | 92 +++ .../setup/AbstractSimpleServerClientTest.java | 112 +++ .../test/setup/BeanAnnotatedServiceTest.java | 68 ++ ...BrokenClientSelfSignedMutualSetupTest.java | 53 ++ ...BrokenServerSelfSignedMutualSetupTest.java | 53 ++ .../CustomCiphersAndProtocolsSetupTest.java | 137 ++++ .../test/setup/ImmediateConnectTests.java | 131 ++++ .../grpc/test/setup/InProcessSetupTest.java | 44 ++ .../setup/InterAndInProcessSetup2Test.java | 180 +++++ .../setup/InterAndInProcessSetupTest.java | 180 +++++ .../setup/NameResolverConnectionTest.java | 86 ++ .../setup/NameResolverIPv4ConnectionTest.java | 76 ++ .../setup/NameResolverIPv6ConnectionTest.java | 75 ++ .../test/setup/OnlyInProcessServerTest.java | 179 +++++ .../grpc/test/setup/PlaintextSetupTest.java | 46 ++ .../setup/SelfNameResolverConnectionTest.java | 73 ++ .../setup/SelfSignedInProcessSetupTest.java | 57 ++ .../test/setup/SelfSignedMutualSetupTest.java | 55 ++ .../test/setup/SelfSignedServerSetupTest.java | 50 ++ .../boot/grpc/test/setup/UnixSetupTest.java | 50 ++ .../GrpcChannelLifecycleWithCallsTest.java | 199 +++++ .../GrpcServerLifecycleWithCallsTest.java | 178 +++++ .../grpc/test/util/DynamicTestCollection.java | 49 ++ .../boot/grpc/test/util/EnableOnIPv6.java | 39 + .../boot/grpc/test/util/FutureAssertions.java | 95 +++ .../boot/grpc/test/util/GrpcAssertions.java | 134 ++++ .../boot/grpc/test/util/LoggerTestUtil.java | 57 ++ .../grpc/test/util/RequireIPv6Condition.java | 75 ++ .../devh/boot/grpc/test/util/TriConsumer.java | 25 + tests/src/test/proto/TestService.proto | 39 + .../test/resources/certificates/client1.crt | 29 + .../test/resources/certificates/client1.key | 52 ++ .../test/resources/certificates/client2.crt | 29 + .../test/resources/certificates/client2.key | 52 ++ .../certificates/generateCertificates.sh | 15 + .../test/resources/certificates/server.crt | 29 + .../test/resources/certificates/server.key | 52 ++ .../certificates/trusted-clients-collection | 58 ++ .../certificates/trusted-servers-collection | 29 + tests/src/test/resources/logback-test.xml | 19 + 407 files changed, 33360 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/ISSUE_TEMPLATE/question.md create mode 100644 .github/workflows/build-master.yml create mode 100644 .github/workflows/pull-request.yml create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README-zh-CN.md create mode 100644 README.md create mode 100644 build.gradle create mode 100644 crowdin.yml create mode 100644 deploy.gradle create mode 100644 docs/_config.yml create mode 100644 docs/assets/css/style.scss create mode 100644 docs/assets/images/client-project-setup.dot create mode 100644 docs/assets/images/client-project-setup.svg create mode 100644 docs/assets/images/server-project-setup.dot create mode 100644 docs/assets/images/server-project-setup.svg create mode 100644 docs/assets/images/server-security.graphml create mode 100644 docs/assets/images/server-security.svg create mode 100644 docs/en/actuator.md create mode 100644 docs/en/benchmarking.md create mode 100644 docs/en/brave.md create mode 100644 docs/en/client/configuration.md create mode 100644 docs/en/client/getting-started.md create mode 100644 docs/en/client/security.md create mode 100644 docs/en/client/testing.md create mode 100644 docs/en/contributions.md create mode 100644 docs/en/examples.md create mode 100644 docs/en/flavors.md create mode 100644 docs/en/index.md create mode 100644 docs/en/kubernetes.md create mode 100644 docs/en/server/configuration.md create mode 100644 docs/en/server/contextual-data.md create mode 100644 docs/en/server/exception-handling.md create mode 100644 docs/en/server/getting-started.md create mode 100644 docs/en/server/security.md create mode 100644 docs/en/server/testing.md create mode 100644 docs/en/trouble-shooting.md create mode 100644 docs/en/versions.md create mode 100644 docs/index.md create mode 100644 docs/zh-CN/actuator.md create mode 100644 docs/zh-CN/benchmarking.md create mode 100644 docs/zh-CN/brave.md create mode 100644 docs/zh-CN/client/configuration.md create mode 100644 docs/zh-CN/client/getting-started.md create mode 100644 docs/zh-CN/client/security.md create mode 100644 docs/zh-CN/contributions.md create mode 100644 docs/zh-CN/examples.md create mode 100644 docs/zh-CN/index.md create mode 100644 docs/zh-CN/server/configuration.md create mode 100644 docs/zh-CN/server/contextual-data.md create mode 100644 docs/zh-CN/server/getting-started.md create mode 100644 docs/zh-CN/server/security.md create mode 100644 docs/zh-CN/server/testing.md create mode 100644 docs/zh-CN/trouble-shooting.md create mode 100644 docs/zh-CN/versions.md create mode 100644 examples/README.md create mode 100644 examples/cloud-eureka-server/build.gradle create mode 100644 examples/cloud-eureka-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/EurekaServerApplication.java create mode 100644 examples/cloud-eureka-server/src/main/resources/application.yml create mode 100644 examples/cloud-grpc-client/build.gradle create mode 100644 examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/CloudClientApplication.java create mode 100644 examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GlobalInterceptorConfiguration.java create mode 100644 examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcClientController.java create mode 100644 examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcClientService.java create mode 100644 examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/LogGrpcInterceptor.java create mode 100644 examples/cloud-grpc-client/src/main/resources/application-consul.yml create mode 100644 examples/cloud-grpc-client/src/main/resources/application-eureka.yml create mode 100644 examples/cloud-grpc-client/src/main/resources/application-nacos.yml create mode 100644 examples/cloud-grpc-client/src/main/resources/application-zookeeper.yml create mode 100644 examples/cloud-grpc-client/src/main/resources/application.yml create mode 100644 examples/cloud-grpc-server/build.gradle create mode 100644 examples/cloud-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/CloudServerApplication.java create mode 100644 examples/cloud-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GlobalInterceptorConfiguration.java create mode 100644 examples/cloud-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GrpcServerService.java create mode 100644 examples/cloud-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/LogGrpcInterceptor.java create mode 100644 examples/cloud-grpc-server/src/main/resources/application-consul.yml create mode 100644 examples/cloud-grpc-server/src/main/resources/application-eureka.yml create mode 100644 examples/cloud-grpc-server/src/main/resources/application-nacos.yml create mode 100644 examples/cloud-grpc-server/src/main/resources/application-zookeeper.yml create mode 100644 examples/cloud-grpc-server/src/main/resources/application.yml create mode 100644 examples/cloud-sleuth-zipkin-grpc-client/build.gradle create mode 100644 examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/CloudSleuthClientApplication.java create mode 100644 examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcClientController.java create mode 100644 examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcClientService.java create mode 100644 examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcSleuthClientConfig.java create mode 100644 examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application-consul.yml create mode 100644 examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application-eureka.yml create mode 100644 examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application-nacos.yml create mode 100644 examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application-zookeeper.yml create mode 100644 examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application.yml create mode 100644 examples/cloud-sleuth-zipkin-grpc-server/build.gradle create mode 100644 examples/cloud-sleuth-zipkin-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/CloudSleuthServerApplication.java create mode 100644 examples/cloud-sleuth-zipkin-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GrpcServerService.java create mode 100644 examples/cloud-sleuth-zipkin-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GrpcSleuthServerConfig.java create mode 100644 examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application-consul.yml create mode 100644 examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application-eureka.yml create mode 100644 examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application-nacos.yml create mode 100644 examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application-zookeeper.yml create mode 100644 examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application.yml create mode 100644 examples/grpc-lib/build.gradle create mode 100644 examples/grpc-lib/src/main/proto/helloworld.proto create mode 100644 examples/local-grpc-client/build.gradle create mode 100644 examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/GlobalClientInterceptorConfiguration.java create mode 100644 examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/GrpcClientController.java create mode 100644 examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/GrpcClientService.java create mode 100644 examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/LocalGrpcClientApplication.java create mode 100644 examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/LogGrpcInterceptor.java create mode 100644 examples/local-grpc-client/src/main/resources/application.yml create mode 100644 examples/local-grpc-server/build.gradle create mode 100644 examples/local-grpc-server/src/main/java/net/devh/boot/grpc/examples/local/server/GlobalInterceptorConfiguration.java create mode 100644 examples/local-grpc-server/src/main/java/net/devh/boot/grpc/examples/local/server/GrpcServerService.java create mode 100644 examples/local-grpc-server/src/main/java/net/devh/boot/grpc/examples/local/server/LocalGrpcServerApplication.java create mode 100644 examples/local-grpc-server/src/main/java/net/devh/boot/grpc/examples/local/server/LogGrpcInterceptor.java create mode 100644 examples/local-grpc-server/src/main/resources/application.yml create mode 100644 examples/security-grpc-client/build.gradle create mode 100644 examples/security-grpc-client/src/main/java/net/devh/boot/grpc/examples/security/client/GrpcClientController.java create mode 100644 examples/security-grpc-client/src/main/java/net/devh/boot/grpc/examples/security/client/GrpcClientService.java create mode 100644 examples/security-grpc-client/src/main/java/net/devh/boot/grpc/examples/security/client/SecurityConfiguration.java create mode 100644 examples/security-grpc-client/src/main/java/net/devh/boot/grpc/examples/security/client/SecurityGrpcClientApplication.java create mode 100644 examples/security-grpc-client/src/main/resources/application.yml create mode 100644 examples/security-grpc-server/build.gradle create mode 100644 examples/security-grpc-server/src/main/java/net/devh/boot/grpc/examples/security/server/GrpcServerService.java create mode 100644 examples/security-grpc-server/src/main/java/net/devh/boot/grpc/examples/security/server/SecurityConfiguration.java create mode 100644 examples/security-grpc-server/src/main/java/net/devh/boot/grpc/examples/security/server/SecurityGrpcServerApplication.java create mode 100644 examples/security-grpc-server/src/main/resources/application.yml create mode 100644 extra/eclipse/eclipse-formatter.xml create mode 100644 extra/eclipse/eclipse.importorder create mode 100644 extra/spotless/mit-license.java create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 grpc-client-spring-boot-autoconfigure/build.gradle create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientHealthAutoConfiguration.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientMetricAutoConfiguration.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientSecurityAutoConfiguration.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientTraceAutoConfiguration.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcDiscoveryClientAutoConfiguration.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/package-info.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/GrpcChannelConfigurer.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/GrpcChannelFactory.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/InProcessChannelFactory.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/InProcessOrAlternativeChannelFactory.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/NettyChannelFactory.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/ShadedNettyChannelFactory.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/package-info.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelsProperties.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/NegotiationType.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/package-info.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClient.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClientBeanPostProcessor.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/StubTransformer.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/package-info.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/AnnotationGlobalClientInterceptorConfigurer.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/GlobalClientInterceptorConfigurer.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/GlobalClientInterceptorRegistry.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/GrpcGlobalClientInterceptor.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/OrderedClientInterceptor.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/package-info.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metric/MetricCollectingClientCall.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metric/MetricCollectingClientCallListener.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metric/MetricCollectingClientInterceptor.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metric/package-info.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/DiscoveryClientNameResolver.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/DiscoveryClientResolverFactory.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/NameResolverRegistration.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/StaticNameResolver.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/StaticNameResolverProvider.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/package-info.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/security/CallCredentialsHelper.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/security/package-info.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/AsyncStubFactory.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/BlockingStubFactory.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/FallbackStubFactory.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/FutureStubFactory.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/StandardJavaGrpcStubFactory.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/StubFactory.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/services/io.grpc.NameResolverProvider create mode 100644 grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories create mode 100644 grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesConfig.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGivenUnitTest.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGlobalTest.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesNegativeGivenUnitTest.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesNegativeNoUnitTest.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesNoUnitTest.java create mode 100644 grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/inject/GrpcClientBeanPostProcessorTest.java create mode 100644 grpc-client-spring-boot-starter/build.gradle create mode 100644 grpc-common-spring-boot/build.gradle create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/autoconfigure/GrpcCommonCodecAutoConfiguration.java create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/autoconfigure/GrpcCommonTraceAutoConfiguration.java create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/autoconfigure/package-info.java create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/AnnotationGrpcCodecDiscoverer.java create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/CodecType.java create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/GrpcCodec.java create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/GrpcCodecDefinition.java create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/GrpcCodecDiscoverer.java create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/package-info.java create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/metric/AbstractMetricCollectingInterceptor.java create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/metric/MetricConstants.java create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/metric/MetricUtils.java create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/metric/package-info.java create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/security/SecurityConstants.java create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/security/package-info.java create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/util/GrpcUtils.java create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/util/InterceptorOrder.java create mode 100644 grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/util/package-info.java create mode 100644 grpc-common-spring-boot/src/main/resources/META-INF/spring.factories create mode 100644 grpc-server-spring-boot-autoconfigure/build.gradle create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdvice.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceDiscoverer.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceExceptionHandler.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceExceptionInterceptor.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceExceptionListener.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceIsPresentCondition.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcExceptionHandler.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcExceptionHandlerMethodResolver.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcAdviceAutoConfiguration.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcHealthServiceAutoConfiguration.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcMetadataConsulConfiguration.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcMetadataEurekaConfiguration.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcMetadataNacosConfiguration.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcMetadataZookeeperConfiguration.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcReflectionServiceAutoConfiguration.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerMetricAutoConfiguration.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerSecurityAutoConfiguration.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerTraceAutoConfiguration.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/package-info.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/cloud/package-info.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/condition/ConditionalOnInterprocessServer.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/config/ClientAuth.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/config/GrpcServerProperties.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/config/package-info.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/AnnotationGlobalServerInterceptorConfigurer.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/GlobalServerInterceptorConfigurer.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/GlobalServerInterceptorRegistry.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/GrpcGlobalServerInterceptor.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/OrderedServerInterceptor.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/package-info.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/metric/MetricCollectingServerCall.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/metric/MetricCollectingServerCallListener.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/metric/MetricCollectingServerInterceptor.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/metric/package-info.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/nameresolver/SelfNameResolver.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/nameresolver/SelfNameResolverFactory.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/nameresolver/package-info.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/scope/GrpcRequestScope.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/AnonymousAuthenticationReader.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/BasicGrpcAuthenticationReader.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/BearerAuthenticationReader.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/CompositeGrpcAuthenticationReader.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/GrpcAuthenticationReader.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReader.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/X509CertificateAuthentication.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/X509CertificateAuthenticationProvider.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/package-info.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AbstractGrpcSecurityMetadataSource.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AccessPredicate.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AccessPredicateConfigAttribute.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AccessPredicateVoter.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AccessPredicates.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/GrpcSecurityMetadataSource.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/ManualGrpcSecurityMetadataSource.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/package-info.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/AbstractAuthenticatingServerCallListener.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/AuthenticatingServerInterceptor.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/AuthorizationCheckingServerInterceptor.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/DefaultAuthenticatingServerInterceptor.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/ExceptionTranslatingServerInterceptor.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/package-info.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/package-info.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/AbstractGrpcServerFactory.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/GrpcServerConfigurer.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/GrpcServerFactory.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/GrpcServerLifecycle.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/InProcessGrpcServerFactory.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/NettyGrpcServerFactory.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/ShadedNettyGrpcServerFactory.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/package-info.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/AnnotationGrpcServiceDiscoverer.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/GrpcService.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/GrpcServiceDefinition.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/GrpcServiceDiscoverer.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/package-info.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories create mode 100644 grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/advice/GrpcAdviceDiscovererTest.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/AwaitableStreamObserver.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcHealthServiceDefaultAutoConfigurationTest.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcHealthServiceFalseAutoConfigurationTest.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcHealthServiceTrueAutoConfigurationTest.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcReflectionServiceDefaultAutoConfigurationTest.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcReflectionServiceFalseAutoConfigurationTest.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcReflectionServiceTrueAutoConfigurationTest.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesConfig.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesGivenUnitTest.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesNegativeGivenUnitTest.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesNegativeNoUnitTest.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesNoUnitTest.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/serverfactory/AbstractGrpcServerFactoryTest.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/serverfactory/GrpcServerLifecycleTest.java create mode 100644 grpc-server-spring-boot-autoconfigure/src/test/resources/logback-test.xml create mode 100644 grpc-server-spring-boot-starter/build.gradle create mode 100644 private.key.enc create mode 100644 settings.gradle create mode 100644 testExamples.sh create mode 100644 tests/build.gradle create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/advice/AbstractSimpleServerClientTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/advice/AdviceExceptionHandlingTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/advice/AdviceIsPresentAutoConfigurationTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/advice/AdviceNotPresentAutoConfigurationTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/advice/GrpcMetaDataUtils.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/codec/AbstractCodecTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/codec/BeanCodecTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/codec/CustomCodecTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/codec/GzipCodecTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/codec/IdentityCodecTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/config/AnnotatedSecurityConfiguration.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/config/AwaitableServerClientCallConfiguration.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/config/BaseAutoConfiguration.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/config/BeanAnnotatedServiceConfig.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/config/GrpcAdviceConfig.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/config/InProcessConfiguration.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/config/ManualSecurityConfiguration.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/config/MetricConfiguration.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/config/OrderedClientInterceptorConfiguration.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/config/OrderedServerInterceptorConfiguration.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/config/ScopedServiceConfiguration.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/config/ServiceConfiguration.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/config/WithBasicAuthSecurityConfiguration.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/config/WithCertificateSecurityConfiguration.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/inject/CustomGrpc.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/inject/CustomStub.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/inject/GrpcClientInjectionTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/inject/OtherStub.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/interceptor/DefaultServerInterceptorTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/interceptor/OrderedClientInterceptorTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/interceptor/OrderedServerInterceptorTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/interceptor/PickupClientInterceptorTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/interceptor/PickupServerInterceptorTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/metric/MetricAutoConfigurationTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/metric/MetricCollectingClientInterceptorTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/metric/MetricCollectingInterceptorTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/metric/MetricCollectingServerInterceptorTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/metric/MetricCustomAutoConfigurationTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/metric/MetricFullAutoConfigurationTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/metric/MetricTestHelper.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/scope/GrpcRequestScopeTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/security/AbstractSecurityTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/security/AbstractSecurityWithBasicAuthTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/security/AnnotatedSecurityWithBasicAuthTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/security/AnnotatedSecurityWithCertificateTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/security/ConcurrentSecurityTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/security/ManualSecurityWithBasicAuthTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/security/ManualSecurityWithCertificateTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/server/ScopedTestServiceImpl.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/server/TestServiceImpl.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/server/WaitingTestService.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/AbstractBrokenServerClientTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/AbstractSimpleServerClientTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/BeanAnnotatedServiceTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/BrokenClientSelfSignedMutualSetupTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/BrokenServerSelfSignedMutualSetupTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/CustomCiphersAndProtocolsSetupTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/ImmediateConnectTests.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/InProcessSetupTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/InterAndInProcessSetup2Test.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/InterAndInProcessSetupTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/NameResolverConnectionTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/NameResolverIPv4ConnectionTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/NameResolverIPv6ConnectionTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/OnlyInProcessServerTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/PlaintextSetupTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/SelfNameResolverConnectionTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/SelfSignedInProcessSetupTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/SelfSignedMutualSetupTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/SelfSignedServerSetupTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/setup/UnixSetupTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/shutdown/GrpcChannelLifecycleWithCallsTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/shutdown/GrpcServerLifecycleWithCallsTest.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/util/DynamicTestCollection.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/util/EnableOnIPv6.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/util/FutureAssertions.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/util/GrpcAssertions.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/util/LoggerTestUtil.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/util/RequireIPv6Condition.java create mode 100644 tests/src/test/java/net/devh/boot/grpc/test/util/TriConsumer.java create mode 100644 tests/src/test/proto/TestService.proto create mode 100644 tests/src/test/resources/certificates/client1.crt create mode 100644 tests/src/test/resources/certificates/client1.key create mode 100644 tests/src/test/resources/certificates/client2.crt create mode 100644 tests/src/test/resources/certificates/client2.key create mode 100644 tests/src/test/resources/certificates/generateCertificates.sh create mode 100644 tests/src/test/resources/certificates/server.crt create mode 100644 tests/src/test/resources/certificates/server.key create mode 100644 tests/src/test/resources/certificates/trusted-clients-collection create mode 100644 tests/src/test/resources/certificates/trusted-servers-collection create mode 100644 tests/src/test/resources/logback-test.xml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..3242b7928 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,38 @@ +# Handle line endings automatically for files detected as text +# and leave all files detected as binary untouched. +* text=auto + +# +# The above will handle all files NOT found below +# +# These files are text and should be normalized (Convert crlf => lf) +*.bash text eol=lf +*.df text +*.java text diff=java +*.js text +*.json text +*.properties text +# ensure that sh files can be run using git-bash or wsl even if pulled on Windows from the repo +*.sh text eol=lf +*.txt text +*.xml text +*.yml text +*.yaml text +*.md text + +# These files are binary and should be left untouched +# (binary is a macro for -text -diff) +*.gz binary +*.class binary +*.dll binary +*.ear binary +*.gif binary +*.ico binary +*.jar binary +*.jpg binary +*.jpeg binary +*.png binary +*.so binary +*.war binary +*.p12 binary +*.zip binary diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..b356211a8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,43 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**The context** + +What do you wish to achieve? + +**The bug** + +What's the problem? What's not working? What do you expect to happen. + +**Stacktrace and logs** + +Is there a stacktrace or a hint in the logs? (very important) +Screenshots work as well, but don't screenshot your logs. + +**Steps to Reproduce** + +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**The application's environment** + +Which versions do you use? +* Spring (boot): +* grpc-java: +* grpc-spring-boot-starter: +* java: version + architecture (64bit?) +* Other relevant libraries... + +**Additional context** + +* Did it ever work before? +* Do you have a demo? diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..a791671e5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,24 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**The problem** + +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**The solution** + +A clear and concise description of what you want to happen. + +**Alternatives considered** + +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** + +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 000000000..e1331b7f3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,35 @@ +--- +name: Question +about: Ask a question about this library and its usage +title: '' +labels: question +assignees: '' + +--- + +**The context** + +What do you wish to achieve? + +**The question** + +What's the problem? What's not working? What's missing and why do you need it? + +**Stacktraces and logs** + +Do you have any relevant stacktraces or logs of your attempts? + +**The application's environment** + +Which versions do you use? +* Spring (boot): +* grpc-java: +* grpc-spring-boot-starter: +* java: version + architecture (64bit?) +* Other relevant libraries... + +**Additional information** + +* Did it ever work before? +* How can we reproduce it? +* Do you have a demo? diff --git a/.github/workflows/build-master.yml b/.github/workflows/build-master.yml new file mode 100644 index 000000000..417152d17 --- /dev/null +++ b/.github/workflows/build-master.yml @@ -0,0 +1,44 @@ +name: Build master branch + +on: + push: + branches: + - master + +jobs: + build: + name: Build on java ${{ matrix.java }} + runs-on: ubuntu-latest + strategy: + matrix: + java: ['8', '11'] + steps: + - uses: actions/checkout@v2 + + - name: Set up java ${{ matrix.java }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + + - name: Cache Gradle packages + uses: actions/cache@v2 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Decrypt file + run: openssl aes-256-cbc -K ${{ secrets.ENCRYPTED_KEY }} -iv ${{ secrets.ENCRYPTED_IV }} -in private.key.enc -out ./private.key -d && gpg --batch --import ./private.key || echo + + - name: Build with Gradle + run: ./gradlew --scan --stacktrace --warning-mode=all build + + - name: Deploy with Gradle + run: ./gradlew --scan publish -x check -Psigning.gnupg.executable=gpg -Psigning.gnupg.keyName=${{ secrets.GPG_NAME }} -Psigning.gnupg.passphrase=${{ secrets.GPG_PASSWORD }} + if: ${{ matrix.java == '8' }} + env: + OSSRH_USER: ${{ secrets.OSSRH_USER }} + OSSRH_PASS: ${{ secrets.OSSRH_PASS }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml new file mode 100644 index 000000000..f2ac04a83 --- /dev/null +++ b/.github/workflows/pull-request.yml @@ -0,0 +1,35 @@ +name: Pull Request CI + +on: ['pull_request'] + +jobs: + build: + name: Build on java ${{ matrix.java }} + runs-on: ubuntu-latest + strategy: + matrix: + java: ['8', '11'] + steps: + - uses: actions/checkout@v2 + + - name: Set up java ${{ matrix.java }} + uses: actions/setup-java@v1 + with: + java-version: ${{ matrix.java }} + + - name: Cache Gradle packages + uses: actions/cache@v2 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build with Gradle + run: ./gradlew --scan --stacktrace --warning-mode=all build + + # Avoid publish errors when upgrading gradle version and dependencyManager plugin + - name: Try publishToMavenLocal + run: ./gradlew publishToMavenLocal diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..a53f29036 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +build +**/generated +classes +.gradle + +.DS_Store +target/ +!.mvn/wrapper/maven-wrapper.jar + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +**/out + +### Eclipse ### +target/ +bin/ +.classpath +.project +.settings/ +.factorypath +.checkstyle diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..daf29b543 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,26 @@ +# How to contribute + +We definitely welcome your patches and contributions to gRPC-Spring-Boot-Starter! + +If you are new to github, please start by reading [Pull Request howto](https://help.github.com/articles/about-pull-requests/) + +## Code Formatting + +Code formatting is enforced using the [Spotless](https://github.com/diffplug/spotless) Gradle plugin. +You can use `gradle spotlessJavaApply` (java only) or `gradle spotlessApply` (all files) +to format new code. Please run this task before submitting your pull request. + +### Eclipse + +For the eclipse IDE we use the following formatter files: + +* [extra/eclipse-formatter.xml](extra/eclipse/eclipse-formatter.xml) +* [extra/eclipse.importorder](extra/eclipse/eclipse.importorder) + +These will help you maintaing the files order, if you run the formatter from eclipse. +There are slight differences to the `spotless` plugin so please run it before submitting your PR anyway. + +### IntelliJ IDEA + +For IntelliJ IDEA there's a [Eclipse Code Formatter plugin](https://plugins.jetbrains.com/plugin/6546) you can use in +conjunction with the Eclipse setting files. diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..9f6739faa --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +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. diff --git a/README-zh-CN.md b/README-zh-CN.md new file mode 100644 index 000000000..e56179a36 --- /dev/null +++ b/README-zh-CN.md @@ -0,0 +1,227 @@ +# gRPC Spring Boot Starter + +[![Build Status](https://travis-ci.org/yidongnan/grpc-spring-boot-starter.svg?branch=master)](https://travis-ci.org/yidongnan/grpc-spring-boot-starter) +[![Maven Central with version prefix filter](https://img.shields.io/maven-central/v/net.devh/grpc-spring-boot-starter.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22net.devh%22%20grpc) +[![MIT License](https://img.shields.io/github/license/mashape/apistatus.svg)](LICENSE) +[![Crowdin](https://badges.crowdin.net/grpc-spring-boot-starter/localized.svg)](https://crowdin.com/project/grpc-spring-boot-starter) + +[![Client-Javadoc](https://www.javadoc.io/badge/net.devh/grpc-client-spring-boot-autoconfigure.svg?label=Client-Javadoc)](https://www.javadoc.io/doc/net.devh/grpc-client-spring-boot-autoconfigure) +[![Server-Javadoc](https://www.javadoc.io/badge/net.devh/grpc-server-spring-boot-autoconfigure.svg?label=Server-Javadoc)](https://www.javadoc.io/doc/net.devh/grpc-server-spring-boot-autoconfigure) +[![Common-Javadoc](https://www.javadoc.io/badge/net.devh/grpc-common-spring-boot.svg?label=Common-Javadoc)](https://www.javadoc.io/doc/net.devh/grpc-common-spring-boot) + +README: [English](README.md) | [中文](README-zh-CN.md) + +**文档:** [English](https://yidongnan.github.io/grpc-spring-boot-starter/en/) | [中文](https://yidongnan.github.io/grpc-spring-boot-starter/zh-CN/) + +QQ交流群:294712648 + +## 特性 + +* 在 spring boot 应用中,通过 `@GrpcService` 自动配置并运行一个嵌入式的 gRPC 服务。 + +* 使用 `@GrpcClient` 自动创建和管理您的 gRPC Channels 和 stubs + +* 支持[Spring Cloud](https://spring.io/projects/spring-cloud) (向 [Consul](https://github.com/spring-cloud/spring-cloud-consul) 或 [Eureka](https://github.com/spring-cloud/spring-cloud-netflix) 或 [Nacos](https://github.com/spring-cloud-incubator/spring-cloud-alibaba) 注册服务并获取 gRPC 服务端信息) + +* 支持[Spring Sleuth](https://github.com/spring-cloud/spring-cloud-sleuth)作为分布式链路跟踪解决方案(如果[brave-instrument-grpc](https://mvnrepository.com/artifact/io.zipkin.brave/brave-instrumentation-grpc)存在) + +* 支持全局和自定义的 gRPC 服务端/客户端拦截器 + +* 支持 [Spring-Security](https://github.com/spring-projects/spring-security) + +* 支持metric (基于[micrometer](https://micrometer.io/)/[actuator](https://github.com/spring-projects/spring-boot/tree/master/spring-boot-project/spring-boot-actuator) ) + +* 也适用于 (non-shaded) grpc-netty + +## 版本 + +2.x.x.RELEASE 支持 Spring Boot 2.1.x/2.2.x 和 Spring Cloud Greenwich / Hoxton。 + +最新版本: `2.12.0.RELEASE` + +( `2.4.0.RELEASE` 用于 Spring Boot 2.0.x & Spring Cloud Finchy). + +1.x.x.RELEASE 支持 Spring Boot 1 & Spring Cloud Edgware, Dalston, Camden. + +最新版本: `1.4.2.RELEASE` + +**注意:** 该项目也可以在没有 Spring-Boot 的情况下使用,但是您需要手动配置一些 bean。 + +## 用法 + +### gRPC 服务端 + 客户端 + +使用一下命令添加 Maven 依赖项: + +````xml + + net.devh + grpc-spring-boot-starter + 2.12.0.RELEASE + +```` + +Gradle: + +````gradle +dependencies { + compile 'net.devh:grpc-spring-boot-starter:2.12.0.RELEASE' +} +```` + +### gRPC 服务端 + +使用一下命令添加 Maven 依赖项: + +````xml + + net.devh + grpc-server-spring-boot-starter + 2.12.0.RELEASE + +```` + +Gradle: + +````gradle +dependencies { + compile 'net.devh:grpc-server-spring-boot-starter:2.12.0.RELEASE' +} +```` + +在服务端接口实现类上添加 `@GrpcService` 注解。 + +````java +@GrpcService +public class GrpcServerService extends GreeterGrpc.GreeterImplBase { + + @Override + public void sayHello(HelloRequest req, StreamObserver responseObserver) { + HelloReply reply = HelloReply.newBuilder().setMessage("Hello ==> " + req.getName()).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + +} +```` + +默认情况下,Grpc 服务器将监听端口 `9090`。 端口的配置和其他的 [设置](grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/config/GrpcServerProperties.java) 可以通过 Spring 的属性机制进行更改。 服务端的配置使用 `grpc.server.` 前缀。 + +详情请参阅我们的[文档](https://yidongnan.github.io/grpc-spring-boot-starter/)。 + +### gRPC 客户端 + +使用一下命令添加 Maven 依赖项: + +````xml + + net.devh + grpc-client-spring-boot-starter + 2.12.0.RELEASE + +```` + +Gradle: + +````gradle +dependencies { + compile 'net.devh:grpc-client-spring-boot-starter:2.12.0.RELEASE' +} +```` +在 grpc 客户端的的 stub 字段上添加 `@GrpcClient(serverName)` 注解。 + +* 请不要将 @GrpcClient 与 `@Autowireed` 或 `@Inject` 一起使用。 + + ````java + @GrpcClient("gRPC server name") + private GreeterGrpc.GreeterBlockingStub greeterStub; + ```` + +**注意:** 你可以将相同的 grpc 服务端名称用于多个 channel, 也可以使用不同的 stub (甚至使用不同的 stub 拦截器) + +然后您可以向您的服务器发送查询,就像这样: + +````java +HelloReply response = stub.sayHello(HelloRequest.newBuilder().setName(name).build()); +```` + +可以单独配置每个客户端的目标地址。 但在某些情况下,您可以仅依靠默认配置。 您可以通过 `NameResolver.Factory` Bean 类自定义默认的 url 映射。 如果您没有配置那个Bean,那么默认的 uri 将使用默认方案和名称(如:`dns:`): + +这些配置和其他的 [设置](grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java) 可以通过 Spring 的属性机制进行更改。 客户端使用`grpc.client.(serverName)。` 前缀。 + +详情请参阅我们的[文档](https://yidongnan.github.io/grpc-spring-boot-starter/)。 + +## 使用 (non-shaded) grpc-netty 运行 + +这个库支持`grpc-netty`和`grpc-nety-shaded`。 后一种可能会防止与不兼容的 gRPC 版本冲突或不同 netty 版本之间的冲突。 + +**注意:** 如果在classpath 中存在 shaded netty, 则 shaded netty 将使用有线与 non-shaded grpc-netty。 + +您可以在 Maven 中这样使用。 + +````xml + + io.grpc + grpc-netty + ${grpcVersion} + + + + + net.devh + grpc-spring-boot-starter + ... + + + io.grpc + grpc-netty-shaded + + + + + + net.devh + grpc-server-spring-boot-starter + ... + + + io.grpc + grpc-netty-shaded + + + + + + net.devh + grpc-client-spring-boot-starter + ... + + + io.grpc + grpc-netty-shaded + + + +```` + +Gradle: + +````groovy +compile "io.grpc:grpc-netty:${grpcVersion}" + +compile 'net.devh:grpc-spring-boot-starter:...' exclude group: 'io.grpc', module: 'grpc-netty-shaded' // For both +compile 'net.devh:grpc-client-spring-boot-starter:...' exclude group: 'io.grpc', module: 'grpc-netty-shaded' // For the client (only) +compile 'net.devh:grpc-server-spring-boot-starter:...' exclude group: 'io.grpc', module: 'grpc-netty-shaded' // For the server (only) +```` + +## 示例项目 + +在 [这里](examples)可以阅读更多关于我们的示例项目。 + +## 排除故障 + +请参阅我们的[文档](https://yidongnan.github.io/grpc-spring-boot-starter/en/trouble-shooting)寻求帮助。 + +## 参与贡献 + +我们始终欢迎您对项目作出任何贡献。 详见[CONTRIBUTING.md](CONTRIBUTING.md)。 diff --git a/README.md b/README.md new file mode 100644 index 000000000..7b4a5c329 --- /dev/null +++ b/README.md @@ -0,0 +1,252 @@ +# gRPC Spring Boot Starter + +[![Build master branch](https://github.com/yidongnan/grpc-spring-boot-starter/workflows/Build%20master%20branch/badge.svg)](https://github.com/yidongnan/grpc-spring-boot-starter/actions) +[![Maven Central with version prefix filter](https://img.shields.io/maven-central/v/net.devh/grpc-spring-boot-starter.svg)](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22net.devh%22%20grpc) +[![MIT License](https://img.shields.io/github/license/mashape/apistatus.svg)](LICENSE) +[![Crowdin](https://badges.crowdin.net/grpc-spring-boot-starter/localized.svg)](https://crowdin.com/project/grpc-spring-boot-starter) + +[![Client-Javadoc](https://www.javadoc.io/badge/net.devh/grpc-client-spring-boot-autoconfigure.svg?label=Client-Javadoc)](https://www.javadoc.io/doc/net.devh/grpc-client-spring-boot-autoconfigure) +[![Server-Javadoc](https://www.javadoc.io/badge/net.devh/grpc-server-spring-boot-autoconfigure.svg?label=Server-Javadoc)](https://www.javadoc.io/doc/net.devh/grpc-server-spring-boot-autoconfigure) +[![Common-Javadoc](https://www.javadoc.io/badge/net.devh/grpc-common-spring-boot.svg?label=Common-Javadoc)](https://www.javadoc.io/doc/net.devh/grpc-common-spring-boot) + +README: [English](README.md) | [中文](README-zh-CN.md) + +**Documentation:** [English](https://yidongnan.github.io/grpc-spring-boot-starter/en/) | [中文](https://yidongnan.github.io/grpc-spring-boot-starter/zh-CN/) + +## Features + +* Automatically configures and runs the gRPC server with your `@GrpcService` implementations + +* Automatically creates and manages your grpc channels and stubs with `@GrpcClient` + +* Supports other grpc-java flavors (e.g. + [Reactive gRPC (RxJava)](https://github.com/salesforce/reactive-grpc/tree/master/rx-java), + [grpc-kotlin](https://github.com/grpc/grpc-kotlin), ...) + * Server-side: Should work for all grpc-java flavors (`io.grpc.BindableService` based) + * Client-side: Requires custom `StubFactory`s\ + Currently build-in support: + * grpc-java + * (Please report missing ones, so we can add support for them) + +* Supports [Spring-Security](https://github.com/spring-projects/spring-security) + +* Supports [Spring Cloud](https://spring.io/projects/spring-cloud) + * Server-side: Adds grpc-port information to the service registration details\ + Currently natively supported: + * [Consul](https://github.com/spring-cloud/spring-cloud-consul) + * [Eureka](https://github.com/spring-cloud/spring-cloud-netflix) + * [Nacos](https://github.com/spring-cloud-incubator/spring-cloud-alibaba) + * (Please report missing ones, so we can add support for them) + * Client-side: Reads the service's target addresses from spring's `DiscoveryClient` (all flavors) + +* Supports [Spring Sleuth](https://github.com/spring-cloud/spring-cloud-sleuth) as distributed tracing solution\ + (If [brave-instrumentation-grpc](https://mvnrepository.com/artifact/io.zipkin.brave/brave-instrumentation-grpc) is present) + +* Supports global and custom gRPC server/client interceptors + +* Automatic metric support ([micrometer](https://micrometer.io/)/[actuator](https://github.com/spring-projects/spring-boot/tree/master/spring-boot-project/spring-boot-actuator) based) + +* Also works with (non-shaded) grpc-netty + +## Versions + +2.x.x.RELEASE supports Spring Boot 2.1.x/2.2.x & Spring Cloud Greenwich/Hoxton. + +The latest version: ``2.12.0.RELEASE`` + +(Use `2.4.0.RELEASE` for Spring Boot 2.0.x & Spring Cloud Finchley). + +1.x.x.RELEASE support Spring Boot 1 & Spring Cloud Edgware, Dalston, Camden. + +The latest version: ``1.4.2.RELEASE`` + +**Note:** This project can also be used without Spring-Boot, however that requires some manual bean configuration. + +## Usage + +### gRPC Server + Client + +To add a dependency using Maven, use the following: + +````xml + + net.devh + grpc-spring-boot-starter + 2.12.0.RELEASE + +```` + +To add a dependency using Gradle: + +````gradle +dependencies { + implementation 'net.devh:grpc-spring-boot-starter:2.12.0.RELEASE' +} +```` + +### gRPC Server + +To add a dependency using Maven, use the following: + +````xml + + net.devh + grpc-server-spring-boot-starter + 2.12.0.RELEASE + +```` + +To add a dependency using Gradle: + +````gradle +dependencies { + implementation 'net.devh:grpc-server-spring-boot-starter:2.12.0.RELEASE' +} +```` + +Annotate your server interface implementation(s) with ``@GrpcService`` + +````java +@GrpcService +public class GrpcServerService extends GreeterGrpc.GreeterImplBase { + + @Override + public void sayHello(HelloRequest req, StreamObserver responseObserver) { + HelloReply reply = HelloReply.newBuilder().setMessage("Hello ==> " + req.getName()).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + +} +```` + +By default, the grpc server will listen to port `9090`. These and other +[settings](grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/config/GrpcServerProperties.java) +can be changed via Spring's property mechanism. The server uses the `grpc.server.` prefix. + +Refer to our [documentation](https://yidongnan.github.io/grpc-spring-boot-starter/) for more details. + +### gRPC Client + +To add a dependency using Maven, use the following: + +````xml + + net.devh + grpc-client-spring-boot-starter + 2.12.0.RELEASE + +```` + +To add a dependency using Gradle: + +````gradle +dependencies { + compile 'net.devh:grpc-client-spring-boot-starter:2.12.0.RELEASE' +} +```` + +Annotate a field of your grpc client stub with `@GrpcClient(serverName)` + +* Do not use in conjunction with `@Autowired` or `@Inject` + + ````java + @GrpcClient("gRPC server name") + private GreeterGrpc.GreeterBlockingStub greeterStub; + ```` + +**Note:** You can use the same grpc server name for multiple channels and also different stubs (even with different +interceptors). + +Then you can send queries to your server just like this: + +````java +HelloReply response = stub.sayHello(HelloRequest.newBuilder().setName(name).build()); +```` + +It is possible to configure the target address for each client individually. +However in some cases, you can just rely on the default configuration. +You can customize the default url mapping via `NameResolver.Factory` beans. If you don't configure that bean, +then the default uri will be guessed using the default scheme and the name (e.g.: `dns:/`): + +These and other +[settings](grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java) +can be changed via Spring's property mechanism. The clients use the `grpc.client.(serverName).` prefix. + +Refer to our [documentation](https://yidongnan.github.io/grpc-spring-boot-starter/) for more details. + +## Running with (non-shaded) grpc-netty + +This library supports both `grpc-netty` and `grpc-netty-shaded`. +The later one might prevent conflicts with incompatible grpc-versions or conflicts between libraries that require different versions of netty. + +**Note:** If the shaded netty is present on the classpath, then this library will always favor it over the non-shaded grpc-netty one. + +You can use it with Maven like this: + +````xml + + io.grpc + grpc-netty + ${grpcVersion} + + + + + net.devh + grpc-spring-boot-starter + ... + + + io.grpc + grpc-netty-shaded + + + + + + net.devh + grpc-server-spring-boot-starter + ... + + + io.grpc + grpc-netty-shaded + + + + + + net.devh + grpc-client-spring-boot-starter + ... + + + io.grpc + grpc-netty-shaded + + + +```` + +and like this when using Gradle: + +````groovy +implementation "io.grpc:grpc-netty:${grpcVersion}" + +implementation 'net.devh:grpc-spring-boot-starter:...' exclude group: 'io.grpc', module: 'grpc-netty-shaded' // For both +implementation 'net.devh:grpc-client-spring-boot-starter:...' exclude group: 'io.grpc', module: 'grpc-netty-shaded' // For the client (only) +implementation 'net.devh:grpc-server-spring-boot-starter:...' exclude group: 'io.grpc', module: 'grpc-netty-shaded' // For the server (only) +```` + +## Example-Projects + +Read more about our example projects [here](examples). + +## Troubleshooting + +Refer to our [documentation](https://yidongnan.github.io/grpc-spring-boot-starter/en/trouble-shooting) for help. + +## Contributing + +Contributions are always welcomed! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines. diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..3be518678 --- /dev/null +++ b/build.gradle @@ -0,0 +1,280 @@ +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter +import io.franzbecker.gradle.lombok.task.DelombokTask + +buildscript { + repositories { + maven { + url 'https://plugins.gradle.org/m2/' +// allowInsecureProtocol = true +// url 'http://maven.aliyun.com/nexus/content/groups/public/' + } + } + ext { + projectVersion = '2.13.0-SNAPSHOT' + + // https://github.com/grpc/grpc-java/releases + grpcVersion = '1.37.0' + + // https://github.com/google/guava/releases + guavaVersion = '30.1.1-jre' + // https://github.com/protocolbuffers/protobuf/releases + protobufVersion = '3.15.8' + protobufGradlePluginVersion = '0.8.12' + + // https://github.com/spring-projects/spring-boot/releases + springBootVersion = '2.4.5' + // https://github.com/spring-cloud/spring-cloud-release/releases + springCloudVersion = '2020.0.2' + // https://github.com/alibaba/spring-cloud-alibaba/releases + springCloudAlibabaNacosVersion = '2021.1' + // https://github.com/spring-projects/spring-security-oauth/releases + springSecurityOAuthVersion = '2.5.1.RELEASE' + + lombokPluginVersion = '4.0.0' + versioningPluginVersion = '2.14.0' + versionsPluginVersion = '0.29.0' + } +} + +plugins { + id 'java' + id 'java-library' + id 'org.springframework.boot' version "${springBootVersion}" apply false + id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'net.nemerosa.versioning' version '2.14.0' + id 'com.google.protobuf' version '0.8.15' + id 'io.franzbecker.gradle-lombok' version '4.0.0' apply false + id 'com.github.ben-manes.versions' version '0.36.0' // gradle dependencyUpdates + id 'com.diffplug.spotless' version '5.11.0' +} + +// If you attempt to build without the `--scan` parameter in `gradle 6.0+` it will cause a build error that it can't find +// a buildScan property to change. This avoids that problem. +if (hasProperty('buildScan')) { + buildScan { + termsOfServiceUrl = 'https://gradle.com/terms-of-service' + termsOfServiceAgree = 'yes' + } +} + +// you may use IntelliJ's project configuration to make it use the gradle version defined in the gradle script's wrapper section +wrapper { + // Update using: + // ./gradlew wrapper --gradle-version=6.5 --distribution-type=bin + gradleVersion = '7.0' +} + +def buildTimeAndDate = OffsetDateTime.now() + +ext { + buildDate = DateTimeFormatter.ISO_LOCAL_DATE.format(buildTimeAndDate) + buildTime = DateTimeFormatter.ofPattern('HH:mm:ss.SSSZ').format(buildTimeAndDate) + buildRevision = versioning.info.commit +} + +allprojects { + apply plugin: 'java' + apply plugin: 'idea' + apply plugin: 'eclipse' + apply plugin: 'io.spring.dependency-management' + apply plugin: 'com.diffplug.spotless' + apply plugin: 'io.franzbecker.gradle-lombok' + + java { + toolchain { + languageVersion = JavaLanguageVersion.of(8) + } + } + + compileJava { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + options.encoding = 'UTF-8' + } + + compileJava.options*.compilerArgs = [ + '-Xlint:all', '-Xlint:-processing' + ] + + eclipse { + classpath { + downloadJavadoc = true + downloadSources = true + } + } + + spotless { + java { + licenseHeaderFile rootProject.file('extra/spotless/mit-license.java') + removeUnusedImports() + importOrderFile rootProject.file('extra/eclipse/eclipse.importorder') + eclipse().configFile rootProject.file('extra/eclipse/eclipse-formatter.xml') + } + format('misc') { + target('**/*.gradle', '**/*.md', '**/*.yml') + targetExclude('**/build/**/*.*') + trimTrailingWhitespace() + endWithNewline() + } + } + + normalization { + runtimeClasspath { + metaInf{ + ignoreAttribute('Build-Time') + } + } + } + + // Copy LICENSE + tasks.withType(Jar) { + from(project.rootDir) { + include 'LICENSE' + into 'META-INF' + } + } + + // Generate MANIFEST.MF + jar { + manifest { + attributes( + 'Created-By': "${System.properties['java.version']} (${System.properties['java.vendor']} ${System.properties['java.vm.version']})".toString(), + 'Built-By': 'travis', + 'Build-Date': buildDate, + 'Build-Time': buildTime, + 'Built-OS': "${System.properties['os.name']}", + 'Build-Revision': buildRevision, + 'Specification-Title': project.name, + 'Specification-Version': projectVersion, + 'Specification-Vendor': 'Michael Zhang', + 'Implementation-Title': project.name, + 'Implementation-Version': projectVersion, + 'Implementation-Vendor': 'Michael Zhang' + ) + } + } + + repositories { + mavenCentral() + } + + buildscript { + repositories { + maven { url 'https://plugins.gradle.org/m2/' } + } + } +} + +Project commonProject = project(':grpc-common-spring-boot') + +String javaAPIdoc +if (JavaVersion.current().isJava9Compatible()) { + javaAPIdoc = 'https://docs.oracle.com/en/java/javase/11/docs/api' +} else { + javaAPIdoc = 'https://docs.oracle.com/javase/8/docs/api/' +} + +allprojects { project -> + buildscript { + dependencyManagement { + imports { + mavenBom "org.springframework.boot:spring-boot-starter-parent:${springBootVersion}" + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + mavenBom "com.alibaba.cloud:spring-cloud-alibaba-dependencies:${springCloudAlibabaNacosVersion}" + mavenBom "com.google.protobuf:protobuf-bom:${protobufVersion}" + mavenBom "com.google.guava:guava-bom:${guavaVersion}" + mavenBom "io.grpc:grpc-bom:${grpcVersion}" + mavenBom "org.junit:junit-bom:5.7.0" + } + } + + ext { + micrometerVersion = dependencyManagement.importedProperties['micrometer.version'] + // not explicitly needed in subprojects, as the BOM for Sprint Boot sets this version + springFrameworkVersion = dependencyManagement.importedProperties['spring-framework.version'] + springSecurityVersion = dependencyManagement.importedProperties['spring-security.version'] + springCloudCommonsVersion = dependencyManagement.importedProperties['spring-cloud-commons.version'] + } + } + + test { + useJUnitPlatform() + testLogging { + // failFast = true + // showStandardStreams = true + exceptionFormat = 'full' + showCauses = true + showExceptions = true + showStackTraces = true + // prints out individual test progress by hooking into junit engine events + // it.events('passed', 'skipped', 'failed', 'standard_out') + it.events('passed', 'skipped', 'failed') + + it.debug { dbg -> + // prints out individual test progress when run under the debugger + // dbg.events('started', 'failed', 'passed', 'skipped', 'standard_error', 'standard_out') + dbg.events('started', 'failed', 'passed', 'skipped') + } + } + } + + if (project.name == 'grpc-common-spring-boot' || project.name == 'grpc-client-spring-boot-autoconfigure' || project.name == 'grpc-server-spring-boot-autoconfigure') { + // Properly generate javadocs for the important projects + + task delombok(type: DelombokTask, dependsOn: compileJava) { + ext.outputDir = file("$buildDir/delombok") + outputs.dir(outputDir) + sourceSets.main.java.srcDirs.each { + inputs.dir(it) + args(it, '-d', outputDir) + } + } + + java { + registerFeature('optionalSupport') { + usingSourceSet(sourceSets.main) + } + } + + // Javadoc Task + javadoc { + dependsOn delombok + if (project.name != 'grpc-common-spring-boot') { + dependsOn(":grpc-common-spring-boot:javadoc") + } + source = delombok.outputDir + failOnError = false + options.locale = 'en_US' + options.encoding = 'UTF-8' + options.jFlags('-Dhttp.agent=gradle-javadoc') // Required for javadoc.io + if (project.name != 'grpc-common-spring-boot') { + options.linksOffline('https://static.javadoc.io/net.devh/grpc-common-spring-boot/' + projectVersion, commonProject.buildDir.getPath() + '/docs/javadoc') + } + options.links = [ + javaAPIdoc, + 'https://grpc.io/grpc-java/javadoc/', + 'https://static.javadoc.io/io.micrometer/micrometer-core/' + micrometerVersion + '/', + 'https://docs.spring.io/spring-framework/docs/' + springFrameworkVersion + '/javadoc-api/', + 'https://docs.spring.io/spring-security/site/docs/' + springSecurityVersion + '/api/', + 'https://docs.spring.io/spring-boot/docs/' + springBootVersion + '/api/', + 'https://static.javadoc.io/org.springframework.cloud/spring-cloud-commons/' + springCloudCommonsVersion + '/', + // 'https://static.javadoc.io/io.zipkin.brave/brave/' + braveInstrumentationGrpc + '/', // Requires javadoc 11 + // 'https://static.javadoc.io/io.zipkin.brave/brave-instrumentation-grpc/' + braveInstrumentationGrpc + '/', // Requires javadoc 11 + 'https://google.github.io/guava/releases/29.0-android/api/docs/' + ] + } + } +} + +apply from: './deploy.gradle' + +group = 'net.devh' +version = projectVersion + +dependencies { + api project(':grpc-server-spring-boot-starter') + api project(':grpc-client-spring-boot-starter') + + testImplementation project(':tests') +} diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 000000000..131ad8f8b --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,9 @@ +files: + - source: /docs/en/client + translation: /docs/%locale%/client/%original_file_name% + - source: /README.md + translation: /README-%locale%.md + - source: /docs/en/server + translation: /docs/%locale%/server/%original_file_name% + - source: /docs/en/*.md + translation: /docs/%locale%/%original_file_name% diff --git a/deploy.gradle b/deploy.gradle new file mode 100644 index 000000000..d37bb8128 --- /dev/null +++ b/deploy.gradle @@ -0,0 +1,97 @@ +apply plugin: 'maven-publish' +apply plugin: 'signing' + +ext { + isReleaseVersion = !(projectVersion =~ /-SNAPSHOT$/) + isNeedSign = project.hasProperty('signing.gnupg.keyName') && isReleaseVersion +} + +task sourcesJar(type: Jar) { + from sourceSets.main.allJava + archiveClassifier.set('sources') +} + +task javadocJar(type: Jar) { + from javadoc + archiveClassifier.set('javadoc') +} + +publishing { + publications { + mavenJava(MavenPublication) { + + from components.java + artifact sourcesJar + artifact javadocJar + + pom { + name = 'gRPC Spring Boot Starter' + description = 'gRPC Spring Boot Starter' + url = 'https://github.com/yidongnan/grpc-spring-boot-starter' + licenses { + license { + name = 'MIT License' + url = 'http://www.opensource.org/licenses/mit-license.php' + distribution = 'repo' + } + } + developers { + developer { + id = 'yidongnan' + name = 'Michael Zhang' + email = 'yidongnan@gmail.com' + } + developer { + id = 'ST-DDT' + name = 'Daniel Theuke' + email = 'daniel.theuke@heuboe.de' + organization = 'Heusch/Boesefeldt GmbH' + organizationUrl = 'https://www.heuboe.de' + } + } + scm { + connection = 'scm:git:git://github.com/yidongnan/grpc-spring-boot-starter.git' + developerConnection = 'scm:git:ssh@github.com:yidongnan/grpc-spring-boot-starter.git' + url = 'https://github.com/yidongnan/grpc-spring-boot-starter' + } + } + + versionMapping { + usage('java-api') { + fromResolutionOf('runtimeClasspath') + } + usage('java-runtime') { + fromResolutionResult() + } + } + } + } + repositories { + maven { + credentials { + username System.getenv('OSSRH_USER') + password System.getenv('OSSRH_PASS') + } + if (project.ext.isReleaseVersion) { + url "https://oss.sonatype.org/service/local/staging/deploy/maven2" + } else { + url "https://oss.sonatype.org/content/repositories/snapshots" + } + } + } + + tasks.withType(Sign) { + onlyIf { project.ext.isNeedSign } + } + + signing { + useGpgCmd() + sign publishing.publications.mavenJava + } + + javadoc { + if(JavaVersion.current().isJava9Compatible()) { + options.addBooleanOption('html5', true) + } + } +} diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 000000000..277f1f2c5 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss new file mode 100644 index 000000000..98f43b8cd --- /dev/null +++ b/docs/assets/css/style.scss @@ -0,0 +1,20 @@ +--- +--- + +@import "{{ site.theme }}"; + +@media screen and (min-width: 64em) { + .page-header { + padding:3rem 3rem; + } +} +@media screen and (min-width: 42em) and (max-width: 64em) { + .page-header { + padding:2rem 3rem; + } +} +@media screen and (max-width: 42em) { + .page-header { + padding:2rem 1rem + } +} \ No newline at end of file diff --git a/docs/assets/images/client-project-setup.dot b/docs/assets/images/client-project-setup.dot new file mode 100644 index 000000000..80470a316 --- /dev/null +++ b/docs/assets/images/client-project-setup.dot @@ -0,0 +1,60 @@ +digraph serversetup { + + rankdir=LR; + + compond=true; + + subgraph cluster_interface { + + label="Interface-Project"; + + protofile [label="protobuf-file", shape=box, URL="https://developers.google.com/protocol-buffers/docs/proto3#simple", target="_blank"]; + + protofile2 [label="protobuf-file2", shape=box, color="gray50", fontcolor="gray50", fillcolor="white", URL="https://developers.google.com/protocol-buffers/docs/proto3#services", target="_blank"]; + protofileN [label="protobuf-fileN", shape=box, color="gray75", fontcolor="gray75", fillcolor="white", URL="https://developers.google.com/protocol-buffers/docs/javatutorial", target="_blank"]; + + { + rank=same; + protoc [label="protobuf-compiler", URL="https://mvnrepository.com/artifact/com.google.protobuf/protoc", target="_blank"]; + grpcc [label="protoc-gen-grpc-java", URL="https://mvnrepository.com/artifact/io.grpc/protoc-gen-grpc-java", target="_blank"]; + } + + servicemodel [label="service and model defintions", shape=box, URL="https://github.com/grpc/grpc-java/blob/master/README.md#generated-code", target="_blank"]; + + protofile -> protoc:w; + protofile2 -> protoc:w [color="gray50"]; + protofileN -> protoc:w [color="gray75"]; + + protoc -> grpcc; + grpcc -> protoc; + + protoc -> servicemodel; + + } + + subgraph cluster_server { + + label="Server-Project" + color="gray50"; + fillColor="white"; + fontcolor="gray50"; + + serviceimpl [label="Service implementations", color="gray50", fontcolor="gray50",fillcolor="white",width="3", URL="https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/GrpcService.java#L49", target="_blank"]; + + servicemodel -> serviceimpl [style=dashed, color="gray50", dir=back]; + + } + + subgraph cluster_clients { + + label="Client-Projects"; + + clientfield [label="Client/Stub usage", width="3", URL="https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClient.java#L69", target="_blank"]; + + servicemodel:se -> clientfield[style=dashed, dir=back]; + servicemodel:se -> clientfield[style=dashed, dir=back, weight=0]; + servicemodel:se -> clientfield[style=dashed, dir=back, weight=0]; + + } + +} \ No newline at end of file diff --git a/docs/assets/images/client-project-setup.svg b/docs/assets/images/client-project-setup.svg new file mode 100644 index 000000000..4b41f4c5f --- /dev/null +++ b/docs/assets/images/client-project-setup.svg @@ -0,0 +1,139 @@ + + + + + + +serversetup + +cluster_interface + +Interface-Project + +cluster_server + +Server-Project + +cluster_clients + +Client-Projects + + +protofile + + +protobuf-file + + + + +protoc + + +protobuf-compiler + + + + +protofile->protoc:w + + + + +protofile2 + + +protobuf-file2 + + + + +protofile2->protoc:w + + + + +protofileN + + +protobuf-fileN + + + + +protofileN->protoc:w + + + + +grpcc + + +protoc-gen-grpc-java + + + + +protoc->grpcc + + + + +servicemodel + + +service and model defintions + + + + +protoc->servicemodel + + + + +grpcc->protoc + + + + +serviceimpl + + +Service implementations + + + + +servicemodel->serviceimpl + + + + +clientfield + + +Client/Stub usage + + + + +servicemodel:se->clientfield + + + + +servicemodel:se->clientfield + + + + +servicemodel:se->clientfield + + + + + diff --git a/docs/assets/images/server-project-setup.dot b/docs/assets/images/server-project-setup.dot new file mode 100644 index 000000000..7d4a00802 --- /dev/null +++ b/docs/assets/images/server-project-setup.dot @@ -0,0 +1,60 @@ +digraph serversetup { + + rankdir=LR; + + compond=true; + + subgraph cluster_interface { + + label="Interface-Project"; + + protofile [label="protobuf-file", shape=box, URL="https://developers.google.com/protocol-buffers/docs/proto3#simple", target="_blank"]; + + protofile2 [label="protobuf-file2", shape=box, color="gray50", fontcolor="gray50", fillcolor="white", URL="https://developers.google.com/protocol-buffers/docs/proto3#services", target="_blank"]; + protofileN [label="protobuf-fileN", shape=box, color="gray75", fontcolor="gray75", fillcolor="white", URL="https://developers.google.com/protocol-buffers/docs/javatutorial", target="_blank"]; + + { + rank=same; + protoc [label="protobuf-compiler", URL="https://mvnrepository.com/artifact/com.google.protobuf/protoc", target="_blank"]; + grpcc [label="protoc-gen-grpc-java", URL="https://mvnrepository.com/artifact/io.grpc/protoc-gen-grpc-java", target="_blank"]; + } + + servicemodel [label="service and model defintions", shape=box, URL="https://github.com/grpc/grpc-java/blob/master/README.md#generated-code", target="_blank"]; + + protofile -> protoc:w; + protofile2 -> protoc:w [color="gray50"]; + protofileN -> protoc:w [color="gray75"]; + + protoc -> grpcc; + grpcc -> protoc; + + protoc -> servicemodel; + + } + + subgraph cluster_server { + + label="Server-Project" + + serviceimpl [label="Service implementations", width="3", URL="https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/GrpcService.java#L49", target="_blank"]; + + servicemodel -> serviceimpl [style=dashed, dir=back]; + + } + + subgraph cluster_clients { + + label="Client-Projects"; + color="gray50"; + fillColor="white"; + fontcolor="gray50"; + + clientfield [label="Client/Stub usage", width="3", color="gray50", fontcolor="gray50",fillcolor="white", URL="https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClient.java#L69", target="_blank"]; + + servicemodel:se -> clientfield[style=dashed, dir=back, color="gray50"]; + servicemodel:se -> clientfield[style=dashed, dir=back, color="gray50", weight=0]; + servicemodel:se -> clientfield[style=dashed, dir=back, color="gray50", weight=0]; + + } + +} \ No newline at end of file diff --git a/docs/assets/images/server-project-setup.svg b/docs/assets/images/server-project-setup.svg new file mode 100644 index 000000000..7a2f55290 --- /dev/null +++ b/docs/assets/images/server-project-setup.svg @@ -0,0 +1,139 @@ + + + + + + +g + +cluster_interface + +Interface-Project + +cluster_server + +Server-Project + +cluster_clients + +Client-Projects + + +protofile + + +protobuf-file + + + + +protoc + + +protobuf-compiler + + + + +protofile->protoc:w + + + + +protofile2 + + +protobuf-file2 + + + + +protofile2->protoc:w + + + + +protofileN + + +protobuf-fileN + + + + +protofileN->protoc:w + + + + +grpcc + + +protoc-gen-grpc-java + + + + +protoc->grpcc + + + + +servicemodel + + +service and model defintions + + + + +protoc->servicemodel + + + + +grpcc->protoc + + + + +serviceimpl + + +Service implementations + + + + +servicemodel->serviceimpl + + + + +clientfield + + +Client/Stub usage + + + + +servicemodel:se->clientfield + + + + +servicemodel:se->clientfield + + + + +servicemodel:se->clientfield + + + + + diff --git a/docs/assets/images/server-security.graphml b/docs/assets/images/server-security.graphml new file mode 100644 index 000000000..46e330f63 --- /dev/null +++ b/docs/assets/images/server-security.graphml @@ -0,0 +1,727 @@ + + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {"backgroundColor":"white","theme":{"name":"light","version":"1.0.0"},"version":"2.0.0","layout":"layout-hierarchic","config":{"noObf_useDrawingAsSketch":false,"noObf_selectedElementsIncrementally":false,"noObf_nodeToNodeDistance":30,"noObf_automaticEdgeGroupingEnabled":false,"noObf_considerNodeLabels":true,"noObf_edgeLabeling":1,"noObf_orientation":0,"noObf_edgeRouting":0}} + + + + + + Request + + + + + + + + + + + + + + + + + + + + grpcMethod + + + + + + + + + + + + + + + + + + + + + + + Authentication + + + + + + + + + + + + + + + + + + + + + + + + + authenticatingServerInterceptor + + + + + + + + + + + {"layerId":"authenticatingServerInterceptor"} + + + + + + + + + + + + + + + + + + + + + + authenticationManager + + + + + + + + + + + {"layerId":"authenticatingServerInterceptor"} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + grpcAuthenticationReader + + + + + + + + + + + {"layerId":"authenticatingServerInterceptor"} + + + + + + + + + + + + + + + + + + + + + authenticationProvider + + + + + + + + + + + + + + + + + + + + + + + + + + + {"layerId":"authenticatingServerInterceptor"} + + + + + + + + + + + + + + + Authorization + + + + + + + + + + + + + + + + + + + + + + + + + authorizationCheckingServerInterceptor + + + + + + + + + + + {"layerId":"authorizationCheckingServerInterceptor"} + + + + + + + + + + + + + + + + + + + + + + + + + accessDecisionManager + + + + + + + + + + + {"layerId":"authorizationCheckingServerInterceptor"} + + + + + + + + + + + + + + + + + + + + + + + + + springSecurityProxy + + + + + + + + + + + {"layerId":"authorizationCheckingServerInterceptor"} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + authenticate + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Authentication + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/assets/images/server-security.svg b/docs/assets/images/server-security.svg new file mode 100644 index 000000000..a0b6adfc5 --- /dev/null +++ b/docs/assets/images/server-security.svg @@ -0,0 +1 @@ +Authentication(Request)authenticate authorize authorizeManual configAuthenticationAnnotation configRequestgrpcMethodAuthenticationauthenticatingServerInterceptorauthenticationManagergrpcAuthenticationReaderauthenticationProviderAuthorizationauthorizationCheckingServerInterceptoraccessDecisionManagerspringSecurityProxy \ No newline at end of file diff --git a/docs/en/actuator.md b/docs/en/actuator.md new file mode 100644 index 000000000..fa45bfdb7 --- /dev/null +++ b/docs/en/actuator.md @@ -0,0 +1,155 @@ +# Spring Boot Actuator Support + +[<- Back to Index](index.md) + +This page focuses on the integration with +[Spring-Boot-Actuator](https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html). +This is an optional feature. Supported features: + +- Client + server metrics +- Server `InfoContributor` + +## Table of Contents + +- [Dependencies](#dependencies) +- [Metrics](#metrics) + - [Counter](#counter) + - [Timer](#timer) + - [Viewing the metrics](#viewing-the-metrics) + - [Metric configuration](#metric-configuration) +- [InfoContributor](#infocontributor) +- [Opt-Out](#opt-out) + +## Dependencies + +The metric collection and other actuator features are optional, they will be enabled automatically if a `MeterRegistry` +is in the application context. + +You can achieve this simply by adding the following dependency to Maven: + +````xml + + org.springframework.boot + spring-boot-starter-actuator + +```` + +or to Gradle: + +````groovy +compile("org.springframework.boot:spring-boot-starter-actuator") +```` + +> **Note:** In most cases you will also need the `spring-boot-web` dependency in order to actually view the metrics. +> Please note that spring-boot-web runs on a different port than the grpc server (usually `8080`). If you don't want to +> add a web-server you can still access the metrics via JMX (if enabled). + +## Metrics + +Once the dependencies are added grpc-spring-boot-starter will automatically configure `ClientInterceptor`s/`ServerInterceptor`s that will gather the metrics. + +### Counter + +- `grpc.client.requests.sent`: The total number of requests sent. +- `grpc.client.responses.received`: The total number of responses received. +- `grpc.server.requests.received`: The total number of requests received. +- `grpc.server.responses.sent`: The total number of responses sent. + +**Tags:** + +- `service`: The requested grpc service name (using protobuf name) +- `method`: The requested grpc method name (using protobuf name) +- `methodType`: The type of the requested grpc method. + +### Timer + +- `grpc.client.processing.duration`: The total time taken for the client to complete the call, including network delay. +- `grpc.server.processing.duration`: The total time taken for the server to complete the call. + +**Tags:** + +- `service`: The requested grpc service name (using protobuf name) +- `method`: The requested grpc method name (using protobuf name) +- `methodType`: The type of the requested grpc method. +- `statusCode`: Response `Status.Code` + +### Viewing the metrics + +You can view the grpc metrics along with your other metrics at `/actuator/metrics` (requires a web-server) or via JMX. + +> **Note:** You might have to enable your metrics endpoint first. +> +> ````properties +> management.endpoints.web.exposure.include=metrics +> #management.endpoints.jmx.exposure.include=metrics +> management.endpoint.metrics.enabled=true +> ```` + +Read the official documentation for more information about +[Spring Boot Actuator](https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html). + +### Metric configuration + +By default, the client will only create metrics for calls that have been made. However, the server will try to find all +registered services and initialize metrics for them. + +You can customize the behavior by overwriting the beans. The following demonstrates this using the +`MetricCollectingClientInterceptor`: + +````java +@Bean +MetricCollectingClientInterceptor metricCollectingClientInterceptor(MeterRegistry registry) { + MetricCollectingClientInterceptor collector = new MetricCollectingClientInterceptor(registry, + counter -> counter.tag("app", "myApp"), // Customize the Counters + timer -> timer.tag("app", "myApp"), // Customize the Timers + Code.OK, Code.INVALID_ARGUMENT, Code.UNAUTHENTICATED); // Eagerly initialized status codes + // Pre-generate metrics for some services (to avoid missing metrics after restarts) + collector.preregisterService(MyServiceGrpc.getServiceDescriptor()); + return collector; +} +```` + +## InfoContributor + +*(Server only)* + +The server part automatically configures an +[`InfoContributor`](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/actuate/info/InfoContributor.html) +that publishes the following information: + +- `grpc.server`: + - `port`: The grpc server port + - `services`: A list of grpc-services + - With their methods + +You can view the grpc info along with your other info at `/actuator/info` (requires a web-server) or via JMX. + +> **Note:** You might have to enable your info endpoint first. +> +> ````properties +> management.endpoints.web.exposure.include=info +> #management.endpoints.jmx.exposure.include=info +> management.endpoint.info.enabled=true +> ```` + +You can turn of the service listing (for both actuator and grpc) using `grpc.server.reflectionServiceEnabled=false`. + +## Opt-Out + +You can opt out from the actuator autoconfiguration using the following annotation: + +````java +@EnableAutoConfiguration(exclude = {GrpcClientMetricAutoConfiguration.class, GrpcServerMetricAutoConfiguration.class}) +```` + +or using properties: + +````properties +spring.autoconfigure.exclude=\ +net.devh.boot.grpc.client.autoconfigure.GrpcClientMetricAutoConfiguration,\ +net.devh.boot.grpc.server.autoconfigure.GrpcServerMetricAutoConfiguration +```` + +---------- + +[<- Back to Index](index.md) diff --git a/docs/en/benchmarking.md b/docs/en/benchmarking.md new file mode 100644 index 000000000..a381305d0 --- /dev/null +++ b/docs/en/benchmarking.md @@ -0,0 +1,23 @@ +# Benchmarking + +[<- Back to Index](index.md) + +This library does not add any performance overhead to gRPC-java. + +Please refer to the official gRPC benchmarks: + +- [grpc.io: Benchmarking](https://grpc.io/docs/guides/benchmarking/) +- [grpc-java: Running Benchmarks](https://github.com/grpc/grpc-java/tree/master/benchmarks#grpc-benchmarks) + +gRPC offers various benefits over plain HTTP, but it's hard to put it in actual numbers. +Heavily optimized web-servers perform as good as normal gRPC-servers. + +Here are the main benefits/differences from grpc over plain HTTP: + +- Binary data format (a lot faster, but not human readable) +- Protobuf defined data scheme, that can be used to generate data classes and clients for many languages +- HTTP/2 connections and connection pooling + +---------- + +[<- Back to Index](index.md) diff --git a/docs/en/brave.md b/docs/en/brave.md new file mode 100644 index 000000000..d58080051 --- /dev/null +++ b/docs/en/brave.md @@ -0,0 +1,82 @@ +# Brave / Sleuth Support + +[<- Back to Index](index.md) + +This page focuses on the integration with [brave](https://github.com/openzipkin/brave) / +[Spring Cloud Sleuth](https://spring.io/projects/spring-cloud-sleuth). This is an optional feature. + +## Table of Contents + +- [Dependencies](#dependencies) + - [Brave](#brave) + - [Spring Cloud Sleuth](#spring-cloud-sleuth) +- [Opt-Out](#opt-out) +- [Additional Notes](#additional-notes) + +## Dependencies + +grpc-spring-boot-starter provides automatic support for `Brave Instrumentation: GRPC`. +However, there are two requirements: + +1. You need the dependencies for Brave on the classpath. +2. You need a `Tracing` bean in your application context. + *If you have Spring Cloud Sleuth in your classpath, it will automatically configure this bean for you.* + +### Brave + +You can add the required dependencies for brave to Maven like this: + +````xml + + io.zipkin.brave + brave-instrumentation-grpc + +```` + +and to Gradle like this: + +````groovy +compile("io.zipkin.brave:brave-instrumentation-grpc") +```` + +### Spring Cloud Sleuth + +You can add sleuth to your application using Maven like this: + +````xml + + org.springframework.cloud + spring-cloud-starter-sleuth + +```` + +and using Gradle like this: + +````groovy +compile("org.springframework.cloud:spring-cloud-starter-sleuth") +```` + +Please refer to the [official documentation](https://spring.io/projects/spring-cloud-sleuth) on how to set up/configure +Sleuth. + +## Opt-Out + +You can opt-out from the grpc-tracing using the following property: + +````property +spring.sleuth.grpc.enabled=false +```` + +## Additional Notes + +Spring-Cloud-Sleuth provides a few classes such as +[`SpringAwareManagedChannelBuilder`](https://javadoc.io/page/org.springframework.cloud/spring-cloud-sleuth-core/latest/org/springframework/cloud/sleuth/instrument/grpc/SpringAwareManagedChannelBuilder.html), +those classes solely exists for compatibility reasons with a different library. Do not use them with this library. +grpc-spring-boot-starter provides the same/extended functionality out of the box with the +[`GrpcChannelFactory`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/channelfactory/GrpcChannelFactory.html) +and related classes. See also +[sleuth's javadoc note](https://github.com/spring-cloud/spring-cloud-sleuth/blob/59216c32f7848ec337fb68d1dbec8e87eeb6bf59/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/grpc/SpringAwareManagedChannelBuilder.java#L31-L34). + +---------- + +[<- Back to Index](index.md) diff --git a/docs/en/client/configuration.md b/docs/en/client/configuration.md new file mode 100644 index 000000000..6c33be934 --- /dev/null +++ b/docs/en/client/configuration.md @@ -0,0 +1,281 @@ +# Configuration + +[<- Back to Index](../index.md) + +This section describes how you can configure your grpc-spring-boot-starter clients. + +## Table of Contents + +- [Configuration via Properties](#configuration-via-properties) + - [Choosing the Target](#choosing-the-target) +- [Configuration via Beans](#configuration-via-beans) + - [GrpcChannelConfigurer](#grpcchannelconfigurer) + - [ClientInterceptor](#clientinterceptor) + - [StubFactory](#stubfactory) + - [StubTransformer](#stubtransformer) +- [Custom NameResolverProvider](#custom-nameresolverprovider) + +## Additional Topics + +- [Getting Started](getting-started.md) +- *Configuration* +- [Security](security.md) +- [Tests with Grpc-Stubs](testing.md) + +## Configuration via Properties + +grpc-spring-boot-starter can be +[configured](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html) via +spring's `@ConfigurationProperties` mechanism. + +You can find all build-in configuration properties here: + +- [`GrpcChannelsProperties`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/config/GrpcChannelsProperties.html) +- [`GrpcChannelProperties`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/config/GrpcChannelProperties.html) +- [`GrpcServerProperties.Security`](https://static.javadoc.io/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/config/GrpcChannelProperties.Security.html) + +If you prefer to read the sources instead, you can do so +[here](https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java#L58). + +The properties for the channels are all prefixed with `grpc.client.__name__.` and `grpc.client.__name__.security.` +respectively. The channel name is taken from the `@GrpcClient("__name__")` annotation. +If you wish to configure some options such as trusted certificates for all servers at once, +you can do so using `GLOBAL` as name. +Properties that are defined for a specific/named channel take precedence over `GLOBAL` ones. + +### Choosing the Target + +You can change the target server using the following property: + +````properties +grpc.client.__name__.address=static://localhost:9090 +```` + +There are a number of supported schemes, that you can use to determine the target server (Priorities 0 (low) - 10 +(high)): + +- `static` (Prio 4): \ + A simple static list of IPs (both v4 and v6), that can be use connect to the server (Supports `localhost`). \ + For resolvable hostnames please use `dns` instead. \ + Example: `static://192.168.1.1:8080,10.0.0.1:1337` +- [`dns`](https://github.com/grpc/grpc-java/blob/master/core/src/main/java/io/grpc/internal/DnsNameResolver.java#L66) + (Prio 5): \ + Resolves all addresses that are bound to the given DNS name. The addresses will be cached and will only be refreshed + if an existing connection is shutdown/fails. \ + Example: `dns:///example.my.company` \ + Notice: There is also a `dns` resolver that is included in grpclb, that has a higher priority (`6`) than the default + one and also supports `SVC` lookups. See also [Kubernetes Setup](../kubernetes.md). +- `discovery` (Prio 6): \ + (Optional) Uses spring-cloud's `DiscoveryClient` to lookup appropriate targets. The connections will be refreshed + automatically during `HeartbeatEvent`s. Uses the `gRPC_port` metadata to determine the port, otherwise uses the + service port. \ + Example: `discovery:///service-name` +- `self` (Prio 0): \ + The self address or scheme is a keyword that is available, if you also use `grpc-server-spring-boot-starter` and + allows you to connect to the server without specifying the own address/port. This is especially useful for tests + where you might want to use random server ports to avoid conflicts. \ + Example: `self` or `self:self` +- `in-process`: \ + This is a special scheme that will bypass the normal channel factory and will use the `InProcessChannelFactory` + instead. Use it to connect to the [`InProcessServer`](../server/configuration.md#enabling-the-inprocessserver). \ + Example: `in-process:foobar` +- `unix` (Available on Unix based systems only): \ + This is a special scheme that uses unix's domain socket addresses to connect to a server. \ + If you are using `grpc-netty` you also need the `netty-transport-native-epoll` dependency. + `grpc-netty-shaded` already contains that dependency, so there is no need to add anything for it to work. \ + Example: `unix:/run/grpc-server` +- *custom*: \ + You can define custom + [`NameResolverProvider`s](https://javadoc.io/page/io.grpc/grpc-all/latest/io/grpc/NameResolverProvider.html) those + will be picked up, by either via Java's `ServiceLoader` or from spring's application context and registered in + the `NameResolverRegistry`. \ + See also [Custom NameResolverProvider](#custom-nameresolverprovider) + +If you don't define an address it will be guessed: + +- First it will try it with just it's name (``) +- If you have configured a default scheme it will try that next (`:/`) +- Then it will use the default scheme of the `NameResolver.Factory` delegate (See the priorities above) + +> **Note:** The number of slashes is important! Also make sure that you don't try to connect to a normal +> web/REST/non-grpc server (port). + +The `SSL`/`TLS` and other security relevant configuration is explained on the [Client Security](security.md) page. + +## Configuration via Beans + +While this library intents to provide most of the features as configuration option, sometimes the overhead for adding it +is too high and thus we didn't add it, yet. If you feel like it is an important feature, feel free to open a feature +request. + +If you want to change the application beyond what you can do through the properties, then you can use the existing +extension points that exist in this library. + +First of all most of the beans can be replaced by custom ones, that you can configure in every way you want. +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. + +### GrpcChannelConfigurer + +The grpc client configurer allows you to add your custom configuration to grpc's `ManagedChannelBuilder`s. + +````java +@Bean +public GrpcChannelConfigurer keepAliveClientConfigurer() { + return (channelBuilder, name) -> { + if (channelBuilder instanceof NettyChannelBuilder) { + ((NettyChannelBuilder) channelBuilder) + .keepAliveTime(30, TimeUnit.SECONDS) + .keepAliveTimeout(5, TimeUnit.SECONDS); + } + }; +} +```` + +> Be aware that depending on your configuration there might be different types of `ManagedChannelBuilder`s in the +> application context (e.g. the `InProcessChannelFactory`). + +### ClientInterceptor + +`ClientInterceptor`s can be used for various tasks, including: + +- Authentication/Authorization +- Request validation +- Response filtering +- Attaching additional context to the call (e.g. tracing ids) +- Exception to error `Status` response mapping +- Logging +- ... + +There are three ways to add a `ClientInterceptor` to your channel. + +- Define the `ClientInterceptor` as a global interceptor using either the `@GrpcGlobalClientInterceptor` annotation, + or a `GlobalClientInterceptorConfigurer` +- Explicitly list them in the `@GrpcClient#interceptors` or `@GrpcClient#interceptorNames` field +- Use a `StubTransformer` and call `stub.withInterceptors(ClientInterceptor... interceptors)` + +The following examples demonstrate how to use annotations to create a global client interceptor: + +````java +@Configuration +public class ThirdPartyInterceptorConfig {} + + @GrpcGlobalServerInterceptor + LogGrpcInterceptor logServerInterceptor() { + return new LogGrpcInterceptor(); + } + +} +```` + +This variant is very handy if you wish to add third-party interceptors to the global scope. + +For your own interceptor implementations you can achieve the same result by adding the annotation to the class itself: + +````java +@GrpcGlobalServerInterceptor +public class LogGrpcInterceptor implements ServerInterceptor { + + private static final Logger log = LoggerFactory.getLogger(LogGrpcInterceptor.class); + + @Override + public ServerCall.Listener interceptCall( + ServerCall serverCall, + Metadata metadata, + ServerCallHandler serverCallHandler) { + + log.info(serverCall.getMethodDescriptor().getFullMethodName()); + return serverCallHandler.startCall(serverCall, metadata); + } + +} +```` + +### StubFactory + +A `StubFactory` is used to create a `Stub` of a specific type. The registered stub factories will be checked in order +and the first applicable factory will be used to create the stub. + +This library has build in support for the `Stub` types defined in grpc-java: + +- [`AsyncStubs`](https://grpc.github.io/grpc-java/javadoc/io/grpc/stub/AbstractAsyncStub.html) +- [`BlockingStubs`](https://grpc.github.io/grpc-java/javadoc/io/grpc/stub/AbstractBlockingStub.html) +- [`FutureStubs`](https://grpc.github.io/grpc-java/javadoc/io/grpc/stub/AbstractFutureStub.html) + +But you can easily add support for other `Stub` types by adding a custom `StubFactory` to your application context. + +````java +@Component +public class MyCustomStubFactory implements StubFactory { + + @Override + public MyCustomStub createStub(Class> stubType, Channel channel) { + try { + Class enclosingClass = stubType.getEnclosingClass(); + Method factoryMethod = enclosingClass.getMethod("newMyBetterFutureStub", Channel.class); + return stubType.cast(factoryMethod.invoke(null, channel)); + } catch (Exception e) { + throw new BeanInstantiationException(stubType, "Failed to create gRPC stub", e); + } + } + + @Override + public boolean isApplicable(Class> stubType) { + return AbstractMyCustomStub.class.isAssignableFrom(stubType); + } + +} +```` + +> **Note:** Please report missing stub types (and the corresponding library) in our issue tracker so that we can add +> support if possible. + +### StubTransformer + +The stub transformer allows you to modify `Stub`s right before they are injected to your beans. +This is the recommended way to add `CallCredentials` to your stubs. + +````java +@Bean +public StubTransformer call() { + return (name, stub) -> { + if ("serviceA".equals(name)) { + return stub.withWaitForReady(); + } else { + return stub; + } + }; +} +```` + +You can also use `StubTransformer`s to add custom `ClientInterceptor`s to your stub. + +> **Note**: The `StubTransformer`s are applied after the `@GrpcClient#interceptors(Names)` have been added. + +## Custom NameResolverProvider + +Sometimes you might have some custom logic that decides which server you wish to connect to, you can achieve this +using a custom `NameResolverProvider`. + +> **Note:** This can only be used to decide this on an application level and not on a per request level. + +This library provides some `NameResolverProvider`s itself so you can use them as a +[reference](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver). + +You can register your `NameResolverProvider` by adding it to `META-INF/services/io.grpc.NameResolverProvider` for Java's +`ServiceLoader` or adding it your spring context. If you wish to use some spring beans inside your `NameResolver`, then +you have to define it via spring context (unless you wish to use `static`). + +> **Note:** `NameResolverProvider`s are registered gloablly, so might run into issues if you boot up two or more +> applications simulataneosly in the same JVM (e.g. during tests). + +## Additional Topics + +- [Getting Started](getting-started.md) +- *Configuration* +- [Security](security.md) +- [Tests with Grpc-Stubs](testing.md) + +---------- + +[<- Back to Index](../index.md) diff --git a/docs/en/client/getting-started.md b/docs/en/client/getting-started.md new file mode 100644 index 000000000..7b0d42a83 --- /dev/null +++ b/docs/en/client/getting-started.md @@ -0,0 +1,185 @@ +# Getting Started + +[<- Back to Index](../index.md) + +This section deals with how to get Spring to connect to a grpc server and manage the connection for you. + +## Table of Contents + +- [Project Setup](#project-setup) +- [Dependencies](#dependencies) + - [Interface-Project](#interface-project) + - [Server-Project](#server-project) + - [Client-Project](#client-project) +- [Using the Stubs to connect to the Server](#using-the-stubs-to-connect-to-the-server) + - [Explaining the Client Components](#explaining-the-client-components) + - [Accessing the Client](#accessing-the-client) + +## Additional Topics + +- *Getting Started* +- [Configuration](configuration.md) +- [Security](security.md) +- [Tests with Grpc-Stubs](testing.md) + +## Project Setup + +Before we start adding the dependencies lets start with some of our recommendation for your project setup. + +![project setup](/grpc-spring-boot-starter/assets/images/client-project-setup.svg) + +We recommend splitting your project into 2-3 separate modules. + +1. **The interface project** + Contains the raw protobuf files and generates the java model and service classes. You probably share this part. +2. **The server project** + Contains the actual implementation of your project and uses the interface project as dependency. +3. **The client projects** (optional and possibly many) + Any client projects that use the pre-generated stubs to access the server. + +## Dependencies + +### Interface-Project + +See the [server getting started page](../server/getting-started.md#interface-project) + +### Server-Project + +See the [server getting started page](../server/getting-started.md#server-project) + +### Client-Project + +#### Maven (Client) + +````xml + + + net.devh + grpc-client-spring-boot-starter + + + + example + my-grpc-interface + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + +```` + +#### Gradle (Client) + +````gradle +apply plugin: 'org.springframework.boot' + +dependencies { + compile('org.springframework.boot:spring-boot-starter') + compile('net.devh:grpc-client-spring-boot-starter') + compile('my-example:my-grpc-interface') +} + +buildscript { + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} + +```` + +## Using the Stubs to connect to the Server + +This section assumes that you have already defined and generated your [Protobuf service](../server/getting-started.md#creating-the-grpc-service-definitions). + +### Explaining the Client Components + +The following list contains all features that you might encounter on the client side. +If you don't wish to use any advanced features, then the first element is probably all you need to use. + +- [`@GrpcClient`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/inject/GrpcClient.html): + 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. \ + **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()`. +- [`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. +- [`ManagedChannel`](https://javadoc.io/page/io.grpc/grpc-all/latest/io/grpc/ManagedChannel.html): + The ManagedChannel is a special variant of a Channel as it allows management operations to the connection pool such as + shuting it down. +- [`NameResolver`](https://javadoc.io/page/io.grpc/grpc-all/latest/io/grpc/NameResolver.html) respectively + [`NameResolver.Factory`](https://javadoc.io/page/io.grpc/grpc-all/latest/io/grpc/NameResolver.Factory.html): + A class that will be used to resolve the address to a list of `SocketAddress`es, the address will usually be + re-resolved when a connection to a previously listed server fails or the channel was idle. + See also [Configuration -> Choosing the Target](configuration.md#choosing-the-target). +- [`ClientInterceptor`](https://javadoc.io/page/io.grpc/grpc-all/latest/io/grpc/ClientInterceptor.html): + Intercepts every call before they are handed to the `Channel`. Can be used for logging, monitoring, metadata handling, + and request/response rewriting. + grpc-spring-boot-starter will automatically pick up all client interceptors that are annotated with + [`@GrpcGlobalClientInterceptor`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/interceptor/GrpcGlobalClientInterceptor.html) + or are manually registered to the + [`GlobalClientInterceptorRegistry`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/interceptor/GlobalClientInterceptorRegistry.html). + See also [Configuration -> ClientInterceptor](configuration.md#clientinterceptor). +- [`CallCredentials`](https://javadoc.io/page/io.grpc/grpc-all/latest/io/grpc/CallCredentials.html): + A potentially active component that manages the authentication for the calls. It can be used to store credentials or + session tokens. It can also be used to authenticate at an authentication provider and then use returned tokens (such + as OAuth) to authorize the actual request. In addition to that, it is able to renew the token, if it expired and + re-sent the request. If exactly one `CallCredentials` bean is present on your application context then spring will + automatically attach it to all `Stub`s (**NOT** `Channel`s). The + [`CallCredentialsHelper`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/security/CallCredentialsHelper.html) + utility class helps you to create commonly used `CallCredentials` types and related `StubTransformer`. +- [`StubFactory`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/stubfactory/StubFactory.html): + A factory that can be used to create a specfic `Stub` type from a `Channel`. Multiple `StubFactory`s can be registered to support different stub types. + See also [Configuration -> StubFactory](configuration.md#stubfactory). +- [`StubTransformer`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/inject/StubTransformer.html): + A transformer that will be applied to all client `Stub`s before they are injected. + See also [Configuration -> StubTransformer](configuration.md#stubtransformer). + +### Accessing the Client + +We recommended to inject (`@GrpcClient`) `Stub`s instead of plain `Channel`s. + +> **Note:** There are different types of `Stub`s. Not all of them support all request types (streaming calls). + +````java +import example.HelloRequest; +import example.MyServiceGrpc.MyServiceBlockingStub; + +import net.devh.boot.grpc.client.inject.GrpcClient; + +import org.springframework.stereotype.Service; + +@Service +public class FoobarService { + + @GrpcClient("myService") + private MyServiceBlockingStub myServiceStub; + + public String receiveGreeting(String name) { + HelloRequest request = HelloRequest.newBuilder() + .setName(name) + .build() + return myServiceStub.sayHello(request).getMessage(); + } + +} +```` + +## Additional Topics + +- *Getting Started* +- [Configuration](configuration.md) +- [Security](security.md) + +---------- + +[<- Back to Index](../index.md) diff --git a/docs/en/client/security.md b/docs/en/client/security.md new file mode 100644 index 000000000..22bdd0c4d --- /dev/null +++ b/docs/en/client/security.md @@ -0,0 +1,133 @@ +# Client Security + +[<- Back to Index](../index.md) + +This page describes how you connect to a grpc server and authenticate yourself. + +## Table of Contents + +- [Enable Transport Layer Security](#enable-transport-layer-security) + - [Prerequisites](#prerequisites) +- [Disable Transport Layer Security](#disable-transport-layer-security) + - [Trusting a Server](#trusting-a-server) +- [Mutual Certificate Authentication](#mutual-certificate-authentication) +- [Authentication](#authentication) + +## Additional Topics + +- [Getting Started](getting-started.md) +- [Configuration](configuration.md) +- *Security* +- [Tests with Grpc-Stubs](testing.md) + +## Enable Transport Layer Security + +Grpc uses `TLS` as the default way to connect to a server so there isn't much else to do. + +If you wish to check that you didn't accidentally overwrite the configuration, then check whether the given property +is configured like this or is using its default: + +````properties +grpc.client..negotiationType=TLS +```` + +For the corresponding server configuration read the [Server Security](../server/security.md) page. + +### Prerequisites + +As always there are some simple prerequisites that needs to be met: + +- Have a compatible `SSL`/`TLS` implementation on your classpath + - [grpc-netty-shaded](https://mvnrepository.com/artifact/io.grpc/grpc-netty-shaded) has this included + - For [`grpc-netty`](https://mvnrepository.com/artifact/io.grpc/grpc-netty) add a dependency to + [`netty-tcnative-boringssl-static`](https://mvnrepository.com/artifact/io.netty/netty-tcnative-boringssl-static) + (Please use the **exact same** (compatible) versions that are listed in the table in [grpc-java's netty security section](https://github.com/grpc/grpc-java/blob/master/SECURITY.md#netty)). + +## Disable Transport Layer Security + +> **WARNING:** Do NOT do this in production. + +Sometimes you don't have the required certificates available (e.g. during development), thus you might you wish to +disable transport layer security, you can do that like this: + +````properties +grpc.client.__name__.negotiationType=PLAINTEXT +```` + +The following example demonstrates how you can configure this property in your tests: + +````java +@SpringBootTest(properties = "grpc.client.test.negotiationType=PLAINTEXT") +@SpringJUnitConfig(classes = TestConfig.class) +@DirtiesContext +public class PlaintextSetupTest { + + @GrpcClient("test") + private MyServiceBlockingStub myService; +```` + +### Trusting a Server + +If you want to trust a server whose certificate is not in the general trust store, or you want to limit which +certificates you trust, you can do so using the following property: + +````properties +grpc.client.__name__.security.trustCertCollection=file:trusted-server.crt.collection +```` + +If you want to know what options are supported here, read +[Spring's Resource docs](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#resources-resourceloader). + +If you use a service identifier, there may be problems with the certificate because it is not valid for the internal service name. In this case you can specify for which name the certificate must be valid: + +````properties +grpc.client.__name__.security.authorityOverride=localhost +```` + +## Mutual Certificate Authentication + +In secure environments, you might have to authenticate yourself using a client certificate. This certificate is +usually provided by the server so all you have to do is configure your application to actually use: + +````properties +grpc.client.__name__.security.clientAuthEnabled=true +grpc.client.__name__.security.certificateChain=file:certificates/client.crt +grpc.client.__name__.security.privateKey=file:certificates/client.key +```` + +## Authentication + +In addition to mutual certificate authentication, there are several other ways to authenticate yourself, such as +`BasicAuth`. + +grpc-spring-boot-starter provides, besides some helper methods, only implementations for BasicAuth. However, there are +various libraries out there that provide implementations for grpc's +[`CallCredentials`](https://grpc.github.io/grpc-java/javadoc/io/grpc/CallCredentials.html). +`CallCredentials` are potentially active components because they can authenticate the request using a (third party) +service and can manage and renew session tokens themselves. + +If you have exactly one `CallCredentials` in your application context, we'll automatically create a `StubTransformer` +for you and configure all `Stub`s to use it. If you wish to configure different credentials per stub, then you use our +helper methods in the +[`CallCredentialsHelper`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/security/CallCredentialsHelper.html) +utility. + +> **Note:** `StubTransformer`s can only automatically configure injected `Stub`s. They are unable to modify raw +> `Channel`s. + +You can also configure the `CallCredentials` just in time (e.g. for user dependent credentials): + +````java +MyServiceBlockingStub myServiceForUser = myService.withCallCredentials(userCredentials); +return myServiceForUser.send(request); +```` + +## Additional Topics + +- [Getting Started](getting-started.md) +- [Configuration](configuration.md) +- *Security* + +---------- + +[<- Back to Index](../index.md) diff --git a/docs/en/client/testing.md b/docs/en/client/testing.md new file mode 100644 index 000000000..a92e5f704 --- /dev/null +++ b/docs/en/client/testing.md @@ -0,0 +1,266 @@ +# Tests with Grpc-Stubs + +[<- Back to Index](../index.md) + +This section describes how you write tests for components that use the `@GrpcClient` annotation or grpc's stubs. + +## Table of Contents + +- [Introductory Words](#introductory-words) +- [The Component to test](#the-component-to-test) +- [Useful Dependencies](#useful-dependencies) +- [Using a Mocked Stub](#using-a-mocked-stub) +- [Running a Dummy Server](#running-a-dummy-server) + +## Additional Topics + +- [Getting Started](getting-started.md) +- [Configuration](configuration.md) +- [Security](security.md) +- *Tests with Grpc-Stubs* + +## Introductory Words + +Generally there are two ways to test your component containing a grpc stub: + +- [Using a Mocked Stub](#using-a-mocked-stub) +- [Running a Dummy Server](#running-a-dummy-server) + +> Note: There are very important differences in both variants that might affect you during the tests. +> Please consider the pros and cons listed at each on the variants carefully. + +## The Component to test + +Let's assume that we wish to test the following component: + +````java +@Component +public class MyComponent { + + private ChatServiceBlockingStub chatService; + + @GrpcClient("chatService") + public void setChatService(ChatServiceBlockingStub chatService) { + this.chatService = chatService; + } + + public String sayHello(String name) { + HelloRequest request = HelloRequest.newBuilder() + .setName(name) + .build(); + HelloReply reply = chatService.sayHello(name) + return reply.getMessage(); + } + +} +```` + +## Useful Dependencies + +Before you start writing your own test framework, you might want to use the following libraries to make your work easier. + +> **Note:** Spring-Boot-Test already contains some of these dependencies, so make sure you exclude conflicting versions. + +For Maven add the following dependencies: + +````xml + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + + io.grpc + grpc-testing + test + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.junit.vintage + junit-vintage-engine + + + + + + org.mockito + mockito-all + test + +```` + +For Gradle use: + +````groovy +// JUnit-Test-Framework +testImplementation("org.junit.jupiter:junit-jupiter-api") +testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") +// Grpc-Test-Support +testImplementation("io.grpc:grpc-testing") +// Spring-Test-Support (Optional) +testImplementation("org.springframework.boot:spring-boot-starter-test") { + // Exclude the test engine you don't need + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' +} +// Mocking Framework (Optional) +testImplementation("org.mockito:mockito-all") +```` + +## Using a Mocked Stub + +In order to test the method we mock the stub and inject it using a setter. + +### Pros + +- Fast +- Supports well-known mocking frameworks + +### Cons + +- Requires "magic" to un-final the stub methods +- Doesn't work out of the box +- Doesn't work for beans that use the stub in `@PostContruct` +- Doesn't work well for beans that use the stub indirectly (via other beans) +- Doesn't work well for tests that start Spring + +### Implementation + +1. Add mockito to our dependencies (see [above](#useful-dependencies)) +2. Configure mockito to work with final classes/methods + + For this we need to create a file `src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker` containing: + + ````txt + mock-maker-inline + ```` + +3. Write our mocks as usual and explicitly set it on your component in test + + ````java + public class MyComponentTest { + + private MyComponent myComponent = new MyComponent(); + private ChatServiceBlockingStub chatService = Mockito.mock(ChatServiceBlockingStub.class); + + @BeforeEach + void setup() { + myComponent.setChatService(chatService); + } + + @Test + void testSayHello() { + Mockito.when(chatService.sayHello(...)).thenAnswer(...); + assertThat(myComponent.sayHello("ThisIsMyName")).contains("ThisIsMyName"); + } + + } + ```` + +## Running a Dummy Server + +In order to test the method we start a grpc server ourselves and connect to it during our tests. + +### Pros + +- No need to fake anything related to the component +- No "magic" + +### Cons + +- Requires us to fake implement the actual service +- Requires Spring to run + +### Implementation + +The actual implementation of the test might look somewhat like this: + +````java +@SpringBootTest(properties = { + "grpc.server.inProcessName=test", // Enable inProcess server + "grpc.server.port=-1", // Disable external server + "grpc.client.chatService.address=in-process:test" // Configure the client to connect to the inProcess server + }) +@SpringJUnitConfig(classes = { MyComponentIntegrationTestConfiguration.class }) +// Spring doesn't start without a config (might be empty) +@DirtiesContext // Ensures that the grpc-server is properly shutdown after each test + // Avoids "port already in use" during tests +public class MyComponentTest { + + @Autowired + private MyComponent myComponent; + + @Test + @DirtiesContext + void testSayHello() { + assertThat(myComponent.sayHello("ThisIsMyName")).contains("ThisIsMyName"); + } + +} +```` + +and the required configuration looks like this: + +````java +@Configuration +@ImportAutoConfiguration({ + GrpcServerAutoConfiguration.class, // Create required server beans + GrpcServerFactoryAutoConfiguration.class, // Select server implementation + GrpcClientAutoConfiguration.class}) // Support @GrpcClient annotation +public class MyComponentIntegrationTestConfiguration { + + @Bean + MyComponent myComponent() { + return new MyComponent(); + } + + @Bean + ChatServiceImplForMyComponentIntegrationTest chatServiceImpl() { + return new ChatServiceImplForMyComponentIntegrationTest(); + } + +} +```` + +and the dummy service implementation might look like this: + +````java +@GrpcService +public class ChatServiceImplForMyComponentIntegrationTest extends ChatServiceGrpc.ChatServiceImplBase { + + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + HelloReply response = HelloReply.newBuilder() + .setMessage("Hello ==> " + request.getName()) + .build(); + responseObserver.onNext(response); + responseObserver.onComplete(); + } + + // Methods that aren't used in the test don't need to be implemented + +} +```` + +## Additional Topics + +- [Getting Started](getting-started.md) +- [Configuration](configuration.md) +- [Security](security.md) +- *Tests with Grpc-Stubs* + +---------- + +[<- Back to Index](../index.md) diff --git a/docs/en/contributions.md b/docs/en/contributions.md new file mode 100644 index 000000000..d126458bd --- /dev/null +++ b/docs/en/contributions.md @@ -0,0 +1,13 @@ +# Contributing + +[<- Back to Index](index.md) + +Contributions are always welcome. + +For more information refer to our [contribution guidelines](https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/CONTRIBUTING.md). + +**Thanks to all our [contributors](https://github.com/yidongnan/grpc-spring-boot-starter/graphs/contributors)!** + +---------- + +[<- Back to Index](index.md) diff --git a/docs/en/examples.md b/docs/en/examples.md new file mode 100644 index 000000000..0d09a23bb --- /dev/null +++ b/docs/en/examples.md @@ -0,0 +1,35 @@ +# Examples + +The example projects demonstrate how to use the projects. + +These projects work as is and could be used as templates for your own projects. +We use them to verify that this library works in different environments, and we don't break anything unnoticed. + +> **Note:** If you have questions regarding these sample projects or would like an example for another use case, +> feel free to open an [issue](https://github.com/yidongnan/grpc-spring-boot-starter/issues). + +## Local example + +The simplest setup of all, using a local server and one or more clients. + +- [Server](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples/local-grpc-server) +- [Client](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples/local-grpc-client) +- [Instructions](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples#local-mode) + +## Cloud example + +A setup for cloud environments using a eureka service discovery service. + +- [Eureka-Server](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples/cloud-eureka-server) +- [Server](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples/cloud-grpc-server) +- [Client](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples/cloud-grpc-client) +- [Instructions](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples#cloud-mode) + +## Basic Auth Example + +A setup that demonstrates the usage of spring-security with grpc. +This setup uses basic auth for simplicity reasons, but other authentication mechanism can be used for this as well. + +- [Server](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples/security-grpc-server) +- [Client](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples/security-grpc-client) +- [Instructions](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples#with-basic-auth-security) diff --git a/docs/en/flavors.md b/docs/en/flavors.md new file mode 100644 index 000000000..0027a0a08 --- /dev/null +++ b/docs/en/flavors.md @@ -0,0 +1,46 @@ +# gRPC-Java Flavors + +Aside from [grpc-java](https://github.com/grpc/grpc-java/), this library also supports other java based +grpc-implementations. + +| Flavor | Server | Client | +| --- | --- | --- | +| [grpc-java](https://github.com/grpc/grpc-java/) | ✔️ | ✔️ | +| [Reactive gRPC (Reactor)](https://github.com/salesforce/reactive-grpc/tree/master/reactor) | ✔️ | ✏️ | +| [Reactive gRPC (RxJava)](https://github.com/salesforce/reactive-grpc/tree/master/rx-java) | ✔️ | ✏️ | +| [grpc-kotlin](https://github.com/grpc/grpc-kotlin) | ✔️ | ✏️ | +| [ScalaPB](https://scalapb.github.io/grpc.html) | ✔️ | ✏️ | +| [akka-grpc](https://github.com/akka/akka-grpc) | ✔️ | ✏️ | +| ... | ✔️ | ✏️ | + +*✔️ = Build-in support* | +*✏️ = Requires customization* + +> **Note:** You might require additional dependencies depending on your grpc java flavor. + +## Server side + +The server side should work without any additional configuration. Just annotatate your implementation of the generated +`BindableService` class with `@GrpcService` and it will be picked up automatically. + +See also: + +- [Server - Getting Started](server/getting-started.md) + +## Client side + +The client side requires a `StubFactory` for each type of stub. + +This library ships the following stub factory beans by default: + +- gRPC-Java + - `AbstractAsyncStub` -> `AsyncStubFactory` + - `AbstractBlockingStub` -> `BlockingStubFactory` + - `AbstractFutureStub` -> `FutureStubFactory` + +Please report missing stub types so that we can add support for them. + +See also: + +- [Client - Getting Started](client/getting-started.md) +- [Client - Configuration - StubFactory](client/configuration.md#stubfactory) diff --git a/docs/en/index.md b/docs/en/index.md new file mode 100644 index 000000000..cc13b7fd7 --- /dev/null +++ b/docs/en/index.md @@ -0,0 +1,33 @@ +# gRPC-Spring-Boot-Starter Documentation + +gRPC-spring-boot-starter combines [google's open-source high performance RPC-framework](https://grpc.io) with +[spring boot's ease of setup](https://spring.io/projects/spring-boot). +This project simplifies the gRPC-server/client setup to adding one dependency to your project and adding a single +annotation to your service class / client (stub) field. +The features of this library are meant to complement your experience with gRPC and still allow you to do any +customization you need for your project. + +## Table of Contents + +- Server + - [Getting Started](server/getting-started.md) + - [Configuration](server/configuration.md) + - [Exception Handling](server/exception-handling.md) + - [Contextual Data / Scoped Beans](server/contextual-data.md) + - [Testing the Service](server/testing.md) + - [Security](server/security.md) +- Client + - [Getting Started](client/getting-started.md) + - [Configuration](client/configuration.md) + - [Security](client/security.md) + - [Tests with Grpc-Stubs](client/testing.md) +- Others setups +- [Trouble-Shooting](trouble-shooting.md) +- [Example Projects](examples.md) +- [gRPC-Java Flavors](flavors.md) +- [Version Overview](versions.md) +- [Spring Boot Actuator / Metrics Support](actuator.md) +- [Brave-Tracing / Spring Cloud Sleuth Support](brave.md) +- [Kubernetes Setup](kubernetes.md) +- [Benchmarking](benchmarking.md) +- [Contributing](contributions.md) diff --git a/docs/en/kubernetes.md b/docs/en/kubernetes.md new file mode 100644 index 000000000..a2419bcae --- /dev/null +++ b/docs/en/kubernetes.md @@ -0,0 +1,70 @@ +# Kubernetes Setup + +The following section assumes that you have at least some knowledge about deploying an application to Kubernetes. +For more details refer to the [official documentation](https://kubernetes.io/docs/home/) + +Kubernetes (or more precisely most of kubernetes DNS provider's) expose enough information for grpc-java to resolve +the addresses of services that run inside the cluter. Should also work for OKD/OpenShift. + +There are a few things you should keep in mind here though. + +1. Inside your (target's) deployment, make sure that you expose the port specified by `grpc.server.port` + (defaults to `9090`) + +````yaml +[...] + spec: + containers: + - name: my-grpc-server-app + image: ... + ports: + - name: grpc # Use whatever name you want + containerPort: 9090 # Use the same as `grpc.server.port` (prefer 80, 443 or 9090) +[...] +```` + +> **Note:** Container ports can be re-used by other deployments/pods, unless you use `hostPort`s. +> So there is no reason not to use a default one. + +2. Inside your (target's) service definition, you should map that port to your preferred port. + +````yaml +apiVersion: v1 +kind: Service +metadata: + name: my-grpc-server-app # This name is important + namespace: example # This name might be important +spec: + selector: + app: my-grpc-server-app + ports: + - name: grpclb # The name is important, if you wish to use DNS-SVC-Lookups (must be grpclb) + port: 1234 # Remember this port number, unless you use DNS-SVC-Lookups (prefer 80, 443 or 9090) + targetPort: grpc # Use the port name from the deployment (or just the port number) +```` + +> **Note:** Service ports can be re-used by other services, unless you use `hostPort`s. +> So there is no reason not to use a default one. + +3. Inside your client application config, configure the channel address to refer to the service name: + +````properties +## Choose your matching variant +# Same namespace (target port=80 or derived by DNS-SVC) +grpc.clients.my-grpc-server-app.address=dns:///my-grpc-server-app +# Same namespace (different port) +grpc.clients.my-grpc-server-app.address=dns:///my-grpc-server-app:1234 +# Different namespace +grpc.clients.my-grpc-server-app.address=dns:///my-grpc-server-app.example:1234 +# Different cluster +grpc.clients.my-grpc-server-app.address=dns:///my-grpc-server-app.example.svc.cluster.local:1234 +# Format +grpc.clients.my-grpc-server-app.address=dns:///[.[.]][:] +```` + +> **Note:** DNS-SVC lookups require the `grpclb` dependency to be present and the service's port name to be `grpclb`. +> Refer to grpc's official docs for more details. + +---------- + +[<- Back to Index](index.md) diff --git a/docs/en/server/configuration.md b/docs/en/server/configuration.md new file mode 100644 index 000000000..0a6e60eff --- /dev/null +++ b/docs/en/server/configuration.md @@ -0,0 +1,147 @@ +# Configuration + +[<- Back to Index](../index.md) + +This section describes how you can configure your grpc-spring-boot-starter application. + +## Table of Contents + +- [Configuration via Properties](#configuration-via-properties) + - [Changing the Server Port](#changing-the-server-port) + - [Enabling the InProcessServer](#enabling-the-inprocessserver) +- [Configuration via Beans](#configuration-via-beans) + - [ServerInterceptor](#serverinterceptor) + - [GrpcServerConfigurer](#grpcserverconfigurer) + +## Additional topics + +- [Getting Started](getting-started.md) +- *Configuration* +- [Exception Handling](exception-handling.md) +- [Contextual Data / Scoped Beans](contextual-data.md) +- [Testing the Service](testing.md) +- [Security](security.md) + +## Configuration via Properties + +grpc-spring-boot-starter can be +[configured](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html) via +spring's `@ConfigurationProperties` mechanism. + +You can find all build-in configuration properties here: + +- [`GrpcServerProperties`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/config/GrpcServerProperties.html) +- [`GrpcServerProperties.Security`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/config/GrpcServerProperties.Security.html) + +If you prefer to read the sources instead, you can do so +[here](https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/config/GrpcServerProperties.java#L50). + +The properties for the server are all prefixed with `grpc.server.` and `grpc.server.security.` respectively. + +### Changing the Server Port + +If you wish to change the grpc server port from the default (`9090`) to a different port you can do so +using: + +````properties +grpc.server.port=80 +```` + +Set the port to `0` to use a free random port. This feature is intended for deployments with a discovery service and +concurrent tests. + +> Please make sure that you won't run into conflicts with other applications or other endpoints such as `spring-web`. + +The `SSL`/`TLS` and other security relevant configuration is explained on the [Server Security](security.md) page. + +### Enabling the InProcessServer + +Sometimes, you might want to consume your own grpc-service in your own application. You can do so like any other grpc server, but you can save the network overhead by using grpc's `InProcessServer`. + +You can turn it on using the following property: + +````properties +grpc.server.in-process-name= +# Optional: Turn off the external grpc-server +#grpc.server.port=-1 +```` + +This allows clients to connect to the server from within the same application using the following configuration: + +````properties +grpc.client.inProcess.address=in-process: +```` + +This is especially useful for tests as they don't need to open a specific port and thus can run concurrently (on a build +server). + +### Using Unix's Domain Sockets + +On Unix based systems you can also use domain sockets to locally communicate between server and clients. + +Simply configure the address like this: + +````properties +grpc.server.address=unix:/run/grpc-server +```` + +Clients can then connect to the server using the same address. + +If you are using `grpc-netty` you also need the `netty-transport-native-epoll` dependency. +`grpc-netty-shaded` already contains that dependency, so there is no need to add anything for it to work. + +## Configuration via Beans + +While this library intents to provide most of the features as configuration option, sometimes the overhead for adding it +is too high and thus we didn't add it, yet. If you feel like it is an important feature, feel free to open a feature +request. + +If you want to change the application beyond what you can do through the properties, then you can use the existing +extension points that exist in this library. + +First of all most of the beans can be replaced by custom ones, that you can configure in every way you want. +If you don't wish to go that far, you can use classes such as `GrpcServerConfigurer` to configure the server and other +components without losing the features provided by this library. + +### ServerInterceptor + +There are three ways to add a `ServerInterceptor` to your server. + +- Define the `ServerInterceptor` as a global interceptor using either the `@GrpcGlobalServerInterceptor` annotation, + or a `GlobalServerInterceptorConfigurer` +- Explicitly list them in the `@GrpcService#interceptors` or `@GrpcService#interceptorNames` field +- Use a `GrpcServerConfigurer` and call `serverBuilder.intercept(ServerInterceptor interceptor)` + +### GrpcServerConfigurer + +The grpc server configurer allows you to add your custom configuration to grpc's `ServerBuilder`s. + +````java +@Bean +public GrpcServerConfigurer keepAliveServerConfigurer() { + return serverBuilder -> { + if (serverBuilder instanceof NettyServerBuilder) { + ((NettyServerBuilder) serverBuilder) + .keepAliveTime(30, TimeUnit.SECONDS) + .keepAliveTimeout(5, TimeUnit.SECONDS) + .permitKeepAliveWithoutCalls(true); + } + }; +} +```` + +> Be aware that depending on your configuration there might be different types of `ServerBuilder`s in the application +> context (e.g. the `InProcessServerBuilder`). + +## Additional Topics + +- [Getting Started](getting-started.md) +- *Configuration* +- [Exception Handling](exception-handling.md) +- [Contextual Data / Scoped Beans](contextual-data.md) +- [Testing the Service](testing.md) +- [Security](security.md) + +---------- + +[<- Back to Index](../index.md) diff --git a/docs/en/server/contextual-data.md b/docs/en/server/contextual-data.md new file mode 100644 index 000000000..4a6024cbd --- /dev/null +++ b/docs/en/server/contextual-data.md @@ -0,0 +1,72 @@ +# Contextual Data / Scoped Beans + +[<- Back to Index](../index.md) + +This section describes how you can store contextual / per request data. + +## Table of Contents + +- [A Word of Warning](#a-word-of-warning) +- [grpcRequest Scope](#grpcrequest-scope) + +## Additional Topics + +- [Getting Started](getting-started.md) +- [Configuration](configuration.md) +- [Exception Handling](exception-handling.md) +- *Contextual Data / Scoped Beans* +- [Testing the Service](testing.md) +- [Security](security.md) + +## A Word of Warning + +In grpc-java different steps in the message delivery / request process can and will run in different threads. This is +not only but especially relevant for streaming calls. Avoid using `ThreadLocal`s inside your `ServerInterceptor`s and +grpc service method implementations (in the entire grpc context). When it comes down to it, the preparation phase, every +single message and the completion phase might run in different threads. If you wish to store data for the duration of +the session, do so either using grpc's `Context` or `grpcRequest` scoped beans. + +## grpcRequest Scope + +This library adds a `grpcRequest` that works similar to spring web's `request` scope. It is only valid for a single +request. + +First you define your bean with the `@Scope` annotation: + +````java +@Bean +@Scope(scopeName = "grpcRequest", proxyMode = ScopedProxyMode.TARGET_CLASS) +//@Scope(scopeName = GrpcRequestScope.GRPC_REQUEST_SCOPE_NAME, proxyMode = ScopedProxyMode.TARGET_CLASS) +ScopedBean myScopedBean() { + return new ScopedBean(); +} +```` + +> The `proxyMode = TARGET_CLASS` is required unless you only use the bean inside another `grpcRequest` scoped bean. +> Please note that this `proxyMode` does not work with final classes or methods. + +After that, you can use the bean as you are used to: + +````java +@Autowired +private ScopedBean myScopedBean; + +@Override +public void grpcMethod(Request request, StreamObserver responseObserver) { + responseObserver.onNext(myScopedBean.magic(request)); + responseObserver.onCompleted(); +} +```` + +## Additional Topics + +- [Getting Started](getting-started.md) +- [Configuration](configuration.md) +- [Exception Handling](exception-handling.md) +- *Contextual Data / Scoped Beans* +- [Testing the Service](testing.md) +- [Security](security.md) + +---------- + +[<- Back to Index](../index.md) diff --git a/docs/en/server/exception-handling.md b/docs/en/server/exception-handling.md new file mode 100644 index 000000000..ec06c1482 --- /dev/null +++ b/docs/en/server/exception-handling.md @@ -0,0 +1,119 @@ +# Exception Handling inside GrpcService + +[<- Back to Index](../index.md) + +This section describes how you can handle exceptions inside GrpcService layer without cluttering up your code. + +## Table of Contents + +- [Proper exception handling](#proper-exception-handling) +- [Detailed explanation](#detailed-explanation) + - [Priority of mapped exceptions](#priority-of-mapped-exceptions) + - [Sending Metadata in response](#sending-metadata-in-response) + - [Overview of returnable types](#overview-of-returnable-types) + +## Additional Topics + +- [Getting Started](getting-started.md) +- [Configuration](configuration.md) +- [Contextual Data](contextual-data.md) +- *Exception Handling* +- [Testing the Service](testing.md) +- [Security](security.md) + +## Proper exception handling + +If you are already familiar with spring's [error handling](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-error-handling), +you should see some similarities with the exception handling for gRPC. + +_An explanation for the following class:_ + +```java +@GrpcAdvice +public class GrpcExceptionAdvice { + + + @GrpcExceptionHandler + public Status handleInvalidArgument(IllegalArgumentException e) { + return Status.INVALID_ARGUMENT.withDescription("Your description").withCause(e); + } + + @GrpcExceptionHandler(ResourceNotFoundException.class) + public StatusException handleResourceNotFoundException(ResourceNotFoundException e) { + Status status = Status.NOT_FOUND.withDescription("Your description").withCause(e); + Metadata metadata = ... + return status.asException(metadata); + } + +} +``` + +- `@GrpcAdvice` marks a class to be checked up for exception handling methods +- `@GrpcExceptionHandler` marks the annotated method to be executed, in case of the _specified_ exception being thrown + - f.e. if your application throws `IllegalArgumentException`, + then the `handleInvalidArgument(IllegalArgumentException e)` method will be executed +- The method must either return a `io.grpc.Status`, `StatusException`, or `StatusRuntimeException` +- If you handle server errors, you might want to log the exception/stacktrace inside the exception handler + +> **Note:** Cause is not transmitted from server to client - as stated in [official docs](https://grpc.github.io/grpc-java/javadoc/io/grpc/Status.html#withCause-java.lang.Throwable-) +> So we recommend adding it to the `Status`/`StatusException` to avoid the loss of information on the server side. + +## Detailed explanation + +### Priority of mapped exceptions + +Given this method with specified exception in the annotation *and* as a method argument + +```java +@GrpcExceptionHandler(ResourceNotFoundException.class) +public StatusException handleResourceNotFoundException(ResourceNotFoundException e) { + // your exception handling +} +``` + +If the `GrpcExceptionHandler` annotation contains at least one exception type, then only those will be +considered for exception handling for that method. The method parameters must be "compatible" with the specified +exception types. If the annotation does not specify any handled exception types, then all method parameters are being +used instead. + +_("Compatible" means that the exception type in annotation is either the same class or a superclass of one of the +listed method parameters)_ + +### Sending Metadata in response + +In case you want to send metadata in your exception response, let's have a look at the following example. + +```java +@GrpcExceptionHandler +public StatusRuntimeException handleResourceNotFoundException(IllegalArgumentException e) { + Status status = Status.INVALID_ARGUMENT.withDescription("Your description"); + Metadata metadata = ... + return status.asRuntimeException(metadata); +} +``` + +If you do not need `Metadata` in your response, just return your specified `Status`. + +### Overview of returnable types + +Here is a small overview of possible mapped return types with `@GrpcExceptionHandler` and if custom `Metadata` can be +returned: + +| Return Type | Supports Custom Metadata | +| ----------- | --------------- | +| `Status` | ✗ | +| `StatusException` | ✔ | +| `StatusRuntimeException` | ✔ | + +## Additional Topics + +- [Getting Started](getting-started.md) +- [Configuration](configuration.md) +- [Contextual Data](contextual-data.md) +- *Exception Handling* +- [Testing the Service](testing.md) +- [Security](security.md) + +---------- + +[<- Back to Index](../index.md) diff --git a/docs/en/server/getting-started.md b/docs/en/server/getting-started.md new file mode 100644 index 000000000..95ad960ae --- /dev/null +++ b/docs/en/server/getting-started.md @@ -0,0 +1,349 @@ +# Getting Started + +[<- Back to Index](../index.md) + +This section describes the steps necessary to convert your application into a grpc-spring-boot-starter one. + +## Table of Contents + +- [Project Setup](#project-setup) +- [Dependencies](#dependencies) + - [Interface-Project](#interface-project) + - [Server-Project](#server-project) + - [Client-Project](#client-project) +- [Creating the gRPC-Service Definitions](#creating-the-grpc-service-definitions) +- [Implementing the Service](#implementing-the-service) + +## Additional Topics + +- *Getting started* +- [Configuration](configuration.md) +- [Exception Handling](exception-handling.md) +- [Contextual Data / Scoped Beans](contextual-data.md) +- [Testing the Service](testing.md) +- [Security](security.md) + +## Project Setup + +Before we start adding the dependencies lets start with some of our recommendation for your project setup. + +![project setup](/grpc-spring-boot-starter/assets/images/server-project-setup.svg) + +We recommend splitting your project into 2-3 separate modules. + +1. **The interface project** + Contains the raw protobuf files and generates the java model and service classes. You probably share this part. +2. **The server project** + Contains the actual implementation of your project and uses the interface project as dependency. +3. **The client projects** (optional and possibly many) + Any client projects that use the pre-generated stubs to access the server. + +## Dependencies + +### Interface-Project + +#### Maven (Interface) + +````xml + + 3.14.0 + 0.6.1 + 1.35.0 + + + + + io.grpc + grpc-stub + ${grpc.version} + + + io.grpc + grpc-protobuf + ${grpc.version} + + + + jakarta.annotation + jakarta.annotation-api + 1.3.5 + true + + + + + + + kr.motd.maven + os-maven-plugin + 1.6.2 + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + ${protobuf-plugin.version} + + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + + + + compile + compile-custom + + + + + + +```` + +#### Gradle (Interface) + +````gradle +buildscript { + ext { + protobufVersion = '3.14.0' + protobufPluginVersion = '0.8.14' + grpcVersion = '1.35.0' + } +} + +plugins { + id 'java-library' + id 'com.google.protobuf' version "${protobufPluginVersion}" +} + +repositories { + mavenCentral() +} + +dependencies { + implementation "io.grpc:grpc-protobuf:${grpcVersion}" + implementation "io.grpc:grpc-stub:${grpcVersion}" + compileOnly 'jakarta.annotation:jakarta.annotation-api:1.3.5' // Java 9+ compatibility - Do NOT update to 2.0.0 +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${protobufVersion}" + } + generatedFilesBaseDir = "$projectDir/src/generated" + clean { + delete generatedFilesBaseDir + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}" + } + } + generateProtoTasks { + all()*.plugins { + grpc {} + } + } +} + +// Optional +eclipse { + classpath { + file.beforeMerged { cp -> + def generatedGrpcFolder = new org.gradle.plugins.ide.eclipse.model.SourceFolder('src/generated/main/grpc', null); + generatedGrpcFolder.entryAttributes['ignore_optional_problems'] = 'true'; + cp.entries.add( generatedGrpcFolder ); + def generatedJavaFolder = new org.gradle.plugins.ide.eclipse.model.SourceFolder('src/generated/main/java', null); + generatedJavaFolder.entryAttributes['ignore_optional_problems'] = 'true'; + cp.entries.add( generatedJavaFolder ); + } + } +} + +// Optional +idea { + module { + sourceDirs += file("src/generated/main/java") + sourceDirs += file("src/generated/main/grpc") + generatedSourceDirs += file("src/generated/main/java") + generatedSourceDirs += file("src/generated/main/grpc") + } +} +```` + +### Server-Project + +#### Maven (Server) + +````xml + + + net.devh + grpc-server-spring-boot-starter + + + + example + my-grpc-interface + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + +```` + +#### Gradle (Server) + +````gradle +apply plugin: 'org.springframework.boot' + +dependencies { + compile('org.springframework.boot:spring-boot-starter') + compile('net.devh:grpc-server-spring-boot-starter') + compile('my-example:my-grpc-interface') +} + +buildscript { + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} + +```` + +### Client-Project + +See the [client getting started page](../client/getting-started.md#client-project) + +## Creating the gRPC-Service Definitions + +Place your protobuf definitions / `.proto` files in `src/main/proto`. +For writing protobuf files please refer to the official +[protobuf docs](https://developers.google.com/protocol-buffers/docs/proto3). + +Your `.proto` files will look similar to the example below: + +````proto +syntax = "proto3"; + +package net.devh.boot.grpc.example; + +option java_multiple_files = true; +option java_package = "net.devh.boot.grpc.examples.lib"; +option java_outer_classname = "HelloWorldProto"; + +// The greeting service definition. +service MyService { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) { + } +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} +```` + +The configured maven/gradle protobuf plugins will then use invoke the +[`protoc`](https://mvnrepository.com/artifact/com.google.protobuf/protoc) compiler with the +[`protoc-gen-grpc-java`](https://mvnrepository.com/artifact/io.grpc/protoc-gen-grpc-java) plugin and generate the data +classes, grpc service `ImplBase`s and `Stub`s. Please note that other plugins such as +[reactive-grpc](https://github.com/salesforce/reactive-grpc) might generate additional/alternative classes that you have +to use instead. However, they can be used in a similar fashion. + +- The `ImplBase` classes contain the base logic that map the dummy implementation to the grpc service methods. + More about this in the [Implementing the service](#implementing-the-service) topic. +- The `Stub` classes are complete client implementations. + More about this on the [Getting the client started](../client/getting-started.md) page. + +## Implementing the Service + +The `protoc-gen-grpc-java` plugin generates a class for each of your grpc services. +For example: `MyServiceGrpc` where `MyService` is the name of the grpc service in the proto file. This class +contains both the client stubs and the server `ImplBase` that you will need to extend. + +After that you have only four tasks to do: + +1. Make sure that your `MyServiceImpl` extends `MyServiceGrpc.MyServiceImplBase` +2. Add the `@GrpcService` annotation to your `MyServiceImpl` class +3. Make sure that the `MyServiceImpl` is added to your application context, + - either by creating `@Bean` definition in one of your `@Configuration` classes + - or placing it in spring's automatically detected paths (e.g. in the same or a sub package of your `Main` class) +4. Actually implement the grpc service methods. + +Your grpc service class will then look somewhat similar to the example below: + +````java +import example.HelloReply; +import example.HelloRequest; +import example.MyServiceGrpc; + +import io.grpc.stub.StreamObserver; + +import net.devh.boot.grpc.server.service.GrpcService; + +@GrpcService +public class MyServiceImpl extends MyServiceGrpc.MyServiceImplBase { + + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + HelloReply reply = HelloReply.newBuilder() + .setMessage("Hello ==> " + request.getName()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + +} +```` + +> **Note**: Theoretically it is not necessary to extend the `ImplBase` and instead implement `BindableService` yourself. +> However, doing so might result in bypassing spring security's checks. + +That's all there is to that. Now you can start your spring-boot application and start sending requests to your +grpc-service. + +By default, the grpc-server will be started on port `9090` using `PLAINTEXT` mode. + +You can test that your application is working as expected by running these [gRPCurl](https://github.com/fullstorydev/grpcurl) commands: + +````sh +grpcurl --plaintext localhost:9090 list +grpcurl --plaintext localhost:9090 list net.devh.boot.grpc.example.MyService +# Linux (Static content) +grpcurl --plaintext -d '{"name": "test"}' localhost:9090 net.devh.boot.grpc.example.MyService/sayHello +# Windows or Linux (dynamic content) +grpcurl --plaintext -d "{\"name\": \"test\"}" localhost:9090 net.devh.boot.grpc.example.MyService/sayHello +```` + +See [here](testing.md#grpcurl) for `gRPCurl` example command output and additional information. + +> Note: Don't forget to write [actual/automated tests](testing.md) for your service implementation. + +## Additional Topics + +- *Getting Started* +- [Configuration](configuration.md) +- [Exception Handling](exception-handling.md) +- [Contextual Data / Scoped Beans](contextual-data.md) +- [Testing the Service](testing.md) +- [Security](security.md) + +---------- + +[<- Back to Index](../index.md) diff --git a/docs/en/server/security.md b/docs/en/server/security.md new file mode 100644 index 000000000..876ee9d8e --- /dev/null +++ b/docs/en/server/security.md @@ -0,0 +1,290 @@ +# Server Security + +[<- Back to Index](../index.md) + +This section describes how you secure your application using transport layer security and authentication. +We strongly recommend enabling at least transport layer security. + +## Table of Contents + +- [Enable Transport Layer Security](#enable-transport-layer-security) + - [Prerequisites](#prerequisites) + - [Configuring the Server](#configuring-the-server) +- [Mutual Certificate Authentication](#mutual-certificate-authentication) +- [Authentication and Authorization](#authentication-and-authorization) + - [Configure Authentication](#configure-authentication) + - [Configure Authorization](#configure-authorization) + +## Additional Topics + +- [Getting Started](getting-started.md) +- [Configuration](configuration.md) +- [Exception Handling](exception-handling.md) +- [Contextual Data / Scoped Beans](contextual-data.md) +- [Testing the Service](testing.md) +- *Security* + +## Enable Transport Layer Security + +You can configure transport level security using spring's configuration mechanisms. For non security related +configuration options refer to the [Configuration](configuration.md) page. + +If you are behind a reverse proxy that handles TLS for you, you might not need to set up `TLS`. Please consult with +security experts, if you are not familiar with security. Don't forget to check the setup for security issues. ^^ + +> **Note:** Please refer to the [official documentation](https://github.com/grpc/grpc-java/blob/master/SECURITY.md) for +> additional information! + +### Prerequisites + +- Have a compatible `SSL`/`TLS` implementation on your classpath + - [grpc-netty-shaded](https://mvnrepository.com/artifact/io.grpc/grpc-netty-shaded) has this included + - For [`grpc-netty`](https://mvnrepository.com/artifact/io.grpc/grpc-netty) add a dependency to + [`netty-tcnative-boringssl-static`](https://mvnrepository.com/artifact/io.netty/netty-tcnative-boringssl-static) + (Please use the **exact same** (compatible) versions that are listed in the table in [grpc-java's netty security section](https://github.com/grpc/grpc-java/blob/master/SECURITY.md#netty)). +- A certificate with its private key + +#### Generating Self Signed Certificates + +If you don't have certificates (e.g. for your internal test server) you can generate them using `openssl`: + +````sh +openssl req -x509 -nodes -subj "//CN=localhost" -newkey rsa:4096 -sha256 -keyout server.key -out server.crt -days 3650 +```` + +Please note that these certificates aren't trusted by any application without additional configuration. +We recommend that you either use certificates that are trusted by a global CA or your company's CA. + +### Configuring the Server + +In order to allow the grpc-server to use `TLS` you have to configure it using the following options: + +````properties +grpc.server.security.enabled=true +grpc.server.security.certificateChain=file:certificates/server.crt +grpc.server.security.privateKey=file:certificates/server.key +#grpc.server.security.privateKeyPassword=MyStrongPassword +```` + +If you want to know what options are supported here, read +[Spring's Resource docs](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#resources-resourceloader). + +For the corresponding client configuration read the [Client Security](../client/security.md) page. + +## Mutual Certificate Authentication + +If you want to make sure that only trustworthy clients can connect to the server you can enable mutual certificate +authentication. +This either allows or forces the client to authenticate itself using a `x509` certificate. + +To enable mutual authentication simply add the following properties to your configuration: + +````properties +grpc.server.security.trustCertCollection=file:certificates/trusted-clients.crt.collection +grpc.server.security.clientAuth=REQUIRE +```` + +You can create the `trusted-clients.crt.collection` file by simply concatenating the clients certificates: + +````sh +cat client*.crt > trusted-clients.crt.collection +```` + +The `clientAuth` mode defines how the server will behave: + +- `REQUIRE` makes client certificate authentication mandatory. +- `OPTIONAL` will request the client to authenticate itself using a certificate, but won't force it to do so. + +You can use `OPTIONAL` if you want to secure only some important services or methods. + +Especially in the later case, it is important to configure the authentication appropriately. + +## Authentication and Authorization + +`grpc-spring-boot-starter` supports `spring-security` natively, so you can just use the well-known annotations to secure +your application. + +![server-request-security](/grpc-spring-boot-starter/assets/images/server-security.svg) + +### Configure Authentication + +In order to support authentication from grpc-clients, you have to define how the clients are allowed to authenticate. +You can do so by defining a +[`GrpcAuthenticationReader`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/security/authentication/GrpcAuthenticationReader.html). + +grpc-spring-boot-starter comes with a number of build-in implementations: + +- [`AnonymousAuthenticationReader`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/security/authentication/AnonymousAuthenticationReader.html) + for spring's anonymous auth. +- [`BasicGrpcAuthenticationReader`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/security/authentication/BasicGrpcAuthenticationReader.html) + for basic auth. +- [`BearerAuthenticationReader`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/security/authentication/BearerAuthenticationReader.html) + for OAuth and similar protocols. +- [`SSLContextGrpcAuthenticationReader`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReader.html) + for certificate based authentication. +- [`CompositeGrpcAuthenticationReader`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/security/authentication/CompositeGrpcAuthenticationReader.html) + to try multiple readers in order. + +Your bean definition will look similar to this example: + +````java +@Bean +public GrpcAuthenticationReader grpcAuthenticationReader() { + return new BasicGrpcAuthenticationReader(); +} +```` + +If you want to force users to authenticate use the `CompositeGrpcAuthenticationReader` and append a +`GrpcAuthenticationReader` that throws a `AuthenticationException`. This marks the authentication as failed and will +stop the processing of the request. If the `GrpcAuthenticationReader` returns null, then the user will continue +unauthenticated. If the reader was able to extract the credentials/authentication, then they will be validated by +spring's `AuthenticationManager`. That instance will then decide, whether the user has sent valid credentials and may +proceed or not. + +#### Example setups + +The following sections contain example configurations for different authentication setups: + +> Note: It is not necessary to wrap the reader in a `CompositeGrpcAuthenticationReader` it should just demonstrate, that +> you could add multiple mechanisms. + +##### BasicAuth + +````java +@Bean +AuthenticationManager authenticationManager() { + final List providers = new ArrayList<>(); + providers.add(...); // Possibly DaoAuthenticationProvider + return new ProviderManager(providers); +} + +@Bean +GrpcAuthenticationReader authenticationReader() { + final List readers = new ArrayList<>(); + readers.add(new BasicGrpcAuthenticationReader()); + return new CompositeGrpcAuthenticationReader(readers); +} +```` + +##### Bearer Authentication (OAuth2/OpenID-Connect) + +````java +@Bean +AuthenticationManager authenticationManager() { + final List providers = new ArrayList<>(); + providers.add(...); // Possibly JwtAuthenticationProvider + return new ProviderManager(providers); +} + +@Bean +GrpcAuthenticationReader authenticationReader() { + final List readers = new ArrayList<>(); + // The actual token class is dependent on your spring-security library (OAuth2/JWT/...) + readers.add(new BearerAuthenticationReader(accessToken -> new BearerTokenAuthenticationToken(accessToken))); + return new CompositeGrpcAuthenticationReader(readers); +} +```` + +You might also want to define your own *GrantedAuthoritiesConverter* to map the permissions/roles in the bearer token +to Spring Security's `GrantedAuthority`s. + +##### Certificate Authentication + +````java +@Bean +AuthenticationManager authenticationManager() { + final List providers = new ArrayList<>(); + providers.add(new X509CertificateAuthenticationProvider(userDetailsService())); + return new ProviderManager(providers); +} + +@Bean +GrpcAuthenticationReader authenticationReader() { + final List readers = new ArrayList<>(); + readers.add(new SSLContextGrpcAuthenticationReader()); + return new CompositeGrpcAuthenticationReader(readers); +} +```` + +See also [Mutual Certificate Authentication](#mutual-certificate-authentication). + +### Configure Authorization + +This step is very important as it actually secures your application against unwanted access. You can secure your +grpc-server in two ways. + +#### gRPC security checks + +One way to secure your application is adding +[`GrpcSecurityMetadataSource`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/security/check/GrpcSecurityMetadataSource.html) +bean to your application context. It allows you to return the security conditions on a per grpc method level. + +An example bean definition (using hard coded rules) might look like this: + +````java +import net.devh.boot.grpc.server.security.check.AccessPredicate; +import net.devh.boot.grpc.server.security.check.ManualGrpcSecurityMetadataSource; + +@Bean +GrpcSecurityMetadataSource grpcSecurityMetadataSource() { + final ManualGrpcSecurityMetadataSource source = new ManualGrpcSecurityMetadataSource(); + source.set(MyServiceGrpc.getMethodA(), AccessPredicate.authenticated()); + source.set(MyServiceGrpc.getMethodB(), AccessPredicate.hasRole("ROLE_USER")); + source.set(MyServiceGrpc.getMethodC(), AccessPredicate.hasAllRole("ROLE_FOO", "ROLE_BAR")); + source.set(MyServiceGrpc.getMethodD(), auth -> "admin".equals(auth.getName())); + source.setDefault(AccessPredicate.denyAll()); + return source; +} + +@Bean +AccessDecisionManager accessDecisionManager() { + final List> voters = new ArrayList<>(); + voters.add(new AccessPredicateVoter()); + return new UnanimousBased(voters); +} +```` + +You have to configure the `AccessDecisionManager` otherwise it doesn't know how to deal with the `AccessPredicate`s. + +This approach has the benefit that you are able to move the configuration to an external file or database. +You have to implement that yourself though. + +#### Spring annotation security checks + +Of course, it is also possible to just use spring-security's annotations. +For this use case you have to add the following annotation to one of your `@Configuration` classes: + +````java +@EnableGlobalMethodSecurity(___Enabled = true, proxyTargetClass = true) +```` + +> Please note that `proxyTargetClass = true` is required! If you forget to add it, you will get a lot of `UNIMPLEMENTED` +> responses. However, you will receive a warning that `MyServiceImpl#bindService()` method is final. Do **NOT** try to +> un-final that method as that would bypass security. + +Then you can simply annotate the grpc method implementation: + +````java +@Override +@Secured("ROLE_ADMIN") +// MyServiceGrpc.methodX +public void methodX(Request request, StreamObserver responseObserver) { + [...] +} +```` + +> This library assumes that you extend the `ImplBase` (generated by grpc) in order to implement the service. Not doing +> so might result in bypassing spring-security. + +## Additional Topics + +- [Getting Started](getting-started.md) +- [Configuration](configuration.md) +- [Exception Handling](exception-handling.md) +- [Contextual Data / Scoped Beans](contextual-data.md) +- [Testing the Service](testing.md) +- *Security* + +---------- + +[<- Back to Index](../index.md) diff --git a/docs/en/server/testing.md b/docs/en/server/testing.md new file mode 100644 index 000000000..0bbaf6277 --- /dev/null +++ b/docs/en/server/testing.md @@ -0,0 +1,379 @@ +# Testing the Service + +[<- Back to Index](../index.md) + +This section describes how you write tests for your grpc-service implementation. + +If you want to test a component that internally uses an `@GrpcClient` annotated field or one of grpc's stubs. +Please refer to [Tests with Grpc-Stubs](../client/testing.md). + +## Table of Contents + +- [Introductory Words](#introductory-words) +- [The Service to Test](#the-service-to-test) +- [Useful Dependencies](#useful-dependencies) +- [Unit Tests](#unit-tests) + - [Standalone Tests](#standalone-tests) + - [Spring-based Tests](#spring-based-tests) +- [Integration Tests](#integration-tests) +- [gRPCurl](#grpcurl) + +## Additional Topics + +- [Getting Started](getting-started.md) +- [Configuration](configuration.md) +- [Exception Handling](exception-handling.md) +- [Contextual Data / Scoped Beans](contextual-data.md) +- *Testing the Service* +- [Security](security.md) + +## Introductory Words + +We all know how important it is to test our application, so I will only refer you to a few links here: + +- [Testing Spring](https://docs.spring.io/spring/docs/current/spring-framework-reference/testing.html) +- [Testing with JUnit](https://junit.org/junit5/docs/current/user-guide/#writing-tests) +- [grpc-spring-boot-starter's Tests](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/tests/src/test/java/net/devh/boot/grpc/test) + +Generally there are three ways to test your grpc service: + +- [Test them directly](#unit-tests) +- [Test them via grpc](#integration-tests) +- [Test them in producation](#grpcurl) (in addition to automated build time tests) + +## The Service to Test + +Let's assume that we wish to test the following service: + +````java +@GrpcService +public class MyServiceImpl extends MyServiceGrpc.MyServiceImplBase { + + private OtherDependency foobar; + + @Autowired + public void setFoobar(OtherDependency foobar) { + this.foobar = foobar; + } + + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + HelloReply response = HelloReply.newBuilder() + .setMessage("Hello ==> " + request.getName()) + .setCounter(foobar.getCount()) + .build(); + responseObserver.onNext(response); + responseObserver.onComplete(); + } + +} +```` + +## Useful Dependencies + +Before you start writing your own test framework, you might want to use the following libraries to make your work easier. + +> **Note:** Spring-Boot-Test already contains some of these dependencies, so make sure you exclude conflicting versions. + +For Maven add the following dependencies: + +````xml + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + + io.grpc + grpc-testing + test + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.junit.vintage + junit-vintage-engine + + + +```` + +For Gradle use: + +````groovy +// JUnit-Test-Framework +testImplementation("org.junit.jupiter:junit-jupiter-api") +testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") +// Grpc-Test-Support +testImplementation("io.grpc:grpc-testing") +// Spring-Test-Support (Optional) +testImplementation("org.springframework.boot:spring-boot-starter-test") { + // Exclude the test engine you don't need + exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' +} +```` + +## Unit Tests + +In the direct tests, we invoke the methods directly on the grpc-service bean/instance. + +> If you create the grpc-service instance yourself, make sure that you populate the required dependencies first. +> If you use Spring, it will take care of the dependencies for you, but in return you will have to configure Spring. + +### Standalone Tests + +The standalone tests don't have any dependencies to external libraries (in fact you don't even need this one). +However, having no external dependencies doesn't always make your life easier, because you might have to replicate +behavior that other libraries might do for you. Using a mocking library such as [Mockito](https://site.mockito.org) +will simplify the process for you, as it limits the depth of the dependency tree. + +````java +public class MyServiceTest { + + private MyServiceImpl myService; + + @BeforeEach + public void setup() { + myService = new MyServiceImpl(); + OtherDependency foobar = ...; // mock(OtherDependency.class) + myService.setFoobar(foobar); + } + + @Test + void testSayHellpo() throws Exception { + HelloRequest request = HelloRequest.newBuilder() + .setName("Test") + .build(); + StreamRecorder responseObserver = StreamRecorder.create(); + myService.sayHello(request, responseObserver); + if (!responseObserver.awaitCompletion(5, TimeUnit.SECONDS)) { + fail("The call did not terminate in time"); + } + assertNull(responseObserver.getError()); + List results = responseObserver.getValues(); + assertEquals(1, results.size()); + HelloReply response = results.get(0); + assertEquals(HelloReply.newBuilder() + .setMessage("Hello ==> Test") + .setCounter(1337) + .build(), response); + } + +} +```` + +### Spring-based Tests + +If you use Spring to manage dependencies for yourself, you're actually tapping into the field of integration tests. +Make sure you don't start the entire application, but only provide the required dependencies as (mocked) beans. + +> **Note:** During tests spring does not automatically setup all required beans. You have to manually create them in +> your `@Configuration` classes. + +````java +@SpringBootTest +@SpringJUnitConfig(classes = { MyServiceUnitTestConfiguration.class }) +// Spring doesn't start without a config (might be empty) +// Don't use @EnableAutoConfiguration in this scenario +public class MyServiceTest { + + @Autowired + private MyServiceImpl myService; + + @Test + void testSayHellpo() throws Exception { + HelloRequest request = HelloRequest.newBuilder() + .setName("Test") + .build(); + StreamRecorder responseObserver = StreamRecorder.create(); + myService.sayHello(request, responseObserver); + if (!responseObserver.awaitCompletion(5, TimeUnit.SECONDS)) { + fail("The call did not terminate in time"); + } + assertNull(responseObserver.getError()); + List results = responseObserver.getValues(); + assertEquals(1, results.size()); + HelloReply response = results.get(0); + assertEquals(HelloReply.newBuilder() + .setMessage("Hello ==> Test") + .setCounter(1337) + .build(), response); + } + +} +```` + +and the required configuration class: + +````java +@Configuration +public class MyServiceUnitTestConfiguration { + + @Bean + OtherDependency foobar() { + // return mock(OtherDependency.class); + } + + @Bean + MyServiceImpl myService() { + return new MyServiceImpl(); + } + +} +```` + +## Integration Tests + +Sometimes, however, you need to test the entire stack. For example, if authentication plays a role. +But also in this case it is recommended to limit the scope of your test to avoid possible external influences like an +empty database. + +At this point it doesn't make any sense to test your spring based application without spring. + +> **Note:** During tests spring does not automatically setup all required beans. You have to manually create them in +> your `@Configuration` or explicitly include the related auto configuration classes. + +````java +@SpringBootTest(properties = { + "grpc.server.inProcessName=test", // Enable inProcess server + "grpc.server.port=-1", // Disable external server + "grpc.client.inProcess.address=in-process:test" // Configure the client to connect to the inProcess server + }) +@SpringJUnitConfig(classes = { MyServiceIntegrationTestConfiguration.class }) +// Spring doesn't start without a config (might be empty) +@DirtiesContext // Ensures that the grpc-server is properly shutdown after each test + // Avoids "port already in use" during tests +public class MyServiceTest { + + @GrpcClient("inProcess") + private MyServiceBlockingStub myService; + + @Test + @DirtiesContext + public void testSayHello() { + HelloRequest request = HelloRequest.newBuilder() + .setName("test") + .build(); + HelloReply response = myService.sayHello(request); + assertNotNull(response); + assertEquals("Hello ==> Test", response.getMessage()) + } + +} +```` + +and the required configuration looks like this: + +````java +@Configuration +@ImportAutoConfiguration({ + GrpcServerAutoConfiguration.class, // Create required server beans + GrpcServerFactoryAutoConfiguration.class, // Select server implementation + GrpcClientAutoConfiguration.class}) // Support @GrpcClient annotation +public class MyServiceIntegrationTestConfiguration { + + @Bean + OtherDependency foobar() { + return ...; // mock(OtherDependency.class); + } + + @Bean + MyServiceImpl myServiceImpl() { + return new MyServiceImpl(); + } + +} +```` + +> Note: This code might look shorter/simpler than the unit test one, but the execution time is serveral times longer. + +## gRPCurl + +[`gRPCurl`](https://github.com/fullstorydev/grpcurl) is a small command line application, +that you can use to query your application at runtime. Or as their Readme states: + +> It's basically `curl` for gRPC servers. + +You can even use the responses with `jq` and use it in your automation. + +Skip the first/this block if you already know what you wish to query. + +````bash +$ # First scan the server for available services +$ grpcurl --plaintext localhost:9090 list +net.devh.boot.grpc.example.MyService +$ # Then list the methods available for that call +$ grpcurl --plaintext localhost:9090 list net.devh.boot.grpc.example.MyService +net.devh.boot.grpc.example.MyService.SayHello +$ # Lets check the request and response types +$ grpcurl --plaintext localhost:9090 describe net.devh.boot.grpc.example.MyService/SayHello +net.devh.boot.grpc.example.MyService.SayHello is a method: +rpc SayHello ( .HelloRequest ) returns ( .HelloReply ); +$ # Now we only have query for the request body structure +$ grpcurl --plaintext localhost:9090 describe net.devh.boot.grpc.example.HelloRequest +net.devh.boot.grpc.example.HelloRequest is a message: +message HelloRequest { + string name = 1; +} +```` + +> Note: `gRPCurl` supports both `.` and `/` as separator between the service name and the method name: +> +> - `net.devh.boot.grpc.example.MyService.SayHello` +> - `net.devh.boot.grpc.example.MyService/SayHello` +> +> We recommend the second variant as it matches grpc's internal full method name and the method name is easier to +> detect in the call. + +````bash +$ # Finally we can call the actual method +$ grpcurl --plaintext localhost:9090 net.devh.boot.grpc.example.MyService/SayHello +{ + "message": "Hello ==> ", + "counter": 1337 +} +$ # Or call it with a populated request body +$ grpcurl --plaintext -d '{"name": "Test"}' localhost:9090 net.devh.boot.grpc.example.MyService/SayHello +{ + "message": "Hello ==> Test", + "counter": 1337 +} +```` + +> Note: If you use the windows terminal or wish to use variables inside the data block then you have to use `"` instead +> of `'` and escape the `"` that are part of the actual json. +> +> ````cmd +> > grpcurl --plaintext -d "{\"name\": \"Test\"}" localhost:9090 net.devh.boot.grpc.example.MyService/sayHello +> { +> "message": "Hello ==> Test", +> "counter": 1337 +> } +> ```` + +For more information regarding `gRPCurl` please refer to their [official documentation](https://github.com/fullstorydev/grpcurl) + +## Additional Topics + +- [Getting Started](getting-started.md) +- [Configuration](configuration.md) +- [Exception Handling](exception-handling.md) +- [Contextual Data / Scoped Beans](contextual-data.md) +- *Testing the Service* +- [Security](security.md) + +---------- + +[<- Back to Index](../index.md) diff --git a/docs/en/trouble-shooting.md b/docs/en/trouble-shooting.md new file mode 100644 index 000000000..de05b6159 --- /dev/null +++ b/docs/en/trouble-shooting.md @@ -0,0 +1,358 @@ +# Trouble-Shooting + +[<- Back to Index](index.md) + +This section describes some common errors with this library and grpc in general, and how to solve them. +Please note that this page can never cover all cases, please also search the existing issues/PRs (both opened and closed +ones) for related topics. If a corresponding topic already exists, leave us a comment / info so that we know that you +are also affected. If there is no such topic, feel free to open a new one as described at the bottom of this page. + +## Table of Contents + +- [NoClassDefFoundError, ClassNotFoundException, NoSuchMethodError, AbstractMethodError](#noclassdeffounderror-classnotfoundexception-nosuchmethoderror-abstractmethoderror) +- [Transport failed](#transport-failed) +- [Network closed for unknown reason](#network-closed-for-unknown-reason) +- [Could not find TLS ALPN provider](#could-not-find-tls-alpn-provider) +- [Dismatching certificates](#dismatching-certificates) +- [Untrusted certificates](#untrusted-certificates) +- [Server port already in use](#server-port-already-in-use) +- [Client fails to resolve domain name](#client-fails-to-resolve-domain-name) +- [Creating issues / asking questions](#creating-issues) + +## NoClassDefFoundError, ClassNotFoundException, NoSuchMethodError, AbstractMethodError + +### Example + +````txt +Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'client' defined in file [~/.../MyGrpcClient.class]: Initialization of bean failed; nested exception is java.lang.NoClassDefFoundError: io/grpc/TlsChannelCredentials$Feature + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:602) + [...] +Caused by: java.lang.NoClassDefFoundError: io/grpc/TlsChannelCredentials$Feature + at io.grpc.netty.ProtocolNegotiators.(ProtocolNegotiators.java:92) +```` + +### The Problem + +The server/client does not start because some class or method is missing. \ +This is usually the case if the grpc-libraries use slightly different versions. + +### The solution + +Make sure to use exactly the same version for all `grpc-java` versions. + +Add the following entry to your `dependencyManagement` section of your project: + +````xml + + io.grpc + grpc-bom + ${grpcVersion} + pom + import + +```` + +You can use a similar approach for gradle: + +````groovy +dependencyManagement { + imports { + mavenBom "io.grpc:grpc-bom:${grpcVersion}" +```` + +> **Note:** grpc-spring-boot-starter isn't strictly bound to a specific version of grpc-java, so you can also use this +> to change the version of grpc-java you are using in your project. + +See also [Could not find TLS ALPN provider](#could-not-find-tls-alpn-provider) + +## Transport failed + +### Server-side + +````txt +2019-07-07 10:05:46.217 INFO 6552 --- [-worker-ELG-3-5] i.g.n.s.i.g.n.N.connections : Transport failed + +io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception: HTTP/2 client preface string missing or corrupt. Hex dump for received bytes: 16030100820100007e0303aae6126974cbb4638b325d6bdb + at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception.connectionError(Http2Exception.java:85) ~[grpc-netty-shaded-1.21.0.jar:1.21.0] + at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.readClientPrefaceString(Http2ConnectionHandler.java:318) ~[grpc-netty-shaded-1.21.0.jar:1.21.0] + at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.decode(Http2ConnectionHandler.java:251) ~[grpc-netty-shaded-1.21.0.jar:1.21.0] + at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler.decode(Http2ConnectionHandler.java:450) [grpc-netty-shaded-1.21.0.jar:1.21.0] +```` + +### Client-side + +````txt +io.grpc.StatusRuntimeException: UNAVAILABLE: io exception + at io.grpc.stub.ClientCalls.toStatusRuntimeException(ClientCalls.java:235) + at io.grpc.stub.ClientCalls.getUnchecked(ClientCalls.java:216) + at io.grpc.stub.ClientCalls.blockingUnaryCall(ClientCalls.java:141) + at net.devh.boot.grpc.examples.lib.SimpleGrpc$SimpleBlockingStub.sayHello(SimpleGrpc.java:178) + [...] +Caused by: io.grpc.netty.shaded.io.netty.handler.ssl.NotSslRecordException: not an SSL/TLS record: 00001204000000000000037fffffff000400100000000600002000000004080000000000000f0001 + at io.grpc.netty.shaded.io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1204) + at io.grpc.netty.shaded.io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1272) + at io.grpc.netty.shaded.io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:502) +```` + +### The Problem + +The server runs in `PLAINTEXT` mode, but the client tries to connect it in `TLS` (default) mode. + +### The simple solution + +a.k.a.: Configure the client to connect in `PLAINTEXT` mode (Not recommended for production). + +Add the following entry to your client side application config: + +````properties +grpc.client.__name__.negotiationType=PLAINTEXT +```` + +### The better solution + +a.k.a.: Configure the server to run in `TLS` mode (Recommended). + +Add the following entry to your sever side application config: + +````properties +grpc.server.security.enabled=true +grpc.server.security.certificateChain=file:certificates/server.crt +grpc.server.security.privateKey=file:certificates/server.key +```` + +## Network closed for unknown reason + +### Client-side + +````txt +io.grpc.StatusRuntimeException: UNAVAILABLE: Network closed for unknown reason +```` + +### The Problem + +You are either (1) trying to connect to an grpc-server in `TLS` mode using a `PLAINTEXT` client +or (2) the target is not a grpc-server (e.g. a web-server). + +### The Solution + +1. Configure your client to use `TLS` mode. + + ````properties + grpc.client.__name__.negotiationType=TLS + ```` + + or remove the `negotiationType` config completely as `TLS` is the default. +2. Validate that the configured server is running and is a grpc-server using + [`grpcurl`](https://github.com/fullstorydev/grpcurl) or a similar tool. + +## Could not find TLS ALPN provider + +### Server-side + +````txt +org.springframework.context.ApplicationContextException: Failed to start bean 'nettyGrpcServerLifecycle'; nested exception is java.lang.IllegalStateException: Could not find TLS ALPN provider; no working netty-tcnative, Conscrypt, or Jetty NPN/ALPN available + at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:185) ~[spring-context-5.1.8.RELEASE.jar:5.1.8.RELEASE] + [...] +Caused by: java.lang.IllegalStateException: Could not find TLS ALPN provider; no working netty-tcnative, Conscrypt, or Jetty NPN/ALPN available + at io.grpc.netty.GrpcSslContexts.defaultSslProvider(GrpcSslContexts.java:258) ~[grpc-netty-1.21.0.jar:1.21.0] + at io.grpc.netty.GrpcSslContexts.configure(GrpcSslContexts.java:171) ~[grpc-netty-1.21.0.jar:1.21.0] + at io.grpc.netty.GrpcSslContexts.forServer(GrpcSslContexts.java:130) ~[grpc-netty-1.21.0.jar:1.21.0] + [...] +```` + +### Client-side + +````txt +[...] +Caused by: java.lang.IllegalStateException: Failed to create channel: + at net.devh.boot.grpc.client.inject.GrpcClientBeanPostProcessor.processInjectionPoint(GrpcClientBeanPostProcessor.java:118) ~[grpc-client-spring-boot-autoconfigure-2.4.0.RELEASE.jar:2.4.0.RELEASE] + at net.devh.boot.grpc.client.inject.GrpcClientBeanPostProcessor.postProcessBeforeInitialization(GrpcClientBeanPostProcessor.java:77) + [...] +Caused by: java.lang.IllegalStateException: Could not find TLS ALPN provider; no working netty-tcnative, Conscrypt, or Jetty NPN/ALPN available + at io.grpc.netty.GrpcSslContexts.defaultSslProvider(GrpcSslContexts.java:258) ~[grpc-netty-1.21.0.jar:1.21.0] + at io.grpc.netty.GrpcSslContexts.configure(GrpcSslContexts.java:171) ~[grpc-netty-1.21.0.jar:1.21.0] + at io.grpc.netty.GrpcSslContexts.forClient(GrpcSslContexts.java:120) ~[grpc-netty-1.21.0.jar:1.21.0] + [...] +```` + +### Both sides + +````txt +AbstractMethodError: io.netty.internal.tcnative.SSL.readFromSSL() +```` + +### The Problem + +There is no (compatible) netty TLS implementation available on the classpath. + +### The Solution + +Either switch from [`grpc-netty`](https://mvnrepository.com/artifact/io.grpc/grpc-netty) to [`grpc-netty-shaded`](https://mvnrepository.com/artifact/io.grpc/grpc-netty-shaded) +or add a dependency to [`netty-tcnative-boringssl-static`](https://mvnrepository.com/artifact/io.netty/netty-tcnative-boringssl-static) +(Please use the **exact same** (compatible) versions that are listed in the table in [grpc-java's netty security section](https://github.com/grpc/grpc-java/blob/master/SECURITY.md#netty). + +> **Note:** You need a 64bit Java JVM. + +## Dismatching certificates + +### Client-side + +````txt +io.grpc.StatusRuntimeException: UNAVAILABLE: io exception +[...] +Caused by: javax.net.ssl.SSLHandshakeException: General OpenSslEngine problem +[...] +Caused by: java.security.cert.CertificateException: No subject alternative names present +```` + +or + +````txt +io.grpc.StatusRuntimeException: UNAVAILABLE: io exception +[...] +Caused by: javax.net.ssl.SSLHandshakeException: General OpenSslEngine problem +[...] +Caused by: java.security.cert.CertificateException: No name matching found +```` + +### The Problem + +The certificate does not match the target's address/name. + +### The Solution + +Configure an override for the name comparison by adding the following to your client config: + +````properties +grpc.client.__name__.security.authorityOverride= +```` + +## Untrusted certificates + +### Client-side + +````txt +io.grpc.StatusRuntimeException: UNAVAILABLE: io exception +[...] +Caused by: javax.net.ssl.SSLHandshakeException: General OpenSslEngine problem +[...] +Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target +[...] +Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target +```` + +### The Problem + +The certificate used by the server is not in the trust store of the client. + +### The Solution + +Either add the certificate to java's truststore by using java's `keytool` +or configure the client to use a custom trusted certificate file: + +````properties +grpc.client.__name__.security.trustCertCollection=file:certificates/trusted-servers-collection.crt.list +```` + +> **Note:** Both stores are currently read only at creation time and updates won't be picked up. + +## Server port already in use + +### Server-side + +````txt +Caused by: java.lang.IllegalStateException: Failed to start the grpc server + at net.devh.boot.grpc.server.serverfactory.GrpcServerLifecycle.start(GrpcServerLifecycle.java:51) ~[grpc-server-spring-boot-autoconfigure-2.4.0.RELEASE.jar:2.4.0.RELEASE] + [...] +Caused by: java.io.IOException: Failed to bind + at io.grpc.netty.shaded.io.grpc.netty.NettyServer.start(NettyServer.java:246) ~[grpc-netty-shaded-1.21.0.jar:1.21.0] + at io.grpc.internal.ServerImpl.start(ServerImpl.java:177) ~[grpc-core-1.21.0.jar:1.21.0] + at io.grpc.internal.ServerImpl.start(ServerImpl.java:85) ~[grpc-core-1.21.0.jar:1.21.0] + at net.devh.boot.grpc.server.serverfactory.GrpcServerLifecycle.createAndStartGrpcServer(GrpcServerLifecycle.java:90) ~[grpc-server-spring-boot-autoconfigure-2.4.0.RELEASE.jar:2.4.0.RELEASE] + at net.devh.boot.grpc.server.serverfactory.GrpcServerLifecycle.start(GrpcServerLifecycle.java:49) ~[grpc-server-spring-boot-autoconfigure-2.4.0.RELEASE.jar:2.4.0.RELEASE] + ... 13 common frames omitted +Caused by: java.net.BindException: Address already in use: bind +```` + +### The Problem + +The port the grpc server is trying to use is already used. + +There are four common cases where this error might occur. + +1. The application is already running +2. Another application is using that port +3. The grpc server uses a port that is already used for something else (e.g. spring-web) +4. You are running tests and spring didn't shutdown the grpc-server after each test + +### The Solution + +1. Try searching for the application using the task manager or `jps` +2. Try searching for the port using `netstat` +3. Check/Change your configuration. This library uses port `9090` by default +4. Adding `@DirtiesContext` to your test classes and methods + Please note that the error will only occur from the second test onwards, + so you have to annotate the first one as well! + +## Client fails to resolve domain name + +### Client-side + +````txt +WARN io.grpc.internal.ManagedChannelImpl - [Failed to resolve name. status=Status{code=UNAVAILABLE, description=No servers found for `discovery-server:443`} +ERROR n.d.b.g.c.n.DiscoveryClientNameResolver - No servers found for `discovery-server:443` +```` + +### The Problem + +The discovery service library or it's configuration failed to specify the scheme how `discovery-server:443` should be +resolved. If you don't have a service discovery, then the default is `dns`, but if you use a discovery service, then +that will be the default and thus failing to resolve that address. + +The same applies to other libraries, such as tracing or reporting libraries, which report their results via grpc to an +external server. + +### The Solution + +- Configure the (discovery service) library to specify the `dns` scheme: + e.g. `dns:///discovery-server:443` +- Search for invocations of `ManagedChannelBuilder#forTarget(String)` or `NettyChannelBuilder#forTarget(String)` + (or similar methods) and make sure they use the `dns` scheme. +- Disable the service discovery for grpc services: + `spring.autoconfigure.exclude=net.devh.boot.grpc.client.autoconfigure.GrpcDiscoveryClientAutoConfiguration` +- or create a custom `NameResolverRegistry` bean + +See also [client target configuration](client/configuration.md#choosing-the-target). + +## Creating issues + +Creating issues/asking questions on GitHub isn't hard, but with a little bit of your effort you can help us solving your +issues faster. + +If your issue/question is about grpc in general consider asking it over at +[grpc-java](https://github.com/grpc/grpc-java). + +Use the provided templates to create new issues, these contain sections for the required/helpful information we need. + +In general, you should include the following information in your issue: + +1. What type of request do you have? + - Question + - Bug-report + - Feature-Request +2. What do you wish to achieve? +3. What's The Problem? What's not working? What's missing and why do you need it? +4. Any relevant stacktraces/logs (very important) +5. Which versions do you use? + - Spring (boot) + - grpc-java + - grpc-spring-boot-starter + - Other relevant libraries +6. Additional context + - Did it ever work before? + - How can we reproduce it? + - Do you have a demo? + +---------- + +[<- Back to Index](index.md) diff --git a/docs/en/versions.md b/docs/en/versions.md new file mode 100644 index 000000000..bce070bde --- /dev/null +++ b/docs/en/versions.md @@ -0,0 +1,100 @@ +# Versions + +[<- Back to Index](index.md) + +This page shows the additional information about our Versioning Policy and lifecycles. + +## Table of Contents + +- [Versioning Policy](#versioning-policy) +- [Version Table](#version-table) + - [Version 2.x](#version-2x) + - [Version 1.x](#version-1x) + - [Upgrading Dependencies](#upgrading-dependencies) + - [Release Notes](#release-notes) + +## Versioning Policy + +The major version of this project defines which spring-boot version we are compatible with. + +- 1.x.x versions are EOL and won't receive any updates +- 2.x.x is the current version and will be updated if there are spring-boot or gRPC releases. + +The minor version defines the feature version of this project. Every time we bump spring-boot's or gRPC's version, +we will also increment our feature version. The same applies if we add/change major features. +In most cases you will not get any incompatibilities by upgrading, but since gRPC evolves just like its API, +this cannot be ruled out. We try to minimize such influences, but can't rule them out. +If you don't use advanced features, you won't usually notice. + +We usually don't release patch versions, but include these patches in the next release. +If you need a patched version, please open an issue. + +## Version Table + +This table shows the spring and gRPC version that this library ships. +In most cases you can upgrade to newer versions, but especially gRPC changes its API more frequently. +Please report any issues to our [repo](https://github.com/yidongnan/grpc-spring-boot-starter/issues). + +> **Note** +> +> If you are using the non-shaded netty (and related libraries) please stick **exactly** to the version that is +> [documented](https://github.com/grpc/grpc-java/blob/master/SECURITY.md#netty) by gRPC. +> (grpc-netty-shaded avoids these issues by keeping these versions in sync.) + +### Version 2.x + +Current version. + +| Version | spring-boot | gRPC | Date | +|:-------:|:-----------:|:----:| ---: | +| 2.13.0* | 2.4.5 | 1.37.0 | TBA | +| 2.12.0 | 2.4.5 | 1.37.0 | Mai, 2021 | +| 2.11.0 | 2.3.8 | 1.35.0 | Feb, 2021 | +| 2.10.1 | 2.3.3 | 1.31.1 | Aug, 2020 | +| 2.10.0 | 2.3.3 | 1.31.1 | Aug, 2020 | +| 2.9.0 | 2.3.1 | 1.30.0 | Jun, 2020 | +| 2.8.0 | 2.2.7 | 1.29.0 | Jun, 2020 | +| 2.7.0 | 2.2.4 | 1.27.1 | Feb, 2020 | +| 2.6.2 | 2.2.1 | 1.25.0 | Jan, 2020 | +| 2.6.1 | 2.2.1 | 1.25.0 | Nov, 2019 | +| 2.6.0 | 2.2.1 | 1.24.2 | Nov, 2019 | +| 2.5.1 | 2.1.6 | 1.22.2 | Aug, 2019 | +| 2.5.0 | 2.1.6 | 1.22.1 | Aug, 2019 | +| 2.4.0 | 2.1.5 | 1.20.0 | Jun, 2019 | +| 2.3.0 | 2.1.4 | 1.18.0 | Apr, 2019 | +| 2.2.1 | 2.0.7 | 1.17.1 | Jan, 2019 | +| 2.2.0 | 2.0.6 | 1.17.1 | Dec, 2018 | +| 2.1.0 | 2.0.? | 1.14.0 | Oct, 2018 | +| 2.0.1 | 2.0.? | 1.14.0 | Aug, 2018 | +| 2.0.0 | 2.0.? | 1.13.1 | Aug, 2018 | + +(* Future versions) + +### Version 1.x + +End of life - No more updates planned. + +| Version | spring-boot | gRPC | Date | +|:-------:|:-----------:|:----:| ---: | +| 1.4.2 | 1.?.? | 1.12.0 | Jun, 2019 | +| 1.4.1 | 1.?.? | 1.12.0 | Jun, 2018 | +| ... | 1.?.? | N/A | + +### Upgrading Dependencies + +If you upgrade any of the versions we strongly recommend doing so using a bom: + +- [spring-boot](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-parent) +- [grpc-java](https://mvnrepository.com/artifact/io.grpc/grpc-bom) + +### Release Notes + +Refer to the release notes for more information on the changes for each version: + +- [grpc-spring-boot-starter](https://github.com/yidongnan/grpc-spring-boot-starter/releases) +- [spring-boot](https://github.com/spring-projects/spring-boot/releases) +- [grpc-java](https://github.com/grpc/grpc-java/releases) + +---------- + +[<- Back to Index](index.md) diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..4647ac8b6 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,6 @@ +# gRPC-Spring-Boot-Starter Documentation + +Please select a language: + +- [English](en) +- [中文](zh-CN) diff --git a/docs/zh-CN/actuator.md b/docs/zh-CN/actuator.md new file mode 100644 index 000000000..3aab93788 --- /dev/null +++ b/docs/zh-CN/actuator.md @@ -0,0 +1,145 @@ +# 支持 Spring Boot Actuator + +[<- 返回索引](index.md) + +此页面重点介绍与 [Spring-Boot-Actator](https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html) 的集成。 这是一个可选的功能。 支持的特性 + +- 客户端 + 服务端指标 +- 服务端的`InfoContributor` + +## 目录 + +- [依赖项](#dependencies) +- [指标](#metrics) + - [计数器](#counter) + - [计时器](#timer) + - [查看指标](#viewing-the-metrics) + - [指标配置](#metric-configuration) +- [InfoContributor](#infocontributor) +- [关闭指标功能](#opt-out) + +## 依赖项 + +指标收集和其他执行器一样都是可选的,如果应用程序环境中有 `MeterRegistry` ,它们将自动启用。 + +您可以简单地通过向Maven添加以下依赖来实现这一点: + +````xml + + org.springframework.boot + spring-boot-starter-actuator + +```` + +Gradle: + +````groovy +compile("org.springframework.boot:spring-boot-starter-actuator") +```` + +> **注意:** 在大多数情况下,您还需要 `spring-boot-web` 依赖才能实际查看到指标。 请注意,spring-boot-web 运行的端口于不同的 grpc 服务端 (通常是 `8080`)。 如果您不想添加添加一个 web-server,您仍然可以通过 JMX (如果启用的话) 访问这些指标。 + +## 指标 + +一旦依赖关系被添加,grpc-spring-boot-starter 将自动配置`ClientIntercertor` / `ServerInterceptor` 以收集指标。 + +### 计数器 + +- `grpc.client.requests.sent`: 发送的总请求数。 +- `grpc.client.responses.received`: 接受的总响应数。 +- `grpc.server.requests.received`: 收到的总请求数。 +- `grpc.server.responses.sent`: 发送的总响应数。 + +**标签** + +- `service`: 请求的 grpc 服务名称(使用 protubuf 名称) +- `method`: 请求的 grpc 方法名称(使用 protobuf 名称) +- `methodType`: 请求的 grpc 方法的类型。 + +### 计时器 + +- `grpc.client.processing.duration`: 客户端完成请求所花费的总时间,包括网络延迟。 +- `grpc.server.processing.duration`: 服务端完成请求所花费的时间。 + +**标签** + +- `service`: 请求的 grpc 服务名称(使用 protobuf 名称) +- `method`: 请求的 grpc 方法名称(使用 protobuf 名称) +- `methodType`: 请求的 grpc 方法的类型。 +- `statusCode`: 响应的 `Status.Code` + +### 查看指标 + +您可以在 `/actorator/metrics` (需要一个web-server) 或通过 JMX 查看 grpc 的指标以及其他指标。 + +> **注意:** 你可能需要先启用指标。 +> +> ````properties management.endpoints.web.exposure.include=metrics +> +> # management.endpoints.jmx.exposure.include=metrics +> +> management.endpoint.metrics.enabled=true ```` + +阅读官方文档以了解更多关于[Spring Boot Actuator](https://docs.spring.io/spring-boot/docs/current/reference/html/production-ready-endpoints.html) 的信息。 + +### 指标配置 + +默认情况下,客户端只会为已进行的请求创建指标。 然而,服务端将尝试所有找到并注册的服务,来初始化它们的指标。 + +您可以通过覆盖Bean的创建自定义的行为。 下面使用`MetricCollectingClientInterceptor`来展示这一点: + +````java +@Bean +MetricCollectingClientInterceptor metricCollectingClientInterceptor(MeterRegistry registry) { + MetricCollectingClientInterceptor collector = new MetricCollectingClientInterceptor(registry, + counter -> counter.tag("app", "myApp"), // Customize the Counters + timer -> timer.tag("app", "myApp"), // Customize the Timers + Code.OK, Code.INVALID_ARGUMENT, Code.UNAUTHENTICATED); // Eagerly initialized status codes + // Pre-generate metrics for some services (to avoid missing metrics after restarts) + collector.preregisterService(MyServiceGrpc.getServiceDescriptor()); + return collector; +} +```` + +## InfoContributor + +*仅限服务器* + +服务端会自动配置一个 [`InfoContributor`](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/actuate/info/InfoContributor.html) 并公开一下信息: + +- `grpc.server`: + - `port`: grpc 服务端的端口 + - `services`: grpc 的服务列表 + - 方法 + +您可以在 `/actorator/info` (需要一个web-server) 或通过 JMX 查看 grpc 的信息以及其他信息。 + +> **注意:** 你可能需要先启用信息。 +> +> ````properties management.endpoints.web.exposure.include=info +> +> # management.endpoints.jmx.exposure.include=info +> +> management.endpoint.info.enabled=true ```` + +您可以使用 `grpc.server.reflectionServiceEnabled=false` 来打开服务列表(对于 actuator 和 grpc)。 + +## 关闭指标功能 + +您可以选择退出自动配置,使用以下注解: + +````java +@EnableAutoConfiguration(exclude = {GrpcClientMetricAutoConfiguration.class, GrpcServerMetricAutoConfiguration.class}) +```` + +或使用配置: + +````properties +spring.autoconfigure.exclude=\ +net.devh.boot.grpc.client.autoconfigure.GrpcClientMetricAutoConfiguration,\ +net.devh.boot.grpc.server.autoconfigure.GrpcServerMetricAutoConfiguration +```` + +---------- + +[<- 返回索引](index.md) diff --git a/docs/zh-CN/benchmarking.md b/docs/zh-CN/benchmarking.md new file mode 100644 index 000000000..20b1a0f55 --- /dev/null +++ b/docs/zh-CN/benchmarking.md @@ -0,0 +1,22 @@ +# 基准测试 + +[<- 返回索引](index.md) + +这个项目不会给 gRPC-java 增加任何性能开销。 + +请参考官方的 gRPC 基准测试: + +- [grpc.io: Benchmarking](https://grpc.io/docs/guides/benchmarking/) +- [grpc-java: Running Benchmarks](https://github.com/grpc/grpc-java/tree/master/benchmarks#grpc-benchmarks) + +与纯 HTTP 相比,gRPC 具有很多优势,但是很难将它数字化。 经过高度优化的 Web 服务器的性能与普通的 gRPC 服务器可能一样好。 + +下面是普通HTTP与 grpc 的主要优点/差异: + +- 二进制数据格式 (更快, 但不可读) +- Protobuf 定义的数据结构,可以用于为许多语言生成数据类和客户端。 +- HTTP/2 连接和连接池 + +---------- + +[<- 返回索引](index.md) diff --git a/docs/zh-CN/brave.md b/docs/zh-CN/brave.md new file mode 100644 index 000000000..a28eccf41 --- /dev/null +++ b/docs/zh-CN/brave.md @@ -0,0 +1,72 @@ +# 支持 Brave / Sleuth + +[<- 返回索引](index.md) + +此页面将着重介绍与 [Brave](https://github.com/openzipkin/brave) / [Sleuth](https://spring.io/projects/spring-cloud-sleuth) 的集成。 这是一个可选的功能。 + +## 目录 + +- [依赖项](#dependencies) + - [Brave](#brave) + - [Spring Cloud Sleuth](#spring-cloud-sleuth) +- [关闭链路跟踪](#opt-out) +- [附加信息](#additional-notes) + +## 依赖项 + +grpc-spring-boot-starter 支持为 `Brave Instrumentation:GRPC` 提供自动配置。 然而,有两个要求: + +1. 您需要在 classpath 添加 Brave 依赖项。 +2. 您需要在应用上下文中有一个 `Trace` bean。 *如果您的 classpath 有 Spring Cloud Sleuth,它将自动为您配置此 Bean* + +### Brave + +您可以在 Maven 中添加 Brave 的依赖项。 + +````xml + + io.zipkin.brave + brave-instrumentation-grpc + +```` + +Gradle: + +````groovy +compile("io.zipkin.brave:brave-instrumentation-grpc") +```` + +### Spring Cloud Sleuth + +您可以在 Maven 中添加 Sleuth 依赖 + +````xml + + org.springframework.cloud + spring-cloud-starter-sleuth + +```` + +Gradle: + +````groovy +compile("org.springframework.cloud:spring-cloud-starter-sleuth") +```` + +请参阅[官方文件](https://spring.io/projects/spring-cloud-sleuth)以了解如何设置 / 配置 Sleuth。 + +## 关闭链路跟踪 + +您可以使用以下属性关闭 grpc 的链路跟踪: + +````property +spring.sleuth.grpc.enabled=false +```` + +## 附加信息 + +Spring-Cloud-Sleuth 提供了一些类,例如[`SpringAwareManagedChannelBuilder`](https://javadoc.io/page/org.springframework.cloud/spring-cloud-sleuth-core/latest/org/springframework/cloud/sleuth/instrument/grpc/SpringAwareManagedChannelBuilder.html),这些类仅仅由于与其他的库兼容而存在。 不要跟那个项目一期使用。 grpc-spring-boot-starter 通过 [`GrpcChannelFactory`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/channelfactory/GrpcChannelFactory.html) 和相关类提供相同 / 扩展的功能提供了开箱即用的能力。 相关阅读 [sleuth's javadoc note](https://github.com/spring-cloud/spring-cloud-sleuth/blob/59216c32f7848ec337fb68d1dbec8e87eeb6bf59/spring-cloud-sleuth-core/src/main/java/org/springframework/cloud/sleuth/instrument/grpc/SpringAwareManagedChannelBuilder.java#L31-L34)。 + +---------- + +[<- 返回索引](index.md) diff --git a/docs/zh-CN/client/configuration.md b/docs/zh-CN/client/configuration.md new file mode 100644 index 000000000..673588e2c --- /dev/null +++ b/docs/zh-CN/client/configuration.md @@ -0,0 +1,114 @@ +# 配置 + +[<- 返回索引](../index.md) + +本节描述您如何配置您的 grpc-spring-boot-starter 客户端。 + +## 目录 + +- [通过属性配置](#configuration-via-properties) + - [选择目标](#choosing-the-target) +- [通过Beans 配置](#configuration-via-beans) + - [GrpcChannelConfigurer](#grpcchannelconfigurer) + - [StubTransformer](#stubtransformer) + +## 附加主题 + +- [入门指南](getting-started.md) +- *配置* +- [安全性](security.md) + +## 通过属性配置 + +grpc-spring-boot-starter 可以通过 Spring 的 `@ConfigurationProperties` 机制来进行 [配置](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html) + +您可以在这里找到所有内建配置属性: + +- [`GrpcChannelsProperties`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/config/GrpcChannelsProperties.html) +- [`GrpcChannelProperties`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/config/GrpcChannelProperties.html) +- [`GrpcServerProperties.Security`](https://static.javadoc.io/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/config/GrpcChannelProperties.Security.html) + +如果你希望阅读源代码,你可以查阅 [这里](https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java#L58)。 + +Channels 的属性都是以 `grpc.client.__name__.` 或 `grpc.client.__name__.security.` 为前缀。 Channel 的名称从 `@GrpcClient` 注解中获取。 如果您想要配置一些其他的选项,如为所有服务端设置可信证书,并可以使用 `GLOBAL` 作为名称。 单个 channel 的属性配置会覆盖全局配置。 + +### 选择目标 + +您可以使用以下属性指定目标服务器: + +````properties +grpc.client.__name__.address=static://localhost:9090 +```` + +目标服务器支持多种 schemes,您可以使用它们来指定目标服务器(优先级0(低) - 10(高)): + +- `static`(优先级 4): 一个简单的IP静态列表 (v4 和 v6), 可用于连接到服务端 (支持 `localhost`)。 例如:`static://192.168.1:8080,10.0.0.1:1337` +- [`dns`](https://github.com/grpc/grpc-java/blob/master/core/src/main/java/io/grpc/internal/DnsNameResolver.java#L66)(优先级 5):解析并绑定到给定 DNS 名称的所有地址。 地址将被缓存,只有当现有连接被关闭 / 失败时才会刷新。 更多选项,例如 `SVC` 查找(对 kubernetes 有用),可以通过系统属性启用。 例如:`dns:///example.my.company` +- `discovery` (优先级 6):(可选) 使用 Spring Cloud 的`DiscoveryClient` 去查找合适的目标。 在 `HeartbeatEvent` 的时候,连接将自动刷新。 使用 `gRPC_port` 元数据来确定端口,否则使用服务端口。 示例: `discovery:///service-name` +- `self`(优先级 0):如果您使用 `grpc-server-spring-boot-starter` 并且不想指定自己的地址 / 端口,则可以使用 self 关键词作为 scheme 或者 address 。 这对于需要使用随机服务器端口以避免冲突的测试特别有用。 例如:`self`或`self:self` +- `in-process`:这是一个特殊的方案,将使用 `InProcessChannelFactory` 来替代原有正常的 ChannelFactory。 并使用它连接到 [`InProcessServer`](../server/configuration.md#enabling-the-inprocessserver)。 例如:`in-process:foobar` +- *custom*: 您可以通过 Java 的 `ServiceLoader` 或从 Spring 的应用上下文中选择要自定义的 [`NameResolverProvider`](https://javadoc.io/page/io.grpc/grpc-all/latest/io/grpc/NameResolverProvider.html) ,并将其注册到 `NameResolverRegistry` 上. + +如果您没有定义地址,它将按照如下方式被猜测: + +- 首先它将尝试使用它的名称 (``) +- 如果您配置了默认方案,它将尝试下一个 (`:`) +- 然后它将使用 `NameResolver.Factory` 委托的默认方案(见上面的优先级) + +> **注意:** 斜杠的数量很重要! 还要确保不要连接到普通的 web / REST / 非 grpc 服务器(端口)。 + +[客户端安全性](security.md) 页面上解释了 `SSL` / `TLS` 和其他与安全相关的配置。 + +## 通过Beans 配置 + +虽然这个项目提供大多数功能作为配置选项,但有时会因为添加它的开销太高了,我们会选择没有添加它。 如果您觉得这是一项重要功能,请随意打开一项功能性 Pull Request。 + +如果您要更改应用程序,而不是通过属性进行更改,则可以使用该项目中现有存在的扩展点。 + +首先,大多数 bean 可以被自定义 bean 替换,您可以按照您想要的任何方式进行配置。 如果您不希望这么麻烦,可以使用 `GrpcChannelConfigurer` 和 `StubTransformer` 等类来配置 channels,stubs 和其他组件,它不会丢失这个项目所提供的任何功能。 + +### GrpcChannelConfigurer + +gRPC 客户端配置器允许您将自定义配置添加到 gRPC 的 `ManagedChannelBuilder` 。 + +````java +@Bean +public GrpcChannelConfigurer keepAliveClientConfigurer() { + return (channelBuilder, name) -> { + if (channelBuilder instanceof NettyChannelBuilder) { + ((NettyChannelBuilder) channelBuilder) + .keepAliveTime(30, TimeUnit.SECONDS) + .keepAliveTimeout(5, TimeUnit.SECONDS); + } + }; +} +```` + +> 注意,根据您的配置,在应用上下文中可能有不同类型的 `ManagedChannelBuilder` (例如`InProcessChannelFactory`)。 + +### StubTransformer + +StubTransformer 允许您在注入您的 Bean 之前修改`Stub`。 + +````java +@Bean +public StubTransformer call() { + return (name, stub) -> { + if ("serviceA".equals(name)) { + return stub.withWaitForReady(); + } else { + return stub; + } + }; +} +```` + +## 附加主题 + +- [入门指南](getting-started.md) +- *配置* +- [安全性](security.md) + +---------- + +[<- 返回索引](../index.md) diff --git a/docs/zh-CN/client/getting-started.md b/docs/zh-CN/client/getting-started.md new file mode 100644 index 000000000..b58d48d1e --- /dev/null +++ b/docs/zh-CN/client/getting-started.md @@ -0,0 +1,144 @@ +# 入门指南 + +[<- 返回索引](../index.md) + +本节讨论如何让 Spring 连接到 gRPC 服务端并管理您的连接。 + +## 目录 + +- [项目创建](#project-setup) +- [依赖项](#dependencies) + - [接口项目](#interface-project) + - [服务端项目](#server-project) + - [客户端项目](#client-project) +- [使用 Stubs 连接服务端](#using-the-stubs-to-connect-to-the-server) + - [解释客户端组件](#explaining-the-client-components) + - [访问客户端](#accessing-the-client) + +## 附加主题 + +- *入门指南* +- [配置](configuration.md) +- [安全性](security.md) + +## 项目创建 + +在我们开始添加依赖关系之前,让我们项目的一些设置建议开始。 + +![project setup](/grpc-spring-boot-starter/assets/images/client-project-setup.svg) + +我们建议将您的项目分为2至3个不同的模块。 + +1. **interface 项目** 包含原始 protobuf 文件并生成 java model 和 service 类。 你可能会在不同的项目中会共享这个部分。 +2. **Server 项目** 包含项目的业务实现,并使用上面的 Interface 项目作为依赖项。 +3. **Client 项目**(可选,可能很多) 任何使用预生成的 stub 来访问服务器的客户端项目。 + +## 依赖项 + +### 接口项目 + +请参阅 [服务端入门指引](../server/getting-started.md#interface-project) 页面 + +### 服务端项目 + +请参阅 [服务端入门指引](../server/getting-started.md#server-project) 页面 + +### 客户端项目 + +#### Maven (客户端) + +````xml + + + net.devh + grpc-client-spring-boot-starter + + + + example + my-grpc-interface + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + +```` + +#### Gradle (客户端) + +````gradle +apply plugin: 'org.springframework.boot' + +dependencies { + compile('org.springframework.boot:spring-boot-starter') + compile('net.devh:grpc-client-spring-boot-starter') + compile('my-example:my-grpc-interface') +} + +buildscript { + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} + +```` + +## 使用 Stubs 连接服务端 + +本节假定您已经定义并生成了[Protobuf](../server/getting-started.md#creating-the-grpc-service-definitions)。 + +### 解释客户端组件 + +- [`Channel`](https://javadoc.io/page/io.grpc/grpc-all/latest/io/grpc/Channel.html): Channel 是单个服务端的连接池。 目标服务器可能是多个 gRPC 服务。 +- [`ManagedChannel`](https://javadoc.io/page/io.grpc/grpc-all/latest/io/grpc/ManagedChannel.html): ManagedChannel 是 Channel 的一种特殊变体,因为它允许对连接池进行管理操作,例如将其关闭。 +- [`ClientInterceptor`](https://javadoc.io/page/io.grpc/grpc-all/latest/io/grpc/ClientInterceptor.html): 在每个 `Channel` 处理之前拦截它们。 可以用于日志、监测、元数据处理和请求/响应的重写。 grpc-spring-boot-starter 将自动接收所有带有 [`@GrpcGlobalClientInterceptor`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/interceptor/GrpcGlobalClientInterceptor.html) 注解以及手动注册在[`GlobalClientInterceptorRegistry`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/interceptor/GlobalClientInterceptorRegistry.html) 上的客户拦截器。 +- [`CallCredentials`](https://javadoc.io/page/io.grpc/grpc-all/latest/io/grpc/CallCredentials.html): 管理身份验证的组件。 它可以用于存储凭据和会话令牌。 它还可以用来身份验证,并且使用返回的令牌(例如 OAuth )来授权实际请求。 除此之外,如果令牌过期并且重新发送请求,它可以续签令牌。 如果您的应用程序上下文中只存在一个 `CallCredentials` bean,那么 spring 将会自动将其附加到`Stub`( **非** `Channel` )。 [`CallCredentialsHelper`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/security/CallCredentialsHelper.html)工具类可以帮助您创建常用的 `CallCredentials` 类型和相关的`StubTransformer`。 +- [`StubTransformer`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/inject/StubTransformer.html): 所有客户端的 `Stub` 的注入之前应用的转换器。 +- [`@GrpcClient`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/inject/GrpcClient.html): 这个注解用在你需要注入客户端的字段或者 set 方法上。 支持 `Channel`和各种类型的 `Stub`。 请不要将 `@GrpcClient` 与 `@Autowireed` 或 `@Inject` 一起使用。 + +### 访问客户端 + +我们建议注入 (`@GrpcClient`) `Stub`,而不是纯粹的 `Channel`. + +> **注意:** 存在不同类型的 `Stub`。 并非所有的都支持所有请求类型 (流式调用)。 + +````java +import example.HelloReply; +import example.HelloRequest; +import example.MyServiceGrpc; + +import io.grpc.stub.StreamObserver; + +import net.devh.boot.grpc.server.service.GrpcService; + +@Service +public class FoobarService { + + @GrpcClient("myService") + private MyServiceBlockingStub myServiceStub; + + public String receiveGreeting(String name) { + HelloRequest request = HelloRequest.newBuilder() + .setName(name) + .build() + return myServiceStub.sayHello(request).getMessage(); + } + +} +```` + +## 附加主题 + +- *入门指南* +- [配置](configuration.md) +- [安全性](security.md) + +---------- + +[<- 返回索引](../index.md) diff --git a/docs/zh-CN/client/security.md b/docs/zh-CN/client/security.md new file mode 100644 index 000000000..af9a5a40f --- /dev/null +++ b/docs/zh-CN/client/security.md @@ -0,0 +1,115 @@ +# 客户端安全 + +[<- 返回索引](../index.md) + +此页面描述了您如何连接到 gRPC 服务器并进行身份验证。 + +## 目录 + +- [启用传输图层安全](#enable-transport-layer-security) + - [基础要求](#prerequisites) +- [禁用传输图层安全](#disable-transport-layer-security) + - [信任服务器](#trusting-a-server) +- [双向证书认证](#mutual-certificate-authentication) +- [身份验证](#authentication) + +## 附加主题 + +- [入门指南](getting-started.md) +- [配置](configuration.md) +- *安全性* + +## 启用传输图层安全 + +gRPC 默认使用 `TLS` 连接服务端,因此无需执行其他任何操作。 + +如果您想要检查您是否意外覆盖配置, 请检查给定的属性有这样配置: + +````properties +grpc.client..negotiationType=TLS +```` + +对于服务端的配置,请参考 [服务端安全](../server/security.md) 页面。 + +### 基础要求 + +如同往常一样,需要满足一些简单的前提条件: + +- 在您的 classpath 上有兼容的 `SSL `/`TLS` 实现 + - 包含 [grpc-netty-shaded](https://mvnrepository.com/artifact/io.grpc/grpc-netty-shaded) + - 对于[`grpc-netty`](https://mvnrepository.com/artifact/io.grpc/grpc-netty),还需要额外添加 [`nety-tcnative-boringssl-static`](https://mvnrepository.com/artifact/io.netty/netty-tcnative-boringssl-static) 依赖。 (请使用 [grpc-java的 Netty 安全部分](https://github.com/grpc/grpc-java/blob/master/SECURITY.md#netty) 表中列出**完全相同** (兼容)的版本)。 + +## 禁用传输图层安全 + +> **警告:** 请勿在生产环境中这样做。 + +有时您没有可用的证书(例如在开发期间),因此您可能希望禁用传输层安全,您可以这样做: + +````properties +grpc.client.__name__.negotiationType=PLAINTEXT +```` + +下面的示例演示如何在测试中配置此属性: + +````java +@SpringBootTest(properties = "grpc.client.test.negotiationType=PLAINTEXT") +@SpringJUnitConfig(classes = TestConfig.class) +@DirtiesContext +public class PlaintextSetupTest { + + @GrpcClient("test") + private MyServiceBlockingStub myService; +```` + +### 信任服务端 + +如果您信任的证书不在常规信任存储区, 或者您想要限制您信任的 证书。您可以使用以下属性: + +````properties +grpc.client.__name__.security.trustCertCollection=file:trusted-server.crt.collection +```` + +如果您想知道这里支持哪些选项,请阅读 [Spring 的 Resource 文档](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#resources-resourceloader)。 + +如果您使用服务标识符,证书可能会出现问题,因为它对内部服务名称无效。 在这种情况下,您可以需要指定证书对哪个名字有效: + +````properties +grpc.client.__name__.security.authorityOverride=localhost +```` + +## 双向证书认证 + +在安全环境中,您可能必须使用客户端证书进行身份验证。 该证书通常由服务端提供,因此您需要为您的应用程序配置如下属性: + +````properties +grpc.client.__name__.security.clientAuthEnabled=true +grpc.client.__name__.security.certificateChain=file:certificates/client.crt +grpc.client.__name__.security.privateKey=file:certificates/client.key +```` + +## 身份验证 + +除了双向证书认证外,还有其他几种认证方式,如 `BasicAuth`。 + +grpc-spring-boot-starter 除了一些帮助方法,同时提供了 BasicAuth 的实现。 然而,这里有很多库可以为 [`CallCredentials`](https://grpc.github.io/grpc-java/javadoc/io/grpc/CallCredentials.html)提供实现功能。 `CallCredentials` 是一个可扩展的组件,因为它们可以使用(第三方)服务队请求进行身份验证,并且可以自己管理和更新会话 token。 + +如果您的应用程序上下文中只有一个`CallCredentials`,我们将自动为您创建一个 `StubTransformer`,并配置到所有的 `Stub`上。 如果您想为每个 Stub 配置不同的凭据,那么您可以使用 [`CallCredentialsHelper`](https://javadoc.io/page/net.devh/grpc-client-spring-boot-autoconfigure/latest/net/devh/boot/grpc/client/security/CallCredentialsHelper.html) 中提供的帮助方法。 + +> **注意:** `StubTransformer` 只能自动配置注入的 `Stub`。 它们无法修改原始的 `Channel`。 + +您还可以配置 `CallCredentials`(例如用于用户的凭据): + +````java +MyServiceBlockingStub myServiceForUser = myService.withCallCredentials(userCredentials); +return myServiceForUser.send(request); +```` + +## 附加主题 + +- [入门指南](getting-started.md) +- [配置](configuration.md) +- *安全性* + +---------- + +[<- 返回索引](../index.md) diff --git a/docs/zh-CN/contributions.md b/docs/zh-CN/contributions.md new file mode 100644 index 000000000..3193ec41d --- /dev/null +++ b/docs/zh-CN/contributions.md @@ -0,0 +1,13 @@ +# 参与贡献 + +[<- 返回索引](index.md) + +我们始终欢迎您对项目作出任何贡献。 + +详情请参阅我们的[贡献准则](https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/CONTRIBUTING.md)。 + +**感谢我们所有的[贡献者](https://github.com/yidongnan/grpc-spring-boot-starter/graphs/contributors)!** + +---------- + +[<- 返回索引](index.md) diff --git a/docs/zh-CN/examples.md b/docs/zh-CN/examples.md new file mode 100644 index 000000000..aec32e321 --- /dev/null +++ b/docs/zh-CN/examples.md @@ -0,0 +1,40 @@ +# 示例 + +示例项目演示如何使用这些项目。 + +这些项目可以作为您自己的项目的模板。 我们使用它们来验证这个库在不同的环境中运行,我们不会在不受注意的情况下改变它的行为。 + +> **注意:** 如果您对这些项目有疑问,或者想要其他的示例,随时可以提出一个 [issue](https://github.com/yidongnan/grpc-spring-boot-starter/issues)。 + +## 本地示例 + +最简单的设置,使用本地服务端和一个或多个客户端 + +- [服务端](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples/local-grpc-server) +- [客户端](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples/local-grpc-client) +- [说明](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples#local-mode) + +## Cloud 示例 + +使用 eureka 服务发现的 Cloud 环境。 + +- [Eureka 服务](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples/cloud-eureka-server) +- [服务端](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples/cloud-grpc-server) +- [客户端](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples/cloud-grpc-client) +- [说明](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples#cloud-mode) + +## Cloud-Nacos 示例 + +使用 nacos 服务发现的 Cloud 环境。 + +- [服务端](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples/cloud-grpc-server-nacos) +- [客户端](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples/cloud-grpc-client-nacos) +- [说明](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples#cloud-mode) + +## 基础认证示例 + +演示了 grpc 跟 spring security 的设置。 为了简单起见,此设置使用 Basic 身份验证,但也可以为其使用其他身份验证机制。 + +- [服务端](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples/security-grpc-server) +- [客户端](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples/security-grpc-client) +- [说明](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/examples#with-basic-auth-security) diff --git a/docs/zh-CN/index.md b/docs/zh-CN/index.md new file mode 100644 index 000000000..a72000604 --- /dev/null +++ b/docs/zh-CN/index.md @@ -0,0 +1,24 @@ +# gRPC-Spring-Boot-Starter 文档 + +gRPC-Spring-Boot-Starter 将 [Google的开源高性能RPC框架](https://grpc.io) 与 [Spring Boot 进行整合](https://spring.io/projects/spring-boot) 该项目简化了 gRPC 服务端 / 客户端的设置,只需要为项目添加了一个依赖项,并在服务实现类 / 客户 (stub) 字段上添加了一个的注解。 这个项目提供的特性仍然能复用您使用 gRPC 的经验,并且允许您执行任何自定义操作。 + +## 目录 + +- 服务端 + - [入门指南](server/getting-started.md) + - [配置](server/configuration.md) + - [上下文数据 / Bean 的作用域](server/contextual-data.md) + - [测试服务](server/testing.md) + - [安全性](server/security.md) +- 客户端 + - [入门指南](client/getting-started.md) + - [配置](client/configuration.md) + - [安全性](client/security.md) +- 其他设置 +- [疑难解答](trouble-shooting.md) +- [示例项目](examples.md) +- [版本概述](versions.md) +- [支持 Spring Boot Actuator / Metrics](actuator.md) +- [支持 Brave-Tracing / Spring Cloud Sleuth](brave.md) +- [基准测试](benchmarking.md) +- [参与贡献](contributions.md) diff --git a/docs/zh-CN/server/configuration.md b/docs/zh-CN/server/configuration.md new file mode 100644 index 000000000..4f2bdb7af --- /dev/null +++ b/docs/zh-CN/server/configuration.md @@ -0,0 +1,108 @@ +# 配置 + +[<- 返回索引](../index.md) + +本节描述您如何配置您的 grpc-spring-boot-starter 应用程序。 + +## 目录 + +- [通过属性配置](#configuration-via-properties) + - [更改服务端端口](#changing-the-server-port) + - [启用 InProcessServer](#enabling-the-inprocessserver) +- [通过 Bean 配置](#configuration-via-beans) + - [GrpcServerConfigurer](#grpcserverconfigurer) + +## 附加主题 + +- [入门指南](getting-started.md) +- *配置* +- [上下文数据 / Bean 的作用域](contextual-data.md) +- [测试服务](testing.md) +- [安全性](security.md) + +## 通过属性配置 + +grpc-spring-boot-starter 可以通过 Spring 的 `@ConfigurationProperties` 机制来进行 [配置](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-external-config.html)。 + +您可以在这里找到所有内置配置属性: + +- [`GrpcServerProperties`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/config/GrpcServerProperties.html) +- [`GrpcServerProperties.Security`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/config/GrpcServerProperties.Security.html) + +如果你希望阅读源代码,你可以查阅 [这里](https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/config/GrpcServerProperties.java#L50)。 + +Channels 的属性都是以 `grpc.server..` 或 `grpc.client..security.` 为前缀。 + +### 更改服务端端口 + +如果您想要将 gRPC 服务端端口从默认值(`9090`) 更改为其他的端口,您可以这样做: + +````properties +grpc.server.port=80 +```` + +将端口设置为 `0` 以使用空闲的随机端口。 此功能用于部署了服务发现的服务和并行测试的场景。 + +> 请确保您不会与其他应用程序或其他端点发生冲突,如`spring-web`。 + +[服务端安全性](security.md) 页面上解释了 `SSL` / `TLS` 和其他与安全相关的配置。 + +### 启用 InProcessServer + +有时,您可能想要在自己的应用程序中调用自己的 grpc 服务。 您可以像调用其他任何 gRPC 服务端一样,您需要使用 grpc 的 `InProcessServer` 来节省网络间开销。 + +您可以使用以下属性将其打开: + +````properties +grpc.server.in-process-name= +# Optional: Turn off the external grpc-server +#grpc.server.port=-1 +```` + +这允许客户端在同一应用程序内使用以下配置连接到服务器: + +````properties +grpc.client.inProcess.address=in-process: +```` + +这对测试特别有用,因为他们不需要打开特定的端口,因此可以并发运行(在构建 服务器上)。 + +## 通过Beans 配置 + +虽然这个项目提供大多数功能作为配置选项,但有时会因为添加它的开销太高了,我们会选择没有添加它。 如果您觉得这是一项重要功能,请随意打开一项功能性 Pull Request。 + +如果您要更改应用程序,而不是通过属性进行更改,则可以使用该项目中现有存在的扩展点。 + +首先,大多数 bean 可以被自定义 bean 替换,您可以按照您想要的任何方式进行配置。 如果您不希望这么麻烦,可以使用 `GrpcServerConfigurer` 来配置你的服务端和其他组件,它不会丢失这个项目所提供的任何功能。 + +### GrpcServerConfigurer + +gRPC 服务端配置器允许您将自定义配置添加到 gRPC 的 `ServerBuilder` 。 + +````java +@Bean +public GrpcServerConfigurer keepAliveServerConfigurer() { + return serverBuilder -> { + if (serverBuilder instanceof NettyServerBuilder) { + ((NettyServerBuilder) serverBuilder) + .keepAliveTime(30, TimeUnit.SECONDS) + .keepAliveTimeout(5, TimeUnit.SECONDS) + .permitKeepAliveWithoutCalls(true); + } + }; +} +```` + +> 注意,根据您的配置,在应用程序上下文中可能有不同类型的 `ServerBuilder` (例如`InProcessServerBuilder`)。 + +## 附加主题 + +- [入门指南](getting-started.md) +- *配置* +- [上下文数据 / Bean 的作用域](contextual-data.md) +- [测试服务](testing.md) +- [安全性](security.md) + +---------- + +[<- 返回索引](../index.md) diff --git a/docs/zh-CN/server/contextual-data.md b/docs/zh-CN/server/contextual-data.md new file mode 100644 index 000000000..98a3d7290 --- /dev/null +++ b/docs/zh-CN/server/contextual-data.md @@ -0,0 +1,64 @@ +# 上下文数据 / Bean 的作用域 + +[<- 返回索引](../index.md) + +本节描述您如何保存请求上下文数据 / 每个请求的数据。 + +## 目录 + +- [警告语](#a-word-of-warning) +- [grpcRequest 作用域](#grpcrequest-scope) + +## 附加主题 + +- [入门指南](getting-started.md) +- [配置](configuration.md) +- *上下文数据 / Bean 的作用域* +- [测试服务](testing.md) +- [安全性](security.md) + +## 警告语 + +在 grpc-java 中,消息发送 / 请求处理中的不同阶段可能在不同的线程中运行。 流式调用中也是这样。 避免在您的`ServerIntercetor` 和 grpc 服务方法实现中(在整个 gRPC 上下文中)使用 `ThreadLocal`。 When it comes down to it, the preparation phase, every single message and the completion phase might run in different threads. 如果您想要在会话中存储数据,请使用 grpc 的 `Context` 或 `grpcRequest` 作用域。 + +## grpcRequest 作用域 + +该项目添加了一个`grpcRequest`,该功能类似于 Spring Web 的`request` 作用域。 它只适用于单个的请求。 + +首先需要用 `@Scope` 注解定义 Bean: + +````java +@Bean +@Scope(scopeName = "grpcRequest", proxyMode = ScopedProxyMode.TARGET_CLASS) +//@Scope(scopeName = GrpcRequestScope.GRPC_REQUEST_SCOPE_NAME, proxyMode = ScopedProxyMode.TARGET_CLASS) +ScopedBean myScopedBean() { + return new ScopedBean(); +} +```` + +> `proxyMode = TARGET_CLASS` 是必须的,除非在另一个 `grpcRequest` 作用域中配置了它. 请注意,这个`proxyMode` 不适用于 final 修饰的类和方法。 + +之后,您就可以像以前那样使用 Bean: + +````java +@Autowired +private ScopedBean myScopedBean; + +@Override +public void grpcMethod(Request request, StreamObserver responseObserver) { + responseObserver.onNext(myScopedBean.magic(request)); + responseObserver.onCompleted(); +} +```` + +## 附加主题 + +- [入门指南](getting-started.md) +- [配置](configuration.md) +- *上下文数据 / Bean 的作用域* +- [测试服务](testing.md) +- [安全性](security.md) + +---------- + +[<- 返回索引](../index.md) diff --git a/docs/zh-CN/server/getting-started.md b/docs/zh-CN/server/getting-started.md new file mode 100644 index 000000000..6549b4930 --- /dev/null +++ b/docs/zh-CN/server/getting-started.md @@ -0,0 +1,302 @@ +# 入门指南 + +[<- 返回索引](../index.md) + +本节描述了将您的应用程序接入 grpc-spring-boot-starter 项目的必要步骤。 + +## 目录 + +- [项目创建](#project-setup) +- [依赖项](#dependencies) + - [接口项目](#interface-project) + - [服务端项目](#server-project) + - [客户端项目](#client-project) +- [创建 gRPC 服务定义](#creating-the-grpc-service-definitions) +- [实现服务逻辑](#implementing-the-service) + +## 附加主题 + +- *入门指南* +- [配置](configuration.md) +- [上下文数据 / Bean 的作用域](contextual-data.md) +- [测试服务](testing.md) +- [安全性](security.md) + +## 项目创建 + +在我们开始添加依赖关系之前,让我们项目的一些设置建议开始。 + +![project setup](/grpc-spring-boot-starter/assets/images/server-project-setup.svg) + +我们建议将您的项目分为2至3个不同的模块。 + +1. **interface 项目** 包含原始 protobuf 文件并生成 java model 和 service 类。 你可能会在不同的项目中会共享这个部分。 +2. **Server 项目** 包含项目的业务实现,并使用上面的 Interface 项目作为依赖项。 +3. **Client 项目**(可选,可能很多) 任何使用预生成的 stub 来访问服务器的客户端项目。 + +## 依赖项 + +### 接口项目 + +#### Maven (Interface) + +````xml + + + io.grpc + grpc-stub + + + io.grpc + grpc-protobuf + + + + javax.annotation + javax.annotation-api + + + + + + + kr.motd.maven + os-maven-plugin + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier} + grpc-java + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + + + + compile + compile-custom + + + + + + +```` + +#### Gradle (Interface) + +````gradle +apply plugin: 'com.google.protobuf' + +dependencies { + compile "io.grpc:grpc-protobuf" + compile "io.grpc:grpc-stub" +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${protobufVersion}" + } + generatedFilesBaseDir = "$projectDir/src/generated" + clean { + delete generatedFilesBaseDir + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java" + } + } + generateProtoTasks { + all()*.plugins { + grpc {} + } + } +} + +buildscript { + dependencies { + classpath "com.google.protobuf:protobuf-gradle-plugin:${protobufGradlePluginVersion}" + } +} + +// Optional +eclipse { + classpath { + file.beforeMerged { cp -> + def generatedGrpcFolder = new org.gradle.plugins.ide.eclipse.model.SourceFolder('src/generated/main/grpc', null); + generatedGrpcFolder.entryAttributes['ignore_optional_problems'] = 'true'; + cp.entries.add( generatedGrpcFolder ); + def generatedJavaFolder = new org.gradle.plugins.ide.eclipse.model.SourceFolder('src/generated/main/java', null); + generatedJavaFolder.entryAttributes['ignore_optional_problems'] = 'true'; + cp.entries.add( generatedJavaFolder ); + } + } +} + +// Optional +idea { + module { + sourceDirs += file("src/generated/main/java") + sourceDirs += file("src/generated/main/grpc") + generatedSourceDirs += file("src/generated/main/java") + generatedSourceDirs += file("src/generated/main/grpc") + } +} +```` + +### 服务端项目 + +#### Maven (Server) + +````xml + + + net.devh + grpc-server-spring-boot-starter + + + + example + my-grpc-interface + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + +```` + +#### Gradle (Server) + +````gradle +apply plugin: 'org.springframework.boot' + +dependencies { + compile('org.springframework.boot:spring-boot-starter') + compile('net.devh:grpc-server-spring-boot-starter') + compile('my-example:my-grpc-interface') +} + +buildscript { + dependencies { + classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") + } +} + +```` + +### 客户端项目 + +请参阅 [客户端入门指引](../client/getting-started.md#client-project) 页面 + +## 创建 gRPC 服务定义 + +将您的 protobuf 定义/`.proto`文件放入`src/main/proto`。 有关编写 protobuf 文件的信息,请参阅官方的 [protobuf 文档](https://developers.google.com/protocol-buffers/docs/proto3)。 + +您的 `.proto` 文件跟如下的示例类似: + +````proto +syntax = "proto3"; + +package net.devh.boot.grpc.example; + +option java_multiple_files = true; +option java_package = "net.devh.boot.grpc.examples.lib"; +option java_outer_classname = "HelloWorldProto"; + +// The greeting service definition. +service MyService { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) { + } +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} +```` + +配置 maven/gradle protobuf 插件使其调用 [`protoc`](https://mvnrepository.com/artifact/com.google.protobuf/protoc) 编译器,并使用 [`protoc-gen-grpc-java`](https://mvnrepository.com/artifact/io.grpc/protoc-gen-grpc-java) 插件并生成数据类、grpc 服务类 `ImplicBase`s 和 `Stub`。 请注意,其他插件,如 [reactive-grpc](https://github.com/salesforce/reactive-grpc) 可能会生成其他额外 / 替代类。 然而,它们也可以同样的方式使用。 + +- `ImplicBase`类包含基本逻辑,映射虚拟实现到grpc 服务方法。 在 [实现服务逻辑](#implementing-the-service) 章节中有更多关于这个问题的信息。 +- `Stub`类是完整的客户端实现。 更多信息可以参考 [客户端指引](../client/getting-started.md) 页面。 + +## 实现服务逻辑 + +`protoc-gen-grpc-java` 插件为你的每个 grpc 服务生成一个类。 例如:`MyServiceGrpc` 的 `MyService` 是 proto 文件中的 grpc 服务名称。 这个类 包含您需要扩展的客户端 stub 和服务端的 `ImplicBase`。 + +在这之后,你还有四个步骤: + +1. 请确保您的 `MyServiceImp` 实现了 `MyServiceGrpc.MyServiceImpBase` +2. 将 `@GrpcService` 注解添加到您的 `MyServiceImp` 类上 +3. 请确保 `MyServiceImplic` 已添加到您的应用程序上下文中。 + - 通过在您的 `@Configuration` 类中创建 `@Bean` + - 或者将其放置在 spring 的自动检测到路径中(例如在您`Main`类的相同或子包中) +4. 实现 grpc 服务方法。 + +您的 grpc 服务类将会看起来与下面的例子有些相似: + +````java +import example.HelloReply; +import example.HelloRequest; +import example.MyServiceGrpc; + +import io.grpc.stub.StreamObserver; + +import net.devh.boot.grpc.server.service.GrpcService; + +@GrpcService +public class MyServiceImpl extends MyServiceGrpc.MyServiceImplBase { + + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + HelloReply reply = HelloReply.newBuilder() + .setMessage("Hello ==> " + request.getName()) + .build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + +} +```` + +> **注意**: 理论上来说,不需要拓展 `ImplBase` 而是自己实现 `BindableService`。 但是,这样做可能会导致绕过 Spring Security 的检查。 + +上面就是你接入过程中所有需要做的。 现在您可以启动您的 spring-boot 应用程序并开始向您的 grpc 服务发送请求。 + +默认情况下,grpc-server 将使用 `PLAINTEXT` 模式在端口 `9090` 中启动。 + +您可以通过运行 [grpcurl](https://github.com/fullstorydev/grpcurl) 命令来测试您的应用程序是否正常运行: + +````sh +grpcurl --plaintext localhost:9090 list +grpcurl --plaintext localhost:9090 list net.devh.boot.grpc.example.MyService +grpcurl --plaintext -d '{"name": "test"}' localhost:9090 net.devh.boot.grpc.example.MyService/sayHello +```` + +## 附加主题 + +- *入门指南* +- [配置](configuration.md) +- [上下文数据 / Bean 的作用域](contextual-data.md) +- [测试服务](testing.md) +- [安全性](security.md) + +---------- + +[<- 返回索引](../index.md) diff --git a/docs/zh-CN/server/security.md b/docs/zh-CN/server/security.md new file mode 100644 index 000000000..c644df043 --- /dev/null +++ b/docs/zh-CN/server/security.md @@ -0,0 +1,255 @@ +# 服务端安全 + +[<- 返回索引](../index.md) + +本节描述如何使用传输层安全和身份验证来保护您的应用程序。 我们强烈建议至少启用运输层安全。 + +## 目录 + +- [启用传输图层安全](#enable-transport-layer-security) + - [基础要求](#prerequisites) + - [服务端配置](#configuring-the-server) +- [双向证书认证](#mutual-certificate-authentication) +- [认证和授权](#authentication-and-authorization) + - [配置身份验证](#configure-authentication) + - [配置授权](#configure-authorization) + +## 附加主题 + +- [入门指南](getting-started.md) +- [配置](configuration.md) +- [上下文数据 / Bean 的作用域](contextual-data.md) +- [测试服务](testing.md) +- *安全性* + +## 启用传输图层安全 + +您可以使用 Spring 的配置机制来配置传输层安全。 与之相关的非安全性配置选项参见 [配置](configuration.md) 页面。 + +如果你的服务在 TLS 的反向代理后面,你可能不需要设置 `TLS/`。 如果您不熟悉安全,请咨询安全专家。 请不要忘记检查是否存在安全问题。 ^^ + +> **注意: ** 请参考 [官方文档](https://github.com/grpc/grpc-java/blob/master/SECURITY.md) 以获取更多信息! + +### 基础要求 + +- 在您的 classpath 上有兼容的 `SSL `/`TLS` 实现 + - 包含 [grpc-netty-shaded](https://mvnrepository.com/artifact/io.grpc/grpc-netty-shaded) + - 对于[`grpc-netty`](https://mvnrepository.com/artifact/io.grpc/grpc-netty),还需要额外添加 [`nety-tcnative-boringssl-static`](https://mvnrepository.com/artifact/io.netty/netty-tcnative-boringssl-static) 依赖。 (请使用 [grpc-java的 Netty 安全部分](https://github.com/grpc/grpc-java/blob/master/SECURITY.md#netty) 表中列出**完全相同** (兼容)的版本)。 +- 带有私钥的证书 + +#### 生成一个自签名的证书 + +如果您没有证书(例如内部测试服务器),您可以使用`openssl`生成证书: + +````sh +openssl req -x509 -nodes -subj "//CN=localhost" -newkey rsa:4096 -sha256 -keyout server.key -out server.crt -days 3650 +```` + +请注意,如果没有额外配置,这些证书不受任何应用程序的信任。 我们建议您使用受全球CA或您公司CA信任的证书。 + +### 服务端配置 + +为了允许 grpc-server 使用 `TLS ` 您必须使用以下选项来配置它: + +````properties +grpc.server.security.enabled=true +grpc.server.security.certificateChain=file:certificates/server.crt +grpc.server.security.privateKey=file:certificates/server.key +#grpc.server.security.privateKeyPassword=MyStrongPassword +```` + +如果您想知道这里支持哪些选项,请阅读 [Spring 的 Resource 文档](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#resources-resourceloader)。 + +对于客户端的配置,请参考 [客户端安全](../client/security.md) 页面。 + +## 双向证书认证 + +如果您想要确保只有可信的客户端能连接到服务端,您可以启用共同证书进行身份验证。 这允许或强制客户端使用`x509`证书验证自己。 + +要启用相互身份验证,只需将以下属性添加到您的配置: + +````properties +grpc.server.security.trustCertCollection=file:certificates/trusted-clients.crt.collection +grpc.server.security.clientAuth=REQUIRE +```` + +您可以通过简单地绑定客户端证书创建 `trusted-clients.crt.collection` 文件: + +````sh +cat client*.crt > trusted-clients.crt.collection +```` + +`客户端认证`模式定义了服务端的行为: + +- `REQUIRE` 客户端证书必须通过认证。 +- `OPTIONAL` 对客户端的证书进行身份验证,但不会强制这么做。 + +如果您只想保护一些重要的服务或方法,您可以使用 `OPTIONAL`。 + +尤其是在后一种情况下,适当的配置身份认证尤为重要。 + +## 认证和授权 + +`grpc-spring-boot-starter` 原生支持 `spring-security` , 因此您可以使用众所周知的一些注解来保护您的应用程序。 + +![server-request-security](/grpc-spring-boot-starter/assets/images/server-security.svg) + +### 配置身份验证 + +为了支持来自 grpc 客户端的身份验证,您必须定义客户端如何被允许进行身份验证。 您可以通过自定义 [`GrpcAuthenticationReader`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/security/authentication/GrpcAuthenticationReader.html) 来实现。 + +grpc-spring-boot-starter 提供了一些内置实现: + +- [`AnonymousAuthenticationReader`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/security/authentication/AnonymousAuthenticationReader.html) Spring 的匿名身份认证。 +- [`BasicGrpcAuthenticationReader`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/security/authentication/BasicGrpcAuthenticationReader.html) 基础身份认证。 +- [`BearerAuthenticationReader`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/security/authentication/BearerAuthenticationReader.html) OAuth 以及类似协议的身份认证。 +- [`SSLContextGrpcAuthenticationReader`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReader.html) 基于证书的身份认证。 +- [`CompositeGrpcAuthenticationReader`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/security/authentication/CompositeGrpcAuthenticationReader.html) 依次尝试多个身份验证器。 + +您 Bean 的定义将跟下面这个示例类似: + +````java +@Bean +public GrpcAuthenticationReader grpcAuthenticationReader() { + return new BasicGrpcAuthenticationReader(); +} +```` + +如果您想要强制用户使用 `CompositegrpcAuthenticationReader` ,而其中的一个`GrpcAuthenticationReader` 抛出一个`AuthenticationException`。 那么身份验证将失败,并且停止请求的处理。 如果 `GrpcAuthenticationReader` 返回 null,用户任然是未经验证。 如果身份验证器能够提取凭证/认证,则会交给 Spring 的`AuthenticationManager` 来管理。 由它来决定是否发送有效凭据,并且是否可以继续进行操作。 + +#### 设置示例 + +以下部分包含不同身份认证的配置示例: + +> 注意:不必在`CompositegrpcAuthenticationReader` 中包装阅读器,你可以直接添加多种机制。 + +##### 基本认证 + +````java +@Bean +AuthenticationManager authenticationManager() { + final List providers = new ArrayList<>(); + providers.add(...); // Possibly DaoAuthenticationProvider + return new ProviderManager(providers); +} + +@Bean +GrpcAuthenticationReader authenticationReader() { + final List readers = new ArrayList<>(); + readers.add(new BasicGrpcAuthenticationReader()); + return new CompositeGrpcAuthenticationReader(readers); +} +```` + +##### Bearer 认证 (OAuth2/OpenID-Connect) + +````java +@Bean +AuthenticationManager authenticationManager() { + final List providers = new ArrayList<>(); + providers.add(...); // Possibly JwtAuthenticationProvider + return new ProviderManager(providers); +} + +@Bean +GrpcAuthenticationReader authenticationReader() { + final List readers = new ArrayList<>(); + // The actual token class is dependent on your spring-security library (OAuth2/JWT/...) + readers.add(new BearerAuthenticationReader(accessToken -> new BearerTokenAuthenticationToken(accessToken))); + return new CompositeGrpcAuthenticationReader(readers); +} +```` + +您也可能想要自定义 *GrantedAuthoritiesConverter* 来映射持有者 token 到权限 / 角色到 Spring Security 的 <>GrantedAuthority 中。 + +##### 证书认证 + +````java +@Bean +AuthenticationManager authenticationManager() { + final List providers = new ArrayList<>(); + providers.add(new X509CertificateAuthenticationProvider(userDetailsService())); + return new ProviderManager(providers); +} + +@Bean +GrpcAuthenticationReader authenticationReader() { + final List readers = new ArrayList<>(); + readers.add(new SSLContextGrpcAuthenticationReader()); + return new CompositeGrpcAuthenticationReader(readers); +} +```` + +另见[双向证书认证](#mutual-certificate-authentication)。 + +### 配置授权 + +这个步骤非常重要,因为它实际保护您的应用程序免受不必要的访问。 您可以通过两种方式保护您的 grpc 服务端。 + +#### gRPC 安全检查 + +保护应用程序安全的一种方式是将 [`GrpcSecurityMetadataSource`](https://javadoc.io/page/net.devh/grpc-server-spring-boot-autoconfigure/latest/net/devh/boot/grpc/server/security/check/GrpcSecurityMetadataSource.html) bean 添加到您的应用商家文中。 它允许您在每个 grpc 方法级别返回安全条件。 + +一个示例 bean 定义 (使用硬代码规则) 可能如下所示: + +````java +import net.devh.boot.grpc.server.security.check.AccessPredicate; +import net.devh.boot.grpc.server.security.check.ManualGrpcSecurityMetadataSource; + +@Bean +GrpcSecurityMetadataSource grpcSecurityMetadataSource() { + final ManualGrpcSecurityMetadataSource source = new ManualGrpcSecurityMetadataSource(); + source.set(MyServiceGrpc.getMethodA(), AccessPredicate.authenticated()); + source.set(MyServiceGrpc.getMethodB(), AccessPredicate.hasRole("ROLE_USER")); + source.set(MyServiceGrpc.getMethodC(), AccessPredicate.hasAllRole("ROLE_FOO", "ROLE_BAR")); + source.set(MyServiceGrpc.getMethodD(), auth -> "admin".equals(auth.getName())); + source.setDefault(AccessPredicate.denyAll()); + return source; +} + +@Bean +AccessDecisionManager accessDecisionManager() { + final List> voters = new ArrayList<>(); + voters.add(new AccessPredicateVoter()); + return new UnanimousBased(voters); +} +```` + +您必须配置 `AccessDecisionManager` 否则它不知道如何处理`AccessPredicate`。 + +此方法的好处是您能够将配置移动到外部文件或数据库。 但是你必须自己实现。 + +#### Spring 注解安全性检查 + +当然,也可以仅仅使用 Spring Security 的注解。 对于这种情况,您必须将以下注解添加到您的某个 `@Configuration` 类中: + +````java +@EnableGlobalMethodSecurity(___Enabled = true, proxyTargetClass = true) +```` + +> 请注意 `proxyTargetClass = true` 是必需的! 如果你忘记添加它,你会得到很多 `UNimpleneted` 的响应。 然而,您添加它将收到一个警告,`MyServiceImplic#bindService()` 方法是 final 修饰的。 **不要** 试图取消这些 final 修饰的方法,这将导致安全被绕过。 + +然后您可以简单地在您的 grpc 方法上加注解: + +````java +@Override +@Secured("ROLE_ADMIN") +// MyServiceGrpc.methodX +public void methodX(Request request, StreamObserver responseObserver) { + [...] +} +```` + +> 这个库假定你扩展 `ImplicBase` (由 grpc生成)来实现服务。 不这样做可能会导致绕过 Spring Security 的安全配置。 + +## 附加主题 + +- [入门指南](getting-started.md) +- [配置](configuration.md) +- [上下文数据 / Bean 的作用域](contextual-data.md) +- [测试服务](testing.md) +- *安全性* + +---------- + +[<- 返回索引](../index.md) diff --git a/docs/zh-CN/server/testing.md b/docs/zh-CN/server/testing.md new file mode 100644 index 000000000..0ca354d59 --- /dev/null +++ b/docs/zh-CN/server/testing.md @@ -0,0 +1,283 @@ +# 测试服务 + +[<- 返回索引](../index.md) + +本节介绍如何为您的 grpc-service 编写测试用例。 + +## 目录 + +- [前言](#introductory-words) +- [测试服务](#the-service-to-test) +- [有用的依赖项](#useful-dependencies) +- [单元测试](#unit-tests) + - [独立测试](#standalone-tests) + - [基于Spring的测试](#spring-based-tests) +- [集成测试](#integration-tests) + +## 附加主题 + +- [入门指南](getting-started.md) +- [配置](configuration.md) +- [上下文数据 / Bean 的作用域](contextual-data.md) +- *测试服务* +- [安全性](security.md) + +## 前言 + +我们都知道测试对我们的应用程序是多么重要,所以我只会在这里向大家介绍几个链接: + +- [Testing Spring](https://docs.spring.io/spring/docs/current/spring-framework-reference/testing.html) +- [Testing with JUnit](https://junit.org/junit5/docs/current/user-guide/#writing-tests) +- [grpc-spring-boot-starter's Tests](https://github.com/yidongnan/grpc-spring-boot-starter/tree/master/tests/src/test/java/net/devh/boot/grpc/test) + +通常有两种方法来测试您的 grpc 服务: + +- [直接测试](#unit-tests) +- [通过 grpc 测试](#integration-tests) + +## 测试服务 + +让我们假设,我们希望测试以下服务: + +````java +@GrpcService +public class MyServiceImpl extends MyServiceGrpc.MyServiceImplBase { + + private OtherDependency foobar; + + @Autowired + public void setFoobar(OtherDependency foobar) { + this.foobar = foobar; + } + + @Override + public void sayHello(HelloRequest request, StreamObserver responseObserver) { + HelloReply response = HelloReply.newBuilder() + .setMessage("Hello ==> " + request.getName()) + .setCounter(foobar.getCount()) + .build(); + responseObserver.onNext(response); + responseObserver.onComplete(); + } + +} +```` + +## 有用的依赖项 + +在您开始编写自己的测试框架之前,您可能想要使用以下库来使您的工作更加简单。 + +对于Maven来说,添加以下依赖: + +````xml +<!-- JUnit-elotelFramework --> + + org.junit。 upiter + junit-jupiter-api + test + + + org。 unit.jupiter + junit-jupiter-engine + test + +<- Grpc-extract Support --> + + io. rpc + grpc-testing + test + +<! - Spring-Extract Support (Optional) --> + + org。 pringframework.boot + spring-boot-start-test + test + +```` + +Gradle 使用: + +````groovy +// JUnit-Test-Framework +testImplementation("org.junit.jupiter:junit-jupiter-api") +testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") +// Grpc-Test-Support +testImplementation("io.grpc:grpc-testing") +// Spring-Test-Support (Optional) +testImplementation("org.springframework.boot:spring-boot-starter-test") +```` + +## 单元测试 + +在直接测试中,我们直接在 grpc-service bean/实例上调用方法。 + +> 如果您自己创建的 grpc-service 实例, 请确保您先处理所需的依赖关系。 如果您使用Spring,它会处理您的依赖关系,但作为代价,您必须配置Spring。 + +### 独立测试 + +独立测试对外部库没有任何依赖关系(事实上你甚至不需要这个项目)。 然而,没有外部依赖关系并不总能使您的生活更加容易, 您可能需要复制其他库来执行您的行为。 使用 [Mockito](https://site.mockito.org) 这样的模拟库会简化你的流程,因为它限制依赖树的深度。 + +````java +public class MyServiceTest { + + private MyServiceImpl myService; + + @BeforeEach + public void setup() { + myService = new MyServiceImpl(); + OtherDependency foobar = ...; // mock(OtherDependency.class) + myService.setFoobar(foobar); + } + + @Test + void testSayHellpo() throws Exception { + HelloRequest request = HelloRequest.newBuilder() + .setName("Test") + .build(); + StreamRecorder responseObserver = StreamRecorder.create(); + myService.sayHello(request, responseObserver); + if (!responseObserver.awaitCompletion(5, TimeUnit.SECONDS)) { + fail("The call did not terminate in time"); + } + assertNull(responseObserver.getError()); + List results = responseObserver.getValues(); + assertEquals(1, results.size()); + HelloReply response = results.get(0); + assertEquals(HelloReply.newBuilder() + .setMessage("Hello ==> Test") + .setCounter(1337) + .build(), response); + } + +} +```` + +### 基于Spring的测试 + +如果您使用Spring来管理您自己的依赖关系,您实际上正在进入集成测试领域。 请确保您不要启动整个应用程序,而只提供所需的依赖关系为 (模拟) 的 Bean 类。 + +> **注意:** 在测试期间,Spring 不会自动配置所有必须的 Bean。 您必须在有`@Configuration` 注解的类中手动创建它们。 + +````java +@SpringBootTest +@SpringJUnitConfig(classes = { MyServiceUnitTestConfiguration.class }) +// Spring doesn't start without a config (might be empty) +// Don't use @EnableAutoConfiguration in this scenario +public class MyServiceTest { + + @Autowired + private MyServiceImpl myService; + + @Test + void testSayHellpo() throws Exception { + HelloRequest request = HelloRequest.newBuilder() + .setName("Test") + .build(); + StreamRecorder responseObserver = StreamRecorder.create(); + myService.sayHello(request, responseObserver); + if (!responseObserver.awaitCompletion(5, TimeUnit.SECONDS)) { + fail("The call did not terminate in time"); + } + assertNull(responseObserver.getError()); + List results = responseObserver.getValues(); + assertEquals(1, results.size()); + HelloReply response = results.get(0); + assertEquals(HelloReply.newBuilder() + .setMessage("Hello ==> Test") + .setCounter(1337) + .build(), response); + } + +} +```` + +和所需的配置类: + +````java +@Configuration +public class MyServiceUnitTestConfiguration { + + @Bean + OtherDependency foobar() { + // return mock(OtherDependency.class); + } + + @Bean + MyServiceImpl myService() { + return new MyServiceImpl(); + } + +} +```` + +## 集成测试 + +然而,您有时需要测试整个调用栈。 例如,如果认证发挥了作用。 但在这种情况下,建议限制您的测试范围,以避免像 空数据库这样可能的外部影响。 + +在这一点上,不使用 Spring 测试您的 Spring 应用程序是毫无意义的。 + +> **注意:** 在测试期间,Spring 不会自动配置所有必须的 Bean。 您必须在有 `@Configuration` 注解修饰的类中手动创建他们,或显式的包含相关的自动配置类。 + +````java +@SpringBootTest(properties = { + "grpc.server.inProcessName=test", // Enable inProcess server + "grpc.server.port=-1", // Disable external server + "grpc.client.inProcess.address=in-process:test" // Configure the client to connect to the inProcess server + }) +@SpringJUnitConfig(classes = { MyServiceIntegrationTestConfiguration.class }) +// Spring doesn't start without a config (might be empty) +@DirtiesContext // Ensures that the grpc-server is properly shutdown after each test + // Avoids "port already in use" during tests +public class MyServiceTest { + + @GrpcClient("inProcess") + private MyServiceBlockingStub myService; + + @Test + @DirtiesContext + public void testSayHello() { + HelloRequest request = HelloRequest.newBuilder() + .setName("test") + .build(); + HelloReply response = myService.sayHello(request); + assertNotNull(response); + assertEquals("Hello ==> Test", response.getMessage()) + } + +} +```` + +所需的配置看起来像这样: + +````java +@Configuration +@ImportAutoConfiguration({ + GrpcServerAutoConfiguration.class, // Create required server beans + GrpcServerFactoryAutoConfiguration.class, // Select server implementation + GrpcClientAutoConfiguration.class}) // Support @GrpcClient annotation +public class MyServiceIntegrationTestConfiguration { + + @Bean + OtherDependency foobar() { + return ...; // mock(OtherDependency.class); + } + + @Bean + MyServiceImpl myServiceImpl() { + return new MyServiceImpl(); + } + +} +```` + +> 注意:这个代码看起来可能比单元测试更短/更简单,但执行时间要长一些。 + +## 附加主题- [入门指南](getting-started.md) +- [配置](configuration.md) +- [上下文数据 / Bean 的作用域](contextual-data.md) +- *测试服务* +- [安全性](security.md) + +---------- + +[<- 返回索引](../index.md) diff --git a/docs/zh-CN/trouble-shooting.md b/docs/zh-CN/trouble-shooting.md new file mode 100644 index 000000000..119bd63b4 --- /dev/null +++ b/docs/zh-CN/trouble-shooting.md @@ -0,0 +1,268 @@ +# 疑难解答 + +[<- 返回索引](index.md) + +本节描述这个项目的一些常见错误,以及如何解决这些错误。 请注意,这个页面永远不能覆盖所有案件,还请搜索现有的 issures/PRs(打开和关闭状态的)。 如果对应的主题已经存在,请给我们留下评论/信息,以便我们知道你也会受到影响。 如果没有这样的主题,请随时打开本页底部描述创建一个的新主题。 + +## 目录 + +- [传输失败](#transport-failed) +- [网络因未知原因关闭](#network-closed-for-unknown-reason) +- [找不到 TLS ALPN 提供商](#could-not-find-tls-alpn-provider) +- [证书不匹配](#dismatching-certificates) +- [不受信任的证书](#untrusted-certificates) +- [服务端端口被占用](#server-port-already-in-use) +- [创建 issues / 提问题](#creating-issues) + +## 传输失败 + +### 服务端 + +````txt +2019-07-07 10:05:46.217 INFO 6552 --- [-worker-ELG-3-5] i.g.n.s.i.g.n.N.connections : Transport failed + +io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception: HTTP/2 client preface string missing or corrupt. Hex dump for received bytes: 16030100820100007e0303aae6126974cbb4638b325d6bdb + at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2Exception.connectionError(Http2Exception.java:85) ~[grpc-netty-shaded-1.21.0.jar:1.21.0] + at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.readClientPrefaceString(Http2ConnectionHandler.java:318) ~[grpc-netty-shaded-1.21.0.jar:1.21.0] + at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler$PrefaceDecoder.decode(Http2ConnectionHandler.java:251) ~[grpc-netty-shaded-1.21.0.jar:1.21.0] + at io.grpc.netty.shaded.io.netty.handler.codec.http2.Http2ConnectionHandler.decode(Http2ConnectionHandler.java:450) [grpc-netty-shaded-1.21.0.jar:1.21.0] +```` + +### 客户端 + +````txt +io.grpc.StatusRuntimeException: UNAVAILABLE: io exception + at io.grpc.stub.ClientCalls.toStatusRuntimeException(ClientCalls.java:235) + at io.grpc.stub.ClientCalls.getUnchecked(ClientCalls.java:216) + at io.grpc.stub.ClientCalls.blockingUnaryCall(ClientCalls.java:141) + at net.devh.boot.grpc.examples.lib.SimpleGrpc$SimpleBlockingStub.sayHello(SimpleGrpc.java:178) + [...] +Caused by: io.grpc.netty.shaded.io.netty.handler.ssl.NotSslRecordException: not an SSL/TLS record: 00001204000000000000037fffffff000400100000000600002000000004080000000000000f0001 + at io.grpc.netty.shaded.io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1204) + at io.grpc.netty.shaded.io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1272) + at io.grpc.netty.shaded.io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:502) +```` + +### 问题 + +服务器运行在`PLAINTEXT`模式,但客户端试图在`TLS`(默认)模式中连接它。 + +### 简单的解决办法 + +将客户端配置在`PLAINTEXT`模式下连接(不推荐生产)。 + +添加以下条目到您的客户端应用程序配置: + +````properties +grpc.client.__name__.negotiationType=PLAINTEXT +```` + +### 更好的解决办法 + +将服务端配置在`TLS`模式下运行(推荐)。 + +添加以下条目到您的服务端应用程序配置: + +````properties +grpc.server.security.enabled=true +grpc.server.security.certificateChain=file:certificates/server.crt +grpc.server.security.privateKey=file:certificates/server.key +```` + +## 网络因未知原因关闭 + +### 客户端 + +````txt +io.grpc.StatusRuntimeException: UNAVAILABLE: Network closed for unknown reason +```` + +### 问题 + +您可能是 (1) 尝试通过 `TLS ` 模式连接到 grpc-server 时,使用 `PLAINTE` 客户端 或 (2) 目标不是一个 grpc-server (例如 Web 服务)。 + +### 解决办法 + +1. 配置您的客户端使用`TLS`模式。 + + ````properties + grpc.client.__name__.negotiationType=TLS + ```` + + 或删除`negotiationType`配置,因为默认情况下`TLS`。 +2. 使用 `grpcurl` 或类似工具,验证已配置的服务端正在运行的是 grpc 服务 + +## 找不到 TLS ALPN 提供商 + +### 服务端 + +````txt +org.springframework.context.ApplicationContextException: Failed to start bean 'nettyGrpcServerLifecycle'; nested exception is java.lang.IllegalStateException: Could not find TLS ALPN provider; no working netty-tcnative, Conscrypt, or Jetty NPN/ALPN available + at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:185) ~[spring-context-5.1.8.RELEASE.jar:5.1.8.RELEASE] + [...] +Caused by: java.lang.IllegalStateException: Could not find TLS ALPN provider; no working netty-tcnative, Conscrypt, or Jetty NPN/ALPN available + at io.grpc.netty.GrpcSslContexts.defaultSslProvider(GrpcSslContexts.java:258) ~[grpc-netty-1.21.0.jar:1.21.0] + at io.grpc.netty.GrpcSslContexts.configure(GrpcSslContexts.java:171) ~[grpc-netty-1.21.0.jar:1.21.0] + at io.grpc.netty.GrpcSslContexts.forServer(GrpcSslContexts.java:130) ~[grpc-netty-1.21.0.jar:1.21.0] + [...] +```` + +### 客户端 + +````txt +[...] +Caused by: java.lang.IllegalStateException: Failed to create channel: + at net.devh.boot.grpc.client.inject.GrpcClientBeanPostProcessor.processInjectionPoint(GrpcClientBeanPostProcessor.java:118) ~[grpc-client-spring-boot-autoconfigure-2.4.0.RELEASE.jar:2.4.0.RELEASE] + at net.devh.boot.grpc.client.inject.GrpcClientBeanPostProcessor.postProcessBeforeInitialization(GrpcClientBeanPostProcessor.java:77) + [...] +Caused by: java.lang.IllegalStateException: Could not find TLS ALPN provider; no working netty-tcnative, Conscrypt, or Jetty NPN/ALPN available + at io.grpc.netty.GrpcSslContexts.defaultSslProvider(GrpcSslContexts.java:258) ~[grpc-netty-1.21.0.jar:1.21.0] + at io.grpc.netty.GrpcSslContexts.configure(GrpcSslContexts.java:171) ~[grpc-netty-1.21.0.jar:1.21.0] + at io.grpc.netty.GrpcSslContexts.forClient(GrpcSslContexts.java:120) ~[grpc-netty-1.21.0.jar:1.21.0] + [...] +```` + +### 两端 + +````txt +AbstractMethodError: io.netty.internal.tcnative.SSL.readFromSSL() +```` + +### 问题 + +classpath 上没有 (兼容) netty TLS 实现。 + +### 解决办法 + +从[`grpc-netty`](https://mvnrepository.com/artifact/io.grpc/grpc-netty)切换到[`grpc-netty-shaded`](https://mvnrepository.com/artifact/io.grpc/grpc-netty-shaded) 或添加依赖于[`nety-tcnative-boringssl-static`](https://mvnrepository.com/artifact/io.netty/netty-tcnative-boringssl-static) (请使用与[grpc-java 的netty 安全性部分](https://github.com/grpc/grpc-java/blob/master/SECURITY.md#netty)**完全相同**(兼容的版本))。 + +> **注意:** 你需要一个 64 位的 Java 虚拟机。 + +## 证书不匹配 + +### 客户端 + +````txt +io.grpc.StatusRuntimeException: UNAVAILABLE: io exception +[...] +Caused by: javax.net.ssl.SSLHandshakeException: General OpenSslEngine problem +[...] +Caused by: java.security.cert.CertificateException: No subject alternative names present +```` + +或 + +````txt +io.grpc.StatusRuntimeException: UNAVAILABLE: io exception +[...] +Caused by: javax.net.ssl.SSLHandshakeException: General OpenSslEngine problem +[...] +Caused by: java.security.cert.CertificateException: No name matching found +```` + +### 问题 + +证书与目标地址/名称不匹配。 + +### 解决办法 + +通过在客户端配置中添加以下内容: + +````properties +grpc.client.__name__.security.authorityOverride= +```` + +## 不受信任的证书 + +### 客户端 + +````txt +io.grpc.StatusRuntimeException: UNAVAILABLE: io exception +[...] +Caused by: javax.net.ssl.SSLHandshakeException: General OpenSslEngine problem +[...] +Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target +[...] +Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target +```` + +### 问题 + +服务器使用的证书不在客户端的信任库中。 + +### 解决办法 + +通过使用 java `keytool` 将证书添加到java的信任商店,或配置客户端使用自定义信任的证书文件: + +````properties +grpc.client.__name__.security.trustCertCollection=file:certificates/trusted-servers-collection.crt.list +```` + +> **注意:** 两边的存储库目前在创建时都是只读的,更新不会被应用。 + +## 服务端端口被占用 + +### 服务端 + +````txt +Caused by: java.lang.IllegalStateException: Failed to start the grpc server + at net.devh.boot.grpc.server.serverfactory.GrpcServerLifecycle.start(GrpcServerLifecycle.java:51) ~[grpc-server-spring-boot-autoconfigure-2.4.0.RELEASE.jar:2.4.0.RELEASE] + [...] +Caused by: java.io.IOException: Failed to bind + at io.grpc.netty.shaded.io.grpc.netty.NettyServer.start(NettyServer.java:246) ~[grpc-netty-shaded-1.21.0.jar:1.21.0] + at io.grpc.internal.ServerImpl.start(ServerImpl.java:177) ~[grpc-core-1.21.0.jar:1.21.0] + at io.grpc.internal.ServerImpl.start(ServerImpl.java:85) ~[grpc-core-1.21.0.jar:1.21.0] + at net.devh.boot.grpc.server.serverfactory.GrpcServerLifecycle.createAndStartGrpcServer(GrpcServerLifecycle.java:90) ~[grpc-server-spring-boot-autoconfigure-2.4.0.RELEASE.jar:2.4.0.RELEASE] + at net.devh.boot.grpc.server.serverfactory.GrpcServerLifecycle.start(GrpcServerLifecycle.java:49) ~[grpc-server-spring-boot-autoconfigure-2.4.0.RELEASE.jar:2.4.0.RELEASE] + ... 13 common frames omitted +Caused by: java.net.BindException: Address already in use: bind +```` + +### 问题 + +grpc 服务端尝试使用的端口被占用。 + +有四种常见情况可能发生这种错误。 + +1. 应用程序已在运行 +2. 另一个应用程序正在使用该端口 +3. grpc 服务器使用了一个已经用于其他用途的端口(例如spring-web) +4. 你正在运行测试,每次测试后你都没有关闭 grpc-server + +### 解决办法 + +1. 尝试使用任务管理器或`jps`搜索应用程序 +2. 尝试使用 `netstat` 搜索端口 +3. 检查/更改您的配置。 此库默认使用端口 `9090` +4. 添加`@DirtiesContext`到您的测试类和方法中,请注意,这个错误只会从第二次测试开始发生,因此你必须在你的第一个测试类上也加上这个注解! + +## 创建 issue + +在 GitHub 上创建问题/提问并不难,但你可以稍微努力帮助我们更快地解决您的 个问题。 + +如果您的问题/疑问一般都是关于 grpc 的问题,请考虑在 [grpc-java](https://github.com/grpc/grpc-java) 上提问。 + +使用提供的模板来创建新问题,其中包含我们需要的必需/有用信息的部分。 + +通常来说,你应该在你的问题上包括以下信息: + +1. 您有什么类型的诉求? + - 问题 + - Bug 反馈 + - 功能​​​​​​​​​​​请求 +2. 你希望的结果是什么? +3. 问题是什么? 什么不起作用? 缺少什么东西,为什么需要? +4. 任何相关堆栈/日志(非常重要) +5. 您使用的是哪个版本? + - Spring (boot) + - grpc-java + - grpc-spring-boot-starter + - 其他相关库 +6. 其他背景 + - 它以前是否正常运行过? + - 我们如何重现? + - 有 demo 演示吗? + +---------- + +[<- 返回索引](index.md) diff --git a/docs/zh-CN/versions.md b/docs/zh-CN/versions.md new file mode 100644 index 000000000..708a2ed1f --- /dev/null +++ b/docs/zh-CN/versions.md @@ -0,0 +1,90 @@ +# 版本 + +[<- 返回索引](index.md) + +此页显示关于我们版本策略和生命周期等额外信息。 + +## 目录 + +- [版本策略](#versioning-policy) +- [版本列表](#version-table) + - [2.x 版本](#version-2x) + - [1.x 版本](#version-1x) + - [升级依赖关系](#upgrading-dependencies) + - [发布日志](#release-notes) + +## 版本策略 + +这个项目的主要版本定义了我们与哪个Spring-boot版本兼容。 + +- 1.x.x 版本为 EOL,不会再收到任何更新。 +- 2.x.x 是当前的维护版本,如果有 spring-boot 或者 gRPC 版本,将进行更新。 + +次版本定义了此项目的功能版本。 每次我们更改 spring boot 或 gRPC 版本时,我们也会增加我们的功能版本。 如果我们增加/改变主要特征,也是如此。 在大多数情况下,你不会通过升级获得任何不兼容之处,但因为gRPC 就像它的 API 一样, 这个问题不能被排除在外。 我们试图尽量减少这种影响,但不能排除这种影响。 如果您不使用高级功能,您通常不会收到通知。 + +我们通常不发布补丁版本,但在下次发布时包含这些补丁。 如果你需要一个修补过的版本,请新开一个 issue。 + +## 版本列表 + +下表显示了该项目和 spring boot 以及 gRPC 版本的关系。 在大多数情况下,你可以升级到较新的版本,但如果是 gRPC 改变了其 API。 请将任何问题报告给我们[仓库](https://github.com/yidongnan/grpc-spring-boot-starter/issues)。 + +> **注意** +> +> 如果您正在使用 non-shaded netty(和相关的库),请 **严格** 保持这些版本跟 gRPC [文档](https://github.com/grpc/grpc-java/blob/master/SECURITY.md#netty) 一致。 (grpc-netty-shaded 通过保持这些版本同步来避免这些问题。) + +### 2.x 版本 + +当前版本。 + +| 版本 | spring-boot | gRPC | 日期 | +|:---------:|:-----------:|:------:| ----------:| +| 2.12.0 | 2.4.5 | 1.34.1 | 2021年05月 | +| 2.11.0 | 2.3.8 | 1.31.1 | 2021年02月 | +| 2.10.1 | 2.3.3 | 1.31.1 | 2020年08月 | +| 2.10.0 | 2.3.3 | 1.31.1 | 2020年08月 | +| 2.9.0 | 2.3.0 | 1.30.0 | 2020年06月 | +| 2.8.0 | 2.2.7 | 1.29.0 | 2020年06月 | +| 2.7.0 | 2.2.4 | 1.27.1 | 2020年02月 | +| 2.6.2 | 2.2.1 | 1.25.0 | 2020年01月 | +| 2.6.1 | 2.2.1 | 1.25.0 | 2019年11月 | +| 2.6.0 | 2.2.1 | 1.24.2 | 2019年11月 | +| 2.5.1 | 2.1.6 | 1.22.2 | 2019年08月 | +| 2.5.0 | 2.1.6 | 1.22.1 | 2019年08月 | +| 2.4.0 | 2.1.5 | 1.20.0 | 2019年06月 | +| 2.3.0 | 2.1.4 | 1.18.0 | 2019年04月 | +| 2.2.1 | 2.0.7 | 1.17.1 | 2019年01月 | +| 2.2.0 | 2.0.6 | 1.17.1 | 2018年12月 | +| 2.1.0 | 2.0.? | 1.14.0 | 2018年10月 | +| 2.0.1 | 2.0.? | 1.14.0 | 2018年08月 | +| 2.0.0 | 2.0.? | 1.13.1 | 2018年08月 | + +(* 代表未来的版本) + +### 1.x 版本 + +生命终结——没有计划的更新。 + +| 版本 | spring-boot | gRPC | 日期 | +|:-----:|:-----------:|:------:| -------:| +| 1.4.2 | 1.?.? | 1.12.0 | 2019年6月 | +| 1.4.1 | 1.?.? | 1.12.0 | 2018年6月 | +| ... | 1.?.? | N/A | | + +### 升级依赖关系 + +如果你升级任何版本,我们强烈建议使用 bom 文件进行升级: + +- [spring-boot](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-parent) +- [grpc-java](https://mvnrepository.com/artifact/io.grpc/grpc-bom) + +### 发布日志 + +有关每个版本的更改,请参考发行说明。 + +- [grpc-spring-boot-starter](https://github.com/yidongnan/grpc-spring-boot-starter/releases) +- [spring-boot](https://github.com/spring-projects/spring-boot/releases) +- [grpc-java](https://github.com/grpc/grpc-java/releases) + +---------- + +[<- 返回索引](index.md) diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..29f45289a --- /dev/null +++ b/examples/README.md @@ -0,0 +1,74 @@ +# gRPC spring boot starter Examples + +## Local Setup + +1. Try the local-grpc-server example first run: + + ````sh + ./gradlew :example:local-grpc-server:bootRun + ```` + +2. In a different terminal window run: + + ````sh + ./gradlew :example:local-grpc-client:bootRun + ```` + +3. Visit http://localhost:8080/ to see the result. + +## Cloud Discovery Setup + +1. Choose a cloud discovery implementation: + + - `consul` + - `eureka` + - `nacos` + + > **Note:** In your actual application you are free to choose any cloud discovery implementation, + > but only the above and `zookeeper` will automatically register the server at the discovery service. + > So you might have to write a few extra lines in your server application. + > Generic registration support is planned for a future release. + > No additional configuration is required for clients. + +2. Start the discovery server (only the chosen one): + + ````sh + # Consul + docker run --name=consul -p 8500:8500 consul + # Eureka + ./gradlew :example:cloud-eureka-server:bootRun + # Nacos + docker run --env MODE=standalone --name nacos -d --rm -p 8848:8848 nacos/nacos-server + ```` + +3. Insert the selected implementation and start the server application (in a new terminal window): + + ````sh + ./gradlew -Pdiscovery=$discovery :example:cloud-grpc-server:bootRun + ```` + +4. Insert the selected implementation and start the client application (in a new terminal window): + + ````sh + ./gradlew -Pdiscovery=$discovery :example:cloud-grpc-client:bootRun + ```` + +5. Visit http://localhost:8080/ to see the result. + +## With Basic auth security + +1. Try the security-grpc-server example first run: + + ````sh + ./gradlew :example:security-grpc-server:bootRun + ```` + +2. In a different terminal window run: + + ````sh + ./gradlew :example:security-grpc-client:bootRun + ```` + +3. Visit http://localhost:8080/ to see the result. + +*You can configure the client's username in the application.yml.* diff --git a/examples/cloud-eureka-server/build.gradle b/examples/cloud-eureka-server/build.gradle new file mode 100644 index 000000000..182481759 --- /dev/null +++ b/examples/cloud-eureka-server/build.gradle @@ -0,0 +1,13 @@ +plugins { + id 'org.springframework.boot' +} + +dependencies { + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + + testImplementation('org.springframework.boot:spring-boot-starter-test'){ + exclude module: 'junit-vintage-engine' + exclude module: 'junit' + } +} diff --git a/examples/cloud-eureka-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/EurekaServerApplication.java b/examples/cloud-eureka-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/EurekaServerApplication.java new file mode 100644 index 000000000..30d9af626 --- /dev/null +++ b/examples/cloud-eureka-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/EurekaServerApplication.java @@ -0,0 +1,40 @@ +/* + * 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.examples.cloud.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; + +/** + * Eureka cloud discovery server application. + */ +@EnableEurekaServer +@SpringBootApplication +public class EurekaServerApplication { + + /** + * Starts the Eureka cloud discovery server application. + * + * @param args The arguments to pass to the application. + */ + public static void main(final String... args) { + SpringApplication.run(EurekaServerApplication.class, args); + } + +} diff --git a/examples/cloud-eureka-server/src/main/resources/application.yml b/examples/cloud-eureka-server/src/main/resources/application.yml new file mode 100644 index 000000000..432033764 --- /dev/null +++ b/examples/cloud-eureka-server/src/main/resources/application.yml @@ -0,0 +1,27 @@ +server: + port: 8761 + +spring: + application: + name: eureka-server + +eureka: + instance: + hostname: localhost + prefer-ip-address: true + status-page-url-path: /actuator/info + health-check-url-path: /actuator/health + lease-expiration-duration-in-seconds: 30 + lease-renewal-interval-in-seconds: 30 + client: + registerWithEureka: false + fetchRegistry: false + serviceUrl: + defaultZone: http://localhost:8761/eureka/ + server: + enable-self-preservation: false + +management: + endpoint: + shutdown: + enabled: true diff --git a/examples/cloud-grpc-client/build.gradle b/examples/cloud-grpc-client/build.gradle new file mode 100644 index 000000000..ca6c47016 --- /dev/null +++ b/examples/cloud-grpc-client/build.gradle @@ -0,0 +1,37 @@ +plugins { + id 'org.springframework.boot' +} + +def discoveryProvider = project.findProperty('discovery') ?: 'consul'; + +dependencies { + switch (discoveryProvider) { + case "nacos": { + implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery' + break; + } + case "consul": { + implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery' + break; + } + case "eureka": { + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + break; + } + case "zookeeper": { + implementation 'org.springframework.cloud:spring-cloud-starter-zookeeper-discovery' + break; + } + } + + implementation project(':grpc-client-spring-boot-starter') // Replace with actual dependency "net.devh:grpc-client-spring-boot-starter:${springBootGrpcVersion}" + implementation project(':examples:grpc-lib') // Replace with your grpc interface spec + + // For demonstration only + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-web' +} + +bootRun { + args = ["--spring.profiles.active=" + discoveryProvider] +} diff --git a/examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/CloudClientApplication.java b/examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/CloudClientApplication.java new file mode 100644 index 000000000..9d4855ea4 --- /dev/null +++ b/examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/CloudClientApplication.java @@ -0,0 +1,40 @@ +/* + * 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.examples.cloud.client; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * Example grpc client application using cloud discovery. + */ +@EnableDiscoveryClient +@SpringBootApplication +public class CloudClientApplication { + + /** + * Starts the grpc cloud discovery client application. + * + * @param args The arguments to pass to the application. + */ + public static void main(final String... args) { + SpringApplication.run(CloudClientApplication.class, args); + } + +} diff --git a/examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GlobalInterceptorConfiguration.java b/examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GlobalInterceptorConfiguration.java new file mode 100644 index 000000000..f69de561c --- /dev/null +++ b/examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GlobalInterceptorConfiguration.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.examples.cloud.client; + +import org.springframework.context.annotation.Configuration; + +import io.grpc.ClientInterceptor; +import net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor; + +/** + * Example configuration class that adds a {@link ClientInterceptor} to the global interceptor list. + */ +@Configuration(proxyBeanMethods = false) +public class GlobalInterceptorConfiguration { + + /** + * Creates a new {@link LogGrpcInterceptor} bean and adds it to the global interceptor list. As an alternative you + * can directly annotate the {@code LogGrpcInterceptor} class and it will automatically be picked up by spring's + * classpath scanning. + * + * @return The newly created bean. + */ + @GrpcGlobalClientInterceptor + LogGrpcInterceptor logClientInterceptor() { + return new LogGrpcInterceptor(); + } + +} diff --git a/examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcClientController.java b/examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcClientController.java new file mode 100644 index 000000000..0e013e5c6 --- /dev/null +++ b/examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcClientController.java @@ -0,0 +1,39 @@ +/* + * 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.examples.cloud.client; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Optional demo controller making the grpc service accessible via browser requests. + */ +@RestController +public class GrpcClientController { + + @Autowired + private GrpcClientService grpcClientService; + + @RequestMapping("/") + public String printMessage(@RequestParam(defaultValue = "Michael") final String name) { + return this.grpcClientService.sendMessage(name); + } + +} diff --git a/examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcClientService.java b/examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcClientService.java new file mode 100644 index 000000000..015445bc5 --- /dev/null +++ b/examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcClientService.java @@ -0,0 +1,49 @@ +/* + * 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.examples.cloud.client; + +import org.springframework.stereotype.Service; + +import io.grpc.StatusRuntimeException; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.examples.lib.HelloReply; +import net.devh.boot.grpc.examples.lib.HelloRequest; +import net.devh.boot.grpc.examples.lib.SimpleGrpc.SimpleBlockingStub; + +/** + * Example class demonstrating the usage of {@link GrpcClient}s inside an application. + */ +@Service +@Slf4j +public class GrpcClientService { + + @GrpcClient("cloud-grpc-server") + private SimpleBlockingStub simpleStub; + + public String sendMessage(final String name) { + try { + final HelloReply response = this.simpleStub.sayHello(HelloRequest.newBuilder().setName(name).build()); + return response.getMessage(); + } catch (final StatusRuntimeException e) { + log.error("Request failed", e); + return "FAILED with " + e.getStatus().getCode(); + } + } + +} diff --git a/examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/LogGrpcInterceptor.java b/examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/LogGrpcInterceptor.java new file mode 100644 index 000000000..d6973dccf --- /dev/null +++ b/examples/cloud-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/LogGrpcInterceptor.java @@ -0,0 +1,50 @@ +/* + * 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.examples.cloud.client; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; +import net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor; + +/** + * Example {@link ClientInterceptor} that logs all called methods. In this example it is added to Spring's application + * context via {@link GlobalInterceptorConfiguration}, but is also possible to directly annotate this class with + * {@link GrpcGlobalClientInterceptor}. + */ +// @GrpcGlobalClientInterceptor +public class LogGrpcInterceptor implements ClientInterceptor { + + private static final Logger log = LoggerFactory.getLogger(LogGrpcInterceptor.class); + + @Override + public ClientCall interceptCall( + final MethodDescriptor method, + final CallOptions callOptions, + final Channel next) { + + log.info(method.getFullMethodName()); + return next.newCall(method, callOptions); + } + +} diff --git a/examples/cloud-grpc-client/src/main/resources/application-consul.yml b/examples/cloud-grpc-client/src/main/resources/application-consul.yml new file mode 100644 index 000000000..a8894a62e --- /dev/null +++ b/examples/cloud-grpc-client/src/main/resources/application-consul.yml @@ -0,0 +1,7 @@ +spring: + cloud: + consul: + discovery: + register: false +# hostname: localhost +# port: 8500 diff --git a/examples/cloud-grpc-client/src/main/resources/application-eureka.yml b/examples/cloud-grpc-client/src/main/resources/application-eureka.yml new file mode 100644 index 000000000..3290476e4 --- /dev/null +++ b/examples/cloud-grpc-client/src/main/resources/application-eureka.yml @@ -0,0 +1,10 @@ +eureka: + instance: + prefer-ip-address: true + status-page-url-path: /actuator/info + health-check-url-path: /actuator/health + client: + register-with-eureka: false + fetch-registry: true + service-url: + defaultZone: http://localhost:8761/eureka/ diff --git a/examples/cloud-grpc-client/src/main/resources/application-nacos.yml b/examples/cloud-grpc-client/src/main/resources/application-nacos.yml new file mode 100644 index 000000000..5f352bc1e --- /dev/null +++ b/examples/cloud-grpc-client/src/main/resources/application-nacos.yml @@ -0,0 +1,6 @@ +spring: + cloud: + nacos: + discovery: + register-enabled: false +# server-addr: localhost:8848 diff --git a/examples/cloud-grpc-client/src/main/resources/application-zookeeper.yml b/examples/cloud-grpc-client/src/main/resources/application-zookeeper.yml new file mode 100644 index 000000000..712947cb4 --- /dev/null +++ b/examples/cloud-grpc-client/src/main/resources/application-zookeeper.yml @@ -0,0 +1,6 @@ +spring: + cloud: + zookeeper: +# connect-string: localhost:2181 + discovery: + registry: false diff --git a/examples/cloud-grpc-client/src/main/resources/application.yml b/examples/cloud-grpc-client/src/main/resources/application.yml new file mode 100644 index 000000000..10fc2ce6a --- /dev/null +++ b/examples/cloud-grpc-client/src/main/resources/application.yml @@ -0,0 +1,12 @@ +server: + port: 8080 +spring: + application: + name: cloud-grpc-client +grpc: + client: + cloud-grpc-server: + address: 'discovery:///cloud-grpc-server' + enableKeepAlive: true + keepAliveWithoutCalls: true + negotiationType: plaintext diff --git a/examples/cloud-grpc-server/build.gradle b/examples/cloud-grpc-server/build.gradle new file mode 100644 index 000000000..41dee86f7 --- /dev/null +++ b/examples/cloud-grpc-server/build.gradle @@ -0,0 +1,37 @@ +plugins { + id 'org.springframework.boot' +} + +def discoveryProvider = project.findProperty('discovery') ?: 'consul'; + +dependencies { + switch (discoveryProvider) { + case "nacos": { + implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery' + break; + } + case "consul": { + implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery' + break; + } + case "eureka": { + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + break; + } + case "zookeeper": { + implementation 'org.springframework.cloud:spring-cloud-starter-zookeeper-discovery' + break; + } + } + + implementation project(':grpc-server-spring-boot-starter') // Replace with actual dependency "net.devh:grpc-server-spring-boot-starter:${springBootGrpcVersion}" + implementation project(':examples:grpc-lib') // Replace with your grpc interface spec + + // For demonstration only + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-web' +} + +bootRun { + args = ["--spring.profiles.active=" + discoveryProvider] +} diff --git a/examples/cloud-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/CloudServerApplication.java b/examples/cloud-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/CloudServerApplication.java new file mode 100644 index 000000000..19319b769 --- /dev/null +++ b/examples/cloud-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/CloudServerApplication.java @@ -0,0 +1,40 @@ +/* + * 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.examples.cloud.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * Example grpc service application supporting cloud discovery. + */ +@EnableDiscoveryClient +@SpringBootApplication +public class CloudServerApplication { + + /** + * Starts the grpc cloud server application. + * + * @param args The arguments to pass to the application. + */ + public static void main(final String... args) { + SpringApplication.run(CloudServerApplication.class, args); + } + +} diff --git a/examples/cloud-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GlobalInterceptorConfiguration.java b/examples/cloud-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GlobalInterceptorConfiguration.java new file mode 100644 index 000000000..11387ea34 --- /dev/null +++ b/examples/cloud-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GlobalInterceptorConfiguration.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.examples.cloud.server; + +import org.springframework.context.annotation.Configuration; + +import io.grpc.ServerInterceptor; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; + +/** + * Example configuration class that adds a {@link ServerInterceptor} to the global interceptor list. + */ +@Configuration(proxyBeanMethods = false) +public class GlobalInterceptorConfiguration { + + /** + * Creates a new {@link LogGrpcInterceptor} bean and adds it to the global interceptor list. As an alternative you + * can directly annotate the {@code LogGrpcInterceptor} class and it will automatically be picked up by spring's + * classpath scanning. + * + * @return The newly created bean. + */ + @GrpcGlobalServerInterceptor + LogGrpcInterceptor logServerInterceptor() { + return new LogGrpcInterceptor(); + } + +} diff --git a/examples/cloud-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GrpcServerService.java b/examples/cloud-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GrpcServerService.java new file mode 100644 index 000000000..64b88c29d --- /dev/null +++ b/examples/cloud-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GrpcServerService.java @@ -0,0 +1,39 @@ +/* + * 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.examples.cloud.server; + +import io.grpc.stub.StreamObserver; +import net.devh.boot.grpc.examples.lib.HelloReply; +import net.devh.boot.grpc.examples.lib.HelloRequest; +import net.devh.boot.grpc.examples.lib.SimpleGrpc; +import net.devh.boot.grpc.server.service.GrpcService; + +/** + * Example grpc server service implementation class. + */ +@GrpcService +public class GrpcServerService extends SimpleGrpc.SimpleImplBase { + + @Override + public void sayHello(final HelloRequest req, final StreamObserver responseObserver) { + final HelloReply reply = HelloReply.newBuilder().setMessage("Hello ==> " + req.getName()).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + +} diff --git a/examples/cloud-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/LogGrpcInterceptor.java b/examples/cloud-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/LogGrpcInterceptor.java new file mode 100644 index 000000000..138102a6e --- /dev/null +++ b/examples/cloud-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/LogGrpcInterceptor.java @@ -0,0 +1,49 @@ +/* + * 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.examples.cloud.server; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; + +/** + * Example {@link ServerInterceptor} that logs all called methods. In this example it is added to Spring's application + * context via {@link GlobalInterceptorConfiguration}, but is also possible to directly annotate this class with + * {@link GrpcGlobalServerInterceptor}. + */ +// @GrpcGlobalServerInterceptor +public class LogGrpcInterceptor implements ServerInterceptor { + + private static final Logger log = LoggerFactory.getLogger(LogGrpcInterceptor.class); + + @Override + public ServerCall.Listener interceptCall( + ServerCall serverCall, + Metadata metadata, + ServerCallHandler serverCallHandler) { + + log.info(serverCall.getMethodDescriptor().getFullMethodName()); + return serverCallHandler.startCall(serverCall, metadata); + } + +} diff --git a/examples/cloud-grpc-server/src/main/resources/application-consul.yml b/examples/cloud-grpc-server/src/main/resources/application-consul.yml new file mode 100644 index 000000000..1113caba6 --- /dev/null +++ b/examples/cloud-grpc-server/src/main/resources/application-consul.yml @@ -0,0 +1,7 @@ +#spring: +# cloud: +# consul: +# discovery: +# register: true +# hostname: localhost +# port: 8500 diff --git a/examples/cloud-grpc-server/src/main/resources/application-eureka.yml b/examples/cloud-grpc-server/src/main/resources/application-eureka.yml new file mode 100644 index 000000000..721b5b88a --- /dev/null +++ b/examples/cloud-grpc-server/src/main/resources/application-eureka.yml @@ -0,0 +1,8 @@ +eureka: + instance: + prefer-ip-address: true + client: + register-with-eureka: true + fetch-registry: false + service-url: + defaultZone: http://localhost:8761/eureka/ diff --git a/examples/cloud-grpc-server/src/main/resources/application-nacos.yml b/examples/cloud-grpc-server/src/main/resources/application-nacos.yml new file mode 100644 index 000000000..161d7399f --- /dev/null +++ b/examples/cloud-grpc-server/src/main/resources/application-nacos.yml @@ -0,0 +1,6 @@ +#spring: +# cloud: +# nacos: +# discovery: +# register-enabled: true +# server-addr: localhost:8848 diff --git a/examples/cloud-grpc-server/src/main/resources/application-zookeeper.yml b/examples/cloud-grpc-server/src/main/resources/application-zookeeper.yml new file mode 100644 index 000000000..38550ce0d --- /dev/null +++ b/examples/cloud-grpc-server/src/main/resources/application-zookeeper.yml @@ -0,0 +1,6 @@ +#spring: +# cloud: +# zookeeper: +# connect-string: localhost:2181 +# discovery: +# register: true diff --git a/examples/cloud-grpc-server/src/main/resources/application.yml b/examples/cloud-grpc-server/src/main/resources/application.yml new file mode 100644 index 000000000..e0c341b78 --- /dev/null +++ b/examples/cloud-grpc-server/src/main/resources/application.yml @@ -0,0 +1,10 @@ +server: + port: 58080 + +spring: + application: + name: cloud-grpc-server + +grpc: + server: + port: 0 diff --git a/examples/cloud-sleuth-zipkin-grpc-client/build.gradle b/examples/cloud-sleuth-zipkin-grpc-client/build.gradle new file mode 100644 index 000000000..f0f8f2323 --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-client/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'org.springframework.boot' +} + +def discoveryProvider = project.findProperty('discovery') ?: 'consul'; + +dependencies { + switch (discoveryProvider) { + case "nacos": { + implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery' + break; + } + case "consul": { + implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery' + break; + } + case "eureka": { + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + break; + } + case "zookeeper": { + implementation 'org.springframework.cloud:spring-cloud-starter-zookeeper-discovery' + break; + } + } + + implementation project(':grpc-client-spring-boot-starter') // Replace with actual dependency "net.devh:grpc-client-spring-boot-starter:${springBootGrpcVersion}" + implementation project(':examples:grpc-lib') // Replace with your grpc interface spec + + // For demonstration only + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-web' + + // zipkin and sleuth + implementation "org.springframework.cloud:spring-cloud-starter-zipkin:2.2.7.RELEASE" + implementation "org.springframework.cloud:spring-cloud-starter-sleuth" + implementation "io.zipkin.brave:brave-instrumentation-grpc:5.13.2" +} + +bootRun { + args = ["--spring.profiles.active=" + discoveryProvider] +} diff --git a/examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/CloudSleuthClientApplication.java b/examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/CloudSleuthClientApplication.java new file mode 100644 index 000000000..b6916fc21 --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/CloudSleuthClientApplication.java @@ -0,0 +1,40 @@ +/* + * 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.examples.cloud.client; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * Example grpc client application using cloud discovery. + */ +@EnableDiscoveryClient +@SpringBootApplication +public class CloudSleuthClientApplication { + + /** + * Starts the grpc cloud discovery client application. + * + * @param args The arguments to pass to the application. + */ + public static void main(final String... args) { + SpringApplication.run(CloudSleuthClientApplication.class, args); + } + +} diff --git a/examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcClientController.java b/examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcClientController.java new file mode 100644 index 000000000..0e013e5c6 --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcClientController.java @@ -0,0 +1,39 @@ +/* + * 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.examples.cloud.client; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * Optional demo controller making the grpc service accessible via browser requests. + */ +@RestController +public class GrpcClientController { + + @Autowired + private GrpcClientService grpcClientService; + + @RequestMapping("/") + public String printMessage(@RequestParam(defaultValue = "Michael") final String name) { + return this.grpcClientService.sendMessage(name); + } + +} diff --git a/examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcClientService.java b/examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcClientService.java new file mode 100644 index 000000000..015445bc5 --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcClientService.java @@ -0,0 +1,49 @@ +/* + * 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.examples.cloud.client; + +import org.springframework.stereotype.Service; + +import io.grpc.StatusRuntimeException; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.examples.lib.HelloReply; +import net.devh.boot.grpc.examples.lib.HelloRequest; +import net.devh.boot.grpc.examples.lib.SimpleGrpc.SimpleBlockingStub; + +/** + * Example class demonstrating the usage of {@link GrpcClient}s inside an application. + */ +@Service +@Slf4j +public class GrpcClientService { + + @GrpcClient("cloud-grpc-server") + private SimpleBlockingStub simpleStub; + + public String sendMessage(final String name) { + try { + final HelloReply response = this.simpleStub.sayHello(HelloRequest.newBuilder().setName(name).build()); + return response.getMessage(); + } catch (final StatusRuntimeException e) { + log.error("Request failed", e); + return "FAILED with " + e.getStatus().getCode(); + } + } + +} diff --git a/examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcSleuthClientConfig.java b/examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcSleuthClientConfig.java new file mode 100644 index 000000000..30c2a567d --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcSleuthClientConfig.java @@ -0,0 +1,84 @@ +/* + * 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.examples.cloud.client; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import brave.Span; +import brave.Tracing; +import brave.grpc.GrpcTracing; +import io.grpc.ClientInterceptor; +import io.grpc.ServerInterceptor; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorConfigurer; +import zipkin2.reporter.Reporter; + +/** + * Example configuration class that add grpc client sleuth/brave and zipkin integration + */ +@ConditionalOnProperty(value = {"spring.sleuth.enabled", "spring.zipkin.enabled"}, havingValue = "true") +@Configuration +@Slf4j +public class GrpcSleuthClientConfig { + + @Bean + public GrpcTracing grpcTracing(Tracing tracing) { + return GrpcTracing.create(tracing); + } + + /** + * We also create a client-side interceptor and put that in the context, this interceptor can then be injected into + * gRPC clients and then applied to the managed channel. + * + * @param grpcTracing + * @return + */ + @Bean + ClientInterceptor grpcClientSleuthInterceptor(GrpcTracing grpcTracing) { + return grpcTracing.newClientInterceptor(); + } + + @Bean + ServerInterceptor grpcServerSleuthInterceptor(GrpcTracing grpcTracing) { + return grpcTracing.newServerInterceptor(); + } + + /** + * Use this for debugging (or if there is no Zipkin server running on port 9411) + * + * @return + */ + @Bean + public Reporter spanReporter() { + return span -> { + if (log.isDebugEnabled()) { + log.debug("{}", span); + } + }; + } + + @Bean + public GlobalClientInterceptorConfigurer globalInterceptorConfigurerAdapter( + ClientInterceptor grpcClientSleuthInterceptor) { + return registry -> registry.add(grpcClientSleuthInterceptor); + } + + +} diff --git a/examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application-consul.yml b/examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application-consul.yml new file mode 100644 index 000000000..a8894a62e --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application-consul.yml @@ -0,0 +1,7 @@ +spring: + cloud: + consul: + discovery: + register: false +# hostname: localhost +# port: 8500 diff --git a/examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application-eureka.yml b/examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application-eureka.yml new file mode 100644 index 000000000..3290476e4 --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application-eureka.yml @@ -0,0 +1,10 @@ +eureka: + instance: + prefer-ip-address: true + status-page-url-path: /actuator/info + health-check-url-path: /actuator/health + client: + register-with-eureka: false + fetch-registry: true + service-url: + defaultZone: http://localhost:8761/eureka/ diff --git a/examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application-nacos.yml b/examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application-nacos.yml new file mode 100644 index 000000000..5f352bc1e --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application-nacos.yml @@ -0,0 +1,6 @@ +spring: + cloud: + nacos: + discovery: + register-enabled: false +# server-addr: localhost:8848 diff --git a/examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application-zookeeper.yml b/examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application-zookeeper.yml new file mode 100644 index 000000000..712947cb4 --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application-zookeeper.yml @@ -0,0 +1,6 @@ +spring: + cloud: + zookeeper: +# connect-string: localhost:2181 + discovery: + registry: false diff --git a/examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application.yml b/examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application.yml new file mode 100644 index 000000000..3132a9add --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-client/src/main/resources/application.yml @@ -0,0 +1,19 @@ +server: + port: 8080 +spring: + application: + name: cloud-sleuth-zipkin-grpc-client + zipkin: + base-url: http://localhost:9411 + enabled: true + sleuth: + sampler: + probability: 1 + +grpc: + client: + cloud-grpc-server: + address: 'discovery:///cloud-sleuth-zipkin-grpc-server' + enableKeepAlive: true + keepAliveWithoutCalls: true + negotiationType: plaintext diff --git a/examples/cloud-sleuth-zipkin-grpc-server/build.gradle b/examples/cloud-sleuth-zipkin-grpc-server/build.gradle new file mode 100644 index 000000000..45ca16789 --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-server/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'org.springframework.boot' +} + +def discoveryProvider = project.findProperty('discovery') ?: 'consul'; + +dependencies { + switch (discoveryProvider) { + case "nacos": { + implementation 'com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery' + break; + } + case "consul": { + implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery' + break; + } + case "eureka": { + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + break; + } + case "zookeeper": { + implementation 'org.springframework.cloud:spring-cloud-starter-zookeeper-discovery' + break; + } + } + + implementation project(':grpc-server-spring-boot-starter') // Replace with actual dependency "net.devh:grpc-server-spring-boot-starter:${springBootGrpcVersion}" + implementation project(':examples:grpc-lib') // Replace with your grpc interface spec + + // For demonstration only + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-web' + + // zipkin and sleuth + implementation "org.springframework.cloud:spring-cloud-starter-zipkin:2.2.7.RELEASE" + implementation "org.springframework.cloud:spring-cloud-starter-sleuth" + implementation "io.zipkin.brave:brave-instrumentation-grpc:5.13.2" +} + +bootRun { + args = ["--spring.profiles.active=" + discoveryProvider] +} diff --git a/examples/cloud-sleuth-zipkin-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/CloudSleuthServerApplication.java b/examples/cloud-sleuth-zipkin-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/CloudSleuthServerApplication.java new file mode 100644 index 000000000..d01a490b3 --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/CloudSleuthServerApplication.java @@ -0,0 +1,40 @@ +/* + * 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.examples.cloud.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.client.discovery.EnableDiscoveryClient; + +/** + * Example grpc service application supporting cloud discovery. + */ +@EnableDiscoveryClient +@SpringBootApplication +public class CloudSleuthServerApplication { + + /** + * Starts the grpc cloud server application. + * + * @param args The arguments to pass to the application. + */ + public static void main(final String... args) { + SpringApplication.run(CloudSleuthServerApplication.class, args); + } + +} diff --git a/examples/cloud-sleuth-zipkin-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GrpcServerService.java b/examples/cloud-sleuth-zipkin-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GrpcServerService.java new file mode 100644 index 000000000..64b88c29d --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GrpcServerService.java @@ -0,0 +1,39 @@ +/* + * 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.examples.cloud.server; + +import io.grpc.stub.StreamObserver; +import net.devh.boot.grpc.examples.lib.HelloReply; +import net.devh.boot.grpc.examples.lib.HelloRequest; +import net.devh.boot.grpc.examples.lib.SimpleGrpc; +import net.devh.boot.grpc.server.service.GrpcService; + +/** + * Example grpc server service implementation class. + */ +@GrpcService +public class GrpcServerService extends SimpleGrpc.SimpleImplBase { + + @Override + public void sayHello(final HelloRequest req, final StreamObserver responseObserver) { + final HelloReply reply = HelloReply.newBuilder().setMessage("Hello ==> " + req.getName()).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + +} diff --git a/examples/cloud-sleuth-zipkin-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GrpcSleuthServerConfig.java b/examples/cloud-sleuth-zipkin-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GrpcSleuthServerConfig.java new file mode 100644 index 000000000..ffe686eed --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GrpcSleuthServerConfig.java @@ -0,0 +1,70 @@ +/* + * 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.examples.cloud.server; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import brave.Span; +import brave.Tracing; +import brave.grpc.GrpcTracing; +import io.grpc.ServerInterceptor; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.interceptor.GlobalServerInterceptorConfigurer; +import zipkin2.reporter.Reporter; + +/** + * Example configuration class that add grpc server sleuth/brave and zipkin integration + */ +@ConditionalOnProperty(value = {"spring.sleuth.enabled", "spring.zipkin.enabled"}, havingValue = "true") +@Configuration +@Slf4j +public class GrpcSleuthServerConfig { + + @Bean + public GrpcTracing grpcTracing(Tracing tracing) { + return GrpcTracing.create(tracing); + } + + @Bean + ServerInterceptor grpcServerSleuthInterceptor(GrpcTracing grpcTracing) { + return grpcTracing.newServerInterceptor(); + } + + /** + * Use this for debugging (or if there is no Zipkin server running on port 9411) + * + * @return + */ + @Bean + public Reporter spanReporter() { + return span -> { + if (log.isDebugEnabled()) { + log.debug("{}", span); + } + }; + } + + @Bean + public GlobalServerInterceptorConfigurer globalInterceptorConfigurerAdapter( + ServerInterceptor grpcServerSleuthInterceptor) { + return registry -> registry.add(grpcServerSleuthInterceptor); + } + +} diff --git a/examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application-consul.yml b/examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application-consul.yml new file mode 100644 index 000000000..1113caba6 --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application-consul.yml @@ -0,0 +1,7 @@ +#spring: +# cloud: +# consul: +# discovery: +# register: true +# hostname: localhost +# port: 8500 diff --git a/examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application-eureka.yml b/examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application-eureka.yml new file mode 100644 index 000000000..721b5b88a --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application-eureka.yml @@ -0,0 +1,8 @@ +eureka: + instance: + prefer-ip-address: true + client: + register-with-eureka: true + fetch-registry: false + service-url: + defaultZone: http://localhost:8761/eureka/ diff --git a/examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application-nacos.yml b/examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application-nacos.yml new file mode 100644 index 000000000..161d7399f --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application-nacos.yml @@ -0,0 +1,6 @@ +#spring: +# cloud: +# nacos: +# discovery: +# register-enabled: true +# server-addr: localhost:8848 diff --git a/examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application-zookeeper.yml b/examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application-zookeeper.yml new file mode 100644 index 000000000..38550ce0d --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application-zookeeper.yml @@ -0,0 +1,6 @@ +#spring: +# cloud: +# zookeeper: +# connect-string: localhost:2181 +# discovery: +# register: true diff --git a/examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application.yml b/examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application.yml new file mode 100644 index 000000000..598f45af9 --- /dev/null +++ b/examples/cloud-sleuth-zipkin-grpc-server/src/main/resources/application.yml @@ -0,0 +1,15 @@ +server: + port: 58080 + +spring: + application: + name: cloud-sleuth-zipkin-grpc-server + zipkin: + base-url: http://localhost:9411 + enabled: true + sleuth: + sampler: + probability: 1 +grpc: + server: + port: 0 diff --git a/examples/grpc-lib/build.gradle b/examples/grpc-lib/build.gradle new file mode 100644 index 000000000..88ad0e3d5 --- /dev/null +++ b/examples/grpc-lib/build.gradle @@ -0,0 +1,56 @@ +plugins { + id 'com.google.protobuf' +} + +dependencies { + implementation 'io.grpc:grpc-netty-shaded' + implementation 'io.grpc:grpc-protobuf' + implementation 'io.grpc:grpc-stub' + if (JavaVersion.current().isJava9Compatible()) { + // Workaround for @javax.annotation.Generated + // see: https://github.com/grpc/grpc-java/issues/3633 + implementation 'jakarta.annotation:jakarta.annotation-api' + } +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${protobufVersion}" + } + generatedFilesBaseDir = "$projectDir/src/generated" + clean { + delete generatedFilesBaseDir + } + plugins { + grpc { + artifact = 'io.grpc:protoc-gen-grpc-java' + } + } + generateProtoTasks { + all()*.plugins { + grpc {} + } + } +} + +eclipse { + classpath { + file.beforeMerged { cp -> + def generatedGrpcFolder = new org.gradle.plugins.ide.eclipse.model.SourceFolder('src/generated/main/grpc', null); + generatedGrpcFolder.entryAttributes['ignore_optional_problems'] = 'true'; + cp.entries.add( generatedGrpcFolder ); + def generatedJavaFolder = new org.gradle.plugins.ide.eclipse.model.SourceFolder('src/generated/main/java', null); + generatedJavaFolder.entryAttributes['ignore_optional_problems'] = 'true'; + cp.entries.add( generatedJavaFolder ); + } + } +} + +idea { + module { + sourceDirs += file('src/generated/main/java') + sourceDirs += file('src/generated/main/grpc') + generatedSourceDirs += file('src/generated/main/java') + generatedSourceDirs += file('src/generated/main/grpc') + } +} diff --git a/examples/grpc-lib/src/main/proto/helloworld.proto b/examples/grpc-lib/src/main/proto/helloworld.proto new file mode 100644 index 000000000..8e0563c79 --- /dev/null +++ b/examples/grpc-lib/src/main/proto/helloworld.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "net.devh.boot.grpc.examples.lib"; +option java_outer_classname = "HelloWorldProto"; + +// The greeting service definition. +service Simple { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) { + } +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} diff --git a/examples/local-grpc-client/build.gradle b/examples/local-grpc-client/build.gradle new file mode 100644 index 000000000..4036f3dd9 --- /dev/null +++ b/examples/local-grpc-client/build.gradle @@ -0,0 +1,9 @@ +plugins { + id 'org.springframework.boot' +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation project(':grpc-client-spring-boot-starter') // replace to implementation("net.devh:grpc-client-spring-boot-starter:${springBootGrpcVersion}") + implementation project(':examples:grpc-lib') +} diff --git a/examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/GlobalClientInterceptorConfiguration.java b/examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/GlobalClientInterceptorConfiguration.java new file mode 100644 index 000000000..620b625b2 --- /dev/null +++ b/examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/GlobalClientInterceptorConfiguration.java @@ -0,0 +1,35 @@ +/* + * 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.examples.local.client; + +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor; + +@Order(Ordered.LOWEST_PRECEDENCE) +@Configuration(proxyBeanMethods = false) +public class GlobalClientInterceptorConfiguration { + + @GrpcGlobalClientInterceptor + LogGrpcInterceptor logClientInterceptor() { + return new LogGrpcInterceptor(); + } + +} diff --git a/examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/GrpcClientController.java b/examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/GrpcClientController.java new file mode 100644 index 000000000..055143103 --- /dev/null +++ b/examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/GrpcClientController.java @@ -0,0 +1,40 @@ +/* + * 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.examples.local.client; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Michael (yidongnan@gmail.com) + * @since 2016/12/4 + */ +@RestController +public class GrpcClientController { + + @Autowired + private GrpcClientService grpcClientService; + + @RequestMapping("/") + public String printMessage(@RequestParam(defaultValue = "Michael") String name) { + return grpcClientService.sendMessage(name); + } + +} diff --git a/examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/GrpcClientService.java b/examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/GrpcClientService.java new file mode 100644 index 000000000..a9343b74e --- /dev/null +++ b/examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/GrpcClientService.java @@ -0,0 +1,47 @@ +/* + * 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.examples.local.client; + +import org.springframework.stereotype.Service; + +import io.grpc.StatusRuntimeException; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.examples.lib.HelloReply; +import net.devh.boot.grpc.examples.lib.HelloRequest; +import net.devh.boot.grpc.examples.lib.SimpleGrpc.SimpleBlockingStub; + +/** + * @author Michael (yidongnan@gmail.com) + * @since 2016/11/8 + */ +@Service +public class GrpcClientService { + + @GrpcClient("local-grpc-server") + private SimpleBlockingStub simpleStub; + + public String sendMessage(final String name) { + try { + final HelloReply response = this.simpleStub.sayHello(HelloRequest.newBuilder().setName(name).build()); + return response.getMessage(); + } catch (final StatusRuntimeException e) { + return "FAILED with " + e.getStatus().getCode().name(); + } + } + +} diff --git a/examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/LocalGrpcClientApplication.java b/examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/LocalGrpcClientApplication.java new file mode 100644 index 000000000..63c3b0313 --- /dev/null +++ b/examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/LocalGrpcClientApplication.java @@ -0,0 +1,34 @@ +/* + * 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.examples.local.client; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Michael (yidongnan@gmail.com) + * @since 2016/11/8 + */ +@SpringBootApplication +public class LocalGrpcClientApplication { + + public static void main(String[] args) { + SpringApplication.run(LocalGrpcClientApplication.class, args); + } + +} diff --git a/examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/LogGrpcInterceptor.java b/examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/LogGrpcInterceptor.java new file mode 100644 index 000000000..5b57dde73 --- /dev/null +++ b/examples/local-grpc-client/src/main/java/net/devh/boot/grpc/examples/local/client/LogGrpcInterceptor.java @@ -0,0 +1,44 @@ +/* + * 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.examples.local.client; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; + +/** + * @author Michael (yidongnan@gmail.com) + * @since 2016/12/8 + */ +public class LogGrpcInterceptor implements ClientInterceptor { + + private static final Logger log = LoggerFactory.getLogger(LogGrpcInterceptor.class); + + @Override + public ClientCall interceptCall(MethodDescriptor method, + CallOptions callOptions, Channel next) { + log.info(method.getFullMethodName()); + return next.newCall(method, callOptions); + } + +} diff --git a/examples/local-grpc-client/src/main/resources/application.yml b/examples/local-grpc-client/src/main/resources/application.yml new file mode 100644 index 000000000..a9e7d5702 --- /dev/null +++ b/examples/local-grpc-client/src/main/resources/application.yml @@ -0,0 +1,13 @@ +server: + port: 8080 +spring: + application: + name: local-grpc-client + +grpc: + client: + local-grpc-server: + address: 'static://127.0.0.1:9898' + enableKeepAlive: true + keepAliveWithoutCalls: true + negotiationType: plaintext diff --git a/examples/local-grpc-server/build.gradle b/examples/local-grpc-server/build.gradle new file mode 100644 index 000000000..4536cb86a --- /dev/null +++ b/examples/local-grpc-server/build.gradle @@ -0,0 +1,9 @@ +plugins { + id 'org.springframework.boot' +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter' + implementation project(':grpc-server-spring-boot-starter') // replace to implementation("net.devh:grpc-server-spring-boot-starter:${springBootGrpcVersion}") + implementation project(':examples:grpc-lib') +} diff --git a/examples/local-grpc-server/src/main/java/net/devh/boot/grpc/examples/local/server/GlobalInterceptorConfiguration.java b/examples/local-grpc-server/src/main/java/net/devh/boot/grpc/examples/local/server/GlobalInterceptorConfiguration.java new file mode 100644 index 000000000..b848071a5 --- /dev/null +++ b/examples/local-grpc-server/src/main/java/net/devh/boot/grpc/examples/local/server/GlobalInterceptorConfiguration.java @@ -0,0 +1,32 @@ +/* + * 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.examples.local.server; + +import org.springframework.context.annotation.Configuration; + +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; + +@Configuration(proxyBeanMethods = false) +public class GlobalInterceptorConfiguration { + + @GrpcGlobalServerInterceptor + LogGrpcInterceptor logServerInterceptor() { + return new LogGrpcInterceptor(); + } + +} diff --git a/examples/local-grpc-server/src/main/java/net/devh/boot/grpc/examples/local/server/GrpcServerService.java b/examples/local-grpc-server/src/main/java/net/devh/boot/grpc/examples/local/server/GrpcServerService.java new file mode 100644 index 000000000..7fe39e3ad --- /dev/null +++ b/examples/local-grpc-server/src/main/java/net/devh/boot/grpc/examples/local/server/GrpcServerService.java @@ -0,0 +1,41 @@ +/* + * 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.examples.local.server; + +import io.grpc.stub.StreamObserver; +import net.devh.boot.grpc.examples.lib.HelloReply; +import net.devh.boot.grpc.examples.lib.HelloRequest; +import net.devh.boot.grpc.examples.lib.SimpleGrpc; +import net.devh.boot.grpc.server.service.GrpcService; + +/** + * @author Michael (yidongnan@gmail.com) + * @since 2016/11/8 + */ + +@GrpcService +public class GrpcServerService extends SimpleGrpc.SimpleImplBase { + + @Override + public void sayHello(HelloRequest req, StreamObserver responseObserver) { + HelloReply reply = HelloReply.newBuilder().setMessage("Hello ==> " + req.getName()).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + +} diff --git a/examples/local-grpc-server/src/main/java/net/devh/boot/grpc/examples/local/server/LocalGrpcServerApplication.java b/examples/local-grpc-server/src/main/java/net/devh/boot/grpc/examples/local/server/LocalGrpcServerApplication.java new file mode 100644 index 000000000..e886d1c2d --- /dev/null +++ b/examples/local-grpc-server/src/main/java/net/devh/boot/grpc/examples/local/server/LocalGrpcServerApplication.java @@ -0,0 +1,34 @@ +/* + * 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.examples.local.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @author Michael (yidongnan@gmail.com) + * @since 2016/11/8 + */ +@SpringBootApplication +public class LocalGrpcServerApplication { + + public static void main(String[] args) { + SpringApplication.run(LocalGrpcServerApplication.class, args); + } + +} diff --git a/examples/local-grpc-server/src/main/java/net/devh/boot/grpc/examples/local/server/LogGrpcInterceptor.java b/examples/local-grpc-server/src/main/java/net/devh/boot/grpc/examples/local/server/LogGrpcInterceptor.java new file mode 100644 index 000000000..86fbb8b28 --- /dev/null +++ b/examples/local-grpc-server/src/main/java/net/devh/boot/grpc/examples/local/server/LogGrpcInterceptor.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.examples.local.server; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; + +/** + * @author Michael (yidongnan@gmail.com) + * @since 2016/12/6 + */ +public class LogGrpcInterceptor implements ServerInterceptor { + + private static final Logger log = LoggerFactory.getLogger(LogGrpcInterceptor.class); + + @Override + public ServerCall.Listener interceptCall(ServerCall serverCall, Metadata metadata, + ServerCallHandler serverCallHandler) { + log.info(serverCall.getMethodDescriptor().getFullMethodName()); + return serverCallHandler.startCall(serverCall, metadata); + } + +} diff --git a/examples/local-grpc-server/src/main/resources/application.yml b/examples/local-grpc-server/src/main/resources/application.yml new file mode 100644 index 000000000..2ce64ebad --- /dev/null +++ b/examples/local-grpc-server/src/main/resources/application.yml @@ -0,0 +1,6 @@ +spring: + application: + name: local-grpc-server +grpc: + server: + port: 9898 diff --git a/examples/security-grpc-client/build.gradle b/examples/security-grpc-client/build.gradle new file mode 100644 index 000000000..6d78151ba --- /dev/null +++ b/examples/security-grpc-client/build.gradle @@ -0,0 +1,10 @@ +plugins { + id 'org.springframework.boot' +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.security:spring-security-core' + implementation project(':grpc-client-spring-boot-starter') // replace to implementation "net.devh:grpc-client-spring-boot-starter:${springBootGrpcVersion}" + implementation project(':examples:grpc-lib') +} diff --git a/examples/security-grpc-client/src/main/java/net/devh/boot/grpc/examples/security/client/GrpcClientController.java b/examples/security-grpc-client/src/main/java/net/devh/boot/grpc/examples/security/client/GrpcClientController.java new file mode 100644 index 000000000..cc3365c97 --- /dev/null +++ b/examples/security-grpc-client/src/main/java/net/devh/boot/grpc/examples/security/client/GrpcClientController.java @@ -0,0 +1,53 @@ +/* + * 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.examples.security.client; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +/** + * A simple rest controller that can be used to send a request and show its response. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@RestController +public class GrpcClientController { + + @Value("${auth.username}") + private String username; + + @Autowired + private GrpcClientService grpcClientService; + + @RequestMapping(path = "/", produces = MediaType.TEXT_PLAIN_VALUE) + public String printMessage(@RequestParam(defaultValue = "Michael") final String name) { + final StringBuilder sb = new StringBuilder(); + sb.append("Input:\n") + .append("- name: " + name + " (Changeable via URL param ?name=X)\n") + .append("Request-Context:\n") + .append("- auth user: " + this.username + " (Configure via application.yml)\n") + .append("Response:\n") + .append(this.grpcClientService.sendMessage(name)); + return sb.toString(); + } + +} diff --git a/examples/security-grpc-client/src/main/java/net/devh/boot/grpc/examples/security/client/GrpcClientService.java b/examples/security-grpc-client/src/main/java/net/devh/boot/grpc/examples/security/client/GrpcClientService.java new file mode 100644 index 000000000..8f3876a45 --- /dev/null +++ b/examples/security-grpc-client/src/main/java/net/devh/boot/grpc/examples/security/client/GrpcClientService.java @@ -0,0 +1,54 @@ +/* + * 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.examples.security.client; + +import org.springframework.stereotype.Service; + +import io.grpc.StatusRuntimeException; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.examples.lib.HelloReply; +import net.devh.boot.grpc.examples.lib.HelloRequest; +import net.devh.boot.grpc.examples.lib.SimpleGrpc.SimpleBlockingStub; + +/** + * A dummy grpc client service that will call the secured grpc service. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Service +public class GrpcClientService { + + @GrpcClient("security-grpc-server") + private SimpleBlockingStub simpleStub; + + /** + * Send a message to the secured grpc service. + * + * @param name The name of the caller. + * @return The response from the server or an failure message. + */ + public String sendMessage(final String name) { + try { + final HelloReply response = this.simpleStub.sayHello(HelloRequest.newBuilder().setName(name).build()); + return response.getMessage(); + } catch (final StatusRuntimeException e) { + return "FAILED with " + e.getStatus().getCode().name(); + } + } + +} diff --git a/examples/security-grpc-client/src/main/java/net/devh/boot/grpc/examples/security/client/SecurityConfiguration.java b/examples/security-grpc-client/src/main/java/net/devh/boot/grpc/examples/security/client/SecurityConfiguration.java new file mode 100644 index 000000000..32b3f42ff --- /dev/null +++ b/examples/security-grpc-client/src/main/java/net/devh/boot/grpc/examples/security/client/SecurityConfiguration.java @@ -0,0 +1,47 @@ +/* + * 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.examples.security.client; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.grpc.CallCredentials; +import net.devh.boot.grpc.client.inject.StubTransformer; +import net.devh.boot.grpc.client.security.CallCredentialsHelper; + +/** + * The security configuration for the client. In this case we assume that we use the same passwords for all stubs. If + * you need per stub credentials you can delete the grpcCredentials and define a {@link StubTransformer} yourself. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + * @see CallCredentialsHelper + */ +@Configuration(proxyBeanMethods = false) +public class SecurityConfiguration { + + @Value("${auth.username}") + private String username; + + @Bean + // Create credentials for username + password. + CallCredentials grpcCredentials() { + return CallCredentialsHelper.basicAuth(this.username, this.username + "Password"); + } + +} diff --git a/examples/security-grpc-client/src/main/java/net/devh/boot/grpc/examples/security/client/SecurityGrpcClientApplication.java b/examples/security-grpc-client/src/main/java/net/devh/boot/grpc/examples/security/client/SecurityGrpcClientApplication.java new file mode 100644 index 000000000..68d04f234 --- /dev/null +++ b/examples/security-grpc-client/src/main/java/net/devh/boot/grpc/examples/security/client/SecurityGrpcClientApplication.java @@ -0,0 +1,35 @@ +/* + * 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.examples.security.client; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * An example client application that demonstrates basic auth security. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@SpringBootApplication +public class SecurityGrpcClientApplication { + + public static void main(final String[] args) { + SpringApplication.run(SecurityGrpcClientApplication.class, args); + } + +} diff --git a/examples/security-grpc-client/src/main/resources/application.yml b/examples/security-grpc-client/src/main/resources/application.yml new file mode 100644 index 000000000..840098653 --- /dev/null +++ b/examples/security-grpc-client/src/main/resources/application.yml @@ -0,0 +1,15 @@ +server: + port: 8080 +spring: + application: + name: security-grpc-client +# auth.username: guest +auth.username: user +# auth.username: unknown +grpc: + client: + security-grpc-server: + address: 'static://127.0.0.1:9090' + enableKeepAlive: true + keepAliveWithoutCalls: true + negotiationType: plaintext diff --git a/examples/security-grpc-server/build.gradle b/examples/security-grpc-server/build.gradle new file mode 100644 index 000000000..0b777cb3b --- /dev/null +++ b/examples/security-grpc-server/build.gradle @@ -0,0 +1,10 @@ +plugins { + id 'org.springframework.boot' +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.security:spring-security-config' + implementation project(':grpc-server-spring-boot-starter') // replace to implementation("net.devh:grpc-server-spring-boot-starter:${springBootGrpcVersion}") + implementation project(':examples:grpc-lib') +} diff --git a/examples/security-grpc-server/src/main/java/net/devh/boot/grpc/examples/security/server/GrpcServerService.java b/examples/security-grpc-server/src/main/java/net/devh/boot/grpc/examples/security/server/GrpcServerService.java new file mode 100644 index 000000000..61a01d220 --- /dev/null +++ b/examples/security-grpc-server/src/main/java/net/devh/boot/grpc/examples/security/server/GrpcServerService.java @@ -0,0 +1,45 @@ +/* + * 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.examples.security.server; + +import org.springframework.security.access.annotation.Secured; + +import io.grpc.stub.StreamObserver; +import net.devh.boot.grpc.examples.lib.HelloReply; +import net.devh.boot.grpc.examples.lib.HelloRequest; +import net.devh.boot.grpc.examples.lib.SimpleGrpc; +import net.devh.boot.grpc.server.service.GrpcService; + +/** + * An example service that checks the user's authentication. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@GrpcService +public class GrpcServerService extends SimpleGrpc.SimpleImplBase { + + // A grpc method that requests the user to be authenticated and have the role "ROLE_GREET". + @Override + @Secured("ROLE_GREET") + public void sayHello(final HelloRequest req, final StreamObserver responseObserver) { + final HelloReply reply = HelloReply.newBuilder().setMessage("Hello ==> " + req.getName()).build(); + responseObserver.onNext(reply); + responseObserver.onCompleted(); + } + +} diff --git a/examples/security-grpc-server/src/main/java/net/devh/boot/grpc/examples/security/server/SecurityConfiguration.java b/examples/security-grpc-server/src/main/java/net/devh/boot/grpc/examples/security/server/SecurityConfiguration.java new file mode 100644 index 000000000..811b72a59 --- /dev/null +++ b/examples/security-grpc-server/src/main/java/net/devh/boot/grpc/examples/security/server/SecurityConfiguration.java @@ -0,0 +1,118 @@ +/* + * 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.examples.security.server; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +import net.devh.boot.grpc.server.security.authentication.BasicGrpcAuthenticationReader; +import net.devh.boot.grpc.server.security.authentication.GrpcAuthenticationReader; + +/** + * The security configuration. If you use spring security for web applications most of the stuff is already configured. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Configuration(proxyBeanMethods = false) +// proxyTargetClass is required, if you use annotation driven security! +// However, you will receive a warning that GrpcServerService#bindService() method is final. +// You cannot avoid that warning (without massive amount of work), but it is safe to ignore it. +// The #bindService() method uses a reference to 'this', which will be used to invoke the methods. +// If the method is not final it will delegate to the original instance and thus it will bypass any security layer that +// you intend to add, unless you re-implement the #bindService() method on the outermost layer (which Spring does not). +@EnableGlobalMethodSecurity(securedEnabled = true, proxyTargetClass = true) +public class SecurityConfiguration { + + private static final Logger log = LoggerFactory.getLogger(SecurityConfiguration.class); + + @Bean + PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(10); + } + + @Bean + // This could be your database lookup. There are some complete implementations in spring-security-web. + UserDetailsService userDetailsService(final PasswordEncoder passwordEncoder) { + return username -> { + log.debug("Searching user: {}", username); + switch (username) { + case "guest": { + return new User(username, passwordEncoder.encode(username + "Password"), Collections.emptyList()); + } + case "user": { + final List authorities = + Arrays.asList(new SimpleGrantedAuthority("ROLE_GREET")); + return new User(username, passwordEncoder.encode(username + "Password"), authorities); + } + default: { + throw new UsernameNotFoundException("Could not find user!"); + } + } + }; + } + + @Bean + // One of your authentication providers. + // They ensure that the credentials are valid and populate the user's authorities. + DaoAuthenticationProvider daoAuthenticationProvider( + final UserDetailsService userDetailsService, + final PasswordEncoder passwordEncoder) { + + final DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(userDetailsService); + provider.setPasswordEncoder(passwordEncoder); + return provider; + } + + @Bean + // Add the authentication providers to the manager. + AuthenticationManager authenticationManager(final DaoAuthenticationProvider daoAuthenticationProvider) { + final List providers = new ArrayList<>(); + providers.add(daoAuthenticationProvider); + return new ProviderManager(providers); + } + + @Bean + // Configure which authentication types you support. + GrpcAuthenticationReader authenticationReader() { + return new BasicGrpcAuthenticationReader(); + // final List readers = new ArrayList<>(); + // readers.add(new BasicGrpcAuthenticationReader()); + // readers.add(new SSLContextGrpcAuthenticationReader()); + // return new CompositeGrpcAuthenticationReader(readers); + } + +} diff --git a/examples/security-grpc-server/src/main/java/net/devh/boot/grpc/examples/security/server/SecurityGrpcServerApplication.java b/examples/security-grpc-server/src/main/java/net/devh/boot/grpc/examples/security/server/SecurityGrpcServerApplication.java new file mode 100644 index 000000000..b7f4d1c47 --- /dev/null +++ b/examples/security-grpc-server/src/main/java/net/devh/boot/grpc/examples/security/server/SecurityGrpcServerApplication.java @@ -0,0 +1,35 @@ +/* + * 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.examples.security.server; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * An example server application that demonstrates basic auth security. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@SpringBootApplication +public class SecurityGrpcServerApplication { + + public static void main(final String[] args) { + SpringApplication.run(SecurityGrpcServerApplication.class, args); + } + +} diff --git a/examples/security-grpc-server/src/main/resources/application.yml b/examples/security-grpc-server/src/main/resources/application.yml new file mode 100644 index 000000000..10d32f2fa --- /dev/null +++ b/examples/security-grpc-server/src/main/resources/application.yml @@ -0,0 +1,6 @@ +spring: + application: + name: security-grpc-server +grpc: + server: + port: 9090 diff --git a/extra/eclipse/eclipse-formatter.xml b/extra/eclipse/eclipse-formatter.xml new file mode 100644 index 000000000..4d22b66ac --- /dev/null +++ b/extra/eclipse/eclipse-formatter.xml @@ -0,0 +1,337 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/extra/eclipse/eclipse.importorder b/extra/eclipse/eclipse.importorder new file mode 100644 index 000000000..4329c4128 --- /dev/null +++ b/extra/eclipse/eclipse.importorder @@ -0,0 +1,6 @@ +#Organize Import Order +0=java +1=javax +2=org +3=com +4= \ No newline at end of file diff --git a/extra/spotless/mit-license.java b/extra/spotless/mit-license.java new file mode 100644 index 000000000..9d2a893ab --- /dev/null +++ b/extra/spotless/mit-license.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2016-$YEAR 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. + */ \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..5d7f9d869 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,7 @@ +org.gradle.caching=true +# It is not possible to use parallel, +# because tests in multiple modules require the same ports +#org.gradle.parallel=true +org.gradle.vfs.watch=true +org.gradle.daemon=true + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f GIT binary patch literal 59203 zcma&O1CT9Y(k9%tZQHhO+qUh#ZQHhO+qmuS+qP|E@9xZO?0h@l{(r>DQ>P;GjjD{w zH}lENr;dU&FbEU?00aa80D$0M0RRB{U*7-#kbjS|qAG&4l5%47zyJ#WrfA#1$1Ctx zf&Z_d{GW=lf^w2#qRJ|CvSJUi(^E3iv~=^Z(zH}F)3Z%V3`@+rNB7gTVU{Bb~90p|f+0(v;nz01EG7yDMX9@S~__vVgv%rS$+?IH+oZ03D5zYrv|^ zC1J)SruYHmCki$jLBlTaE5&dFG9-kq3!^i>^UQL`%gn6)jz54$WDmeYdsBE9;PqZ_ zoGd=P4+|(-u4U1dbAVQrFWoNgNd;0nrghPFbQrJctO>nwDdI`Q^i0XJDUYm|T|RWc zZ3^Qgo_Qk$%Fvjj-G}1NB#ZJqIkh;kX%V{THPqOyiq)d)0+(r9o(qKlSp*hmK#iIY zA^)Vr$-Hz<#SF=0@tL@;dCQsm`V9s1vYNq}K1B)!XSK?=I1)tX+bUV52$YQu*0%fnWEukW>mxkz+%3-S!oguE8u#MGzST8_Dy^#U?fA@S#K$S@9msUiX!gd_ow>08w5)nX{-KxqMOo7d?k2&?Vf z&diGDtZr(0cwPe9z9FAUSD9KC)7(n^lMWuayCfxzy8EZsns%OEblHFSzP=cL6}?J| z0U$H!4S_TVjj<`6dy^2j`V`)mC;cB%* z8{>_%E1^FH!*{>4a7*C1v>~1*@TMcLK{7nEQ!_igZC}ikJ$*<$yHy>7)oy79A~#xE zWavoJOIOC$5b6*q*F_qN1>2#MY)AXVyr$6x4b=$x^*aqF*L?vmj>Mgv+|ITnw_BoW zO?jwHvNy^prH{9$rrik1#fhyU^MpFqF2fYEt(;4`Q&XWOGDH8k6M=%@fics4ajI;st# zCU^r1CK&|jzUhRMv;+W~6N;u<;#DI6cCw-otsc@IsN3MoSD^O`eNflIoR~l4*&-%RBYk@gb^|-JXs&~KuSEmMxB}xSb z@K76cXD=Y|=I&SNC2E+>Zg?R6E%DGCH5J1nU!A|@eX9oS(WPaMm==k2s_ueCqdZw| z&hqHp)47`c{BgwgvY2{xz%OIkY1xDwkw!<0veB#yF4ZKJyabhyyVS`gZepcFIk%e2 zTcrmt2@-8`7i-@5Nz>oQWFuMC_KlroCl(PLSodswHqJ3fn<;gxg9=}~3x_L3P`9Sn zChIf}8vCHvTriz~T2~FamRi?rh?>3bX1j}%bLH+uFX+p&+^aXbOK7clZxdU~6Uxgy z8R=obwO4dL%pmVo*Ktf=lH6hnlz_5k3cG;m8lgaPp~?eD!Yn2kf)tU6PF{kLyn|oI@eQ`F z3IF7~Blqg8-uwUuWZScRKn%c2_}dXB6Dx_&xR*n9M9LXasJhtZdr$vBY!rP{c@=)& z#!?L$2UrkvClwQO>U*fSMs67oSj2mxiJ$t;E|>q%Kh_GzzWWO&3;ufU%2z%ucBU8H z3WIwr$n)cfCXR&>tyB7BcSInK>=ByZA%;cVEJhcg<#6N{aZC4>K41XF>ZgjG`z_u& zGY?;Ad?-sgiOnI`oppF1o1Gurqbi*;#x2>+SSV6|1^G@ooVy@fg?wyf@0Y!UZ4!}nGuLeC^l)6pwkh|oRY`s1Pm$>zZ3u-83T|9 zGaKJIV3_x+u1>cRibsaJpJqhcm%?0-L;2 zitBrdRxNmb0OO2J%Y&Ym(6*`_P3&&5Bw157{o7LFguvxC$4&zTy#U=W*l&(Q2MNO} zfaUwYm{XtILD$3864IA_nn34oVa_g^FRuHL5wdUd)+W-p-iWCKe8m_cMHk+=? zeKX)M?Dt(|{r5t7IenkAXo%&EXIb-i^w+0CX0D=xApC=|Xy(`xy+QG^UyFe z+#J6h_&T5i#sV)hj3D4WN%z;2+jJcZxcI3*CHXGmOF3^)JD5j&wfX)e?-|V0GPuA+ zQFot%aEqGNJJHn$!_}#PaAvQ^{3-Ye7b}rWwrUmX53(|~i0v{}G_sI9uDch_brX&6 zWl5Ndj-AYg(W9CGfQf<6!YmY>Ey)+uYd_JNXH=>|`OH-CDCmcH(0%iD_aLlNHKH z7bcW-^5+QV$jK?R*)wZ>r9t}loM@XN&M-Pw=F#xn(;u3!(3SXXY^@=aoj70;_=QE9 zGghsG3ekq#N||u{4We_25U=y#T*S{4I{++Ku)> zQ!DZW;pVcn>b;&g2;YE#+V`v*Bl&Y-i@X6D*OpNA{G@JAXho&aOk(_j^weW{#3X5Y z%$q_wpb07EYPdmyH(1^09i$ca{O<}7) zRWncXdSPgBE%BM#by!E>tdnc$8RwUJg1*x($6$}ae$e9Knj8gvVZe#bLi!<+&BkFj zg@nOpDneyc+hU9P-;jmOSMN|*H#>^Ez#?;%C3hg_65leSUm;iz)UkW)jX#p)e&S&M z1|a?wDzV5NVnlhRBCd_;F87wp>6c<&nkgvC+!@KGiIqWY4l}=&1w7|r6{oBN8xyzh zG$b#2=RJp_iq6)#t5%yLkKx(0@D=C3w+oiXtSuaQ%I1WIb-eiE$d~!)b@|4XLy!CZ z9p=t=%3ad@Ep+<9003D2KZ5VyP~_n$=;~r&YUg5UZ0KVD&tR1DHy9x)qWtKJp#Kq# zP*8p#W(8JJ_*h_3W}FlvRam?<4Z+-H77^$Lvi+#vmhL9J zJ<1SV45xi;SrO2f=-OB(7#iNA5)x1uNC-yNxUw|!00vcW2PufRm>e~toH;M0Q85MQLWd?3O{i8H+5VkR@l9Dg-ma ze2fZ%>G(u5(k9EHj2L6!;(KZ8%8|*-1V|B#EagbF(rc+5iL_5;Eu)L4Z-V;0HfK4d z*{utLse_rvHZeQ>V5H=f78M3Ntg1BPxFCVD{HbNA6?9*^YIq;B-DJd{Ca2L#)qWP? zvX^NhFmX?CTWw&Ns}lgs;r3i+Bq@y}Ul+U%pzOS0Fcv9~aB(0!>GT0)NO?p=25LjN z2bh>6RhgqD7bQj#k-KOm@JLgMa6>%-ok1WpOe)FS^XOU{c?d5shG(lIn3GiVBxmg`u%-j=)^v&pX1JecJics3&jvPI)mDut52? z3jEA)DM%}BYbxxKrizVYwq?(P&19EXlwD9^-6J+4!}9{ywR9Gk42jjAURAF&EO|~N z)?s>$Da@ikI4|^z0e{r`J8zIs>SpM~Vn^{3fArRu;?+43>lD+^XtUcY1HidJwnR6+ z!;oG2=B6Z_=M%*{z-RaHc(n|1RTKQdNjjV!Pn9lFt^4w|AeN06*j}ZyhqZ^!-=cyGP_ShV1rGxkx8t zB;8`h!S{LD%ot``700d0@Grql(DTt4Awgmi+Yr0@#jbe=2#UkK%rv=OLqF)9D7D1j z!~McAwMYkeaL$~kI~90)5vBhBzWYc3Cj1WI0RS`z000R8-@ET0dA~*r(gSiCJmQMN&4%1D zyVNf0?}sBH8zNbBLn>~(W{d3%@kL_eQ6jEcR{l>C|JK z(R-fA!z|TTRG40|zv}7E@PqCAXP3n`;%|SCQ|ZS%ym$I{`}t3KPL&^l5`3>yah4*6 zifO#{VNz3)?ZL$be;NEaAk9b#{tV?V7 zP|wf5YA*1;s<)9A4~l3BHzG&HH`1xNr#%){4xZ!jq%o=7nN*wMuXlFV{HaiQLJ`5G zBhDi#D(m`Q1pLh@Tq+L;OwuC52RdW7b8}~60WCOK5iYMUad9}7aWBuILb({5=z~YF zt?*Jr5NG+WadM{mDL>GyiByCuR)hd zA=HM?J6l1Xv0Dl+LW@w$OTcEoOda^nFCw*Sy^I@$sSuneMl{4ys)|RY#9&NxW4S)9 zq|%83IpslTLoz~&vTo!Ga@?rj_kw{|k{nv+w&Ku?fyk4Ki4I?);M|5Axm)t+BaE)D zm(`AQ#k^DWrjbuXoJf2{Aj^KT zFb1zMSqxq|vceV+Mf-)$oPflsO$@*A0n0Z!R{&(xh8s}=;t(lIy zv$S8x>m;vQNHuRzoaOo?eiWFe{0;$s`Bc+Osz~}Van${u;g(su`3lJ^TEfo~nERfP z)?aFzpDgnLYiERsKPu|0tq4l2wT)Atr6Qb%m-AUn6HnCue*yWICp7TjW$@sO zm5rm4aTcPQ(rfi7a`xP7cKCFrJD}*&_~xgLyr^-bmsL}y;A5P|al8J3WUoBSjqu%v zxC;mK!g(7r6RRJ852Z~feoC&sD3(6}^5-uLK8o)9{8L_%%rItZK9C){UxB|;G>JbP zsRRtS4-3B*5c+K2kvmgZK8472%l>3cntWUOVHxB|{Ay~aOg5RN;{PJgeVD*H%ac+y!h#wi%o2bF2Ca8IyMyH{>4#{E_8u^@+l-+n=V}Sq?$O z{091@v%Bd*3pk0^2UtiF9Z+(a@wy6 zUdw8J*ze$K#=$48IBi1U%;hmhO>lu!uU;+RS}p&6@rQila7WftH->*A4=5W|Fmtze z)7E}jh@cbmr9iup^i%*(uF%LG&!+Fyl@LFA-}Ca#bxRfDJAiR2dt6644TaYw1Ma79 zt8&DYj31j^5WPNf5P&{)J?WlCe@<3u^78wnd(Ja4^a>{^Tw}W>|Cjt^If|7l^l)^Q zbz|7~CF(k_9~n|h;ysZ+jHzkXf(*O*@5m zLzUmbHp=x!Q|!9NVXyipZ3)^GuIG$k;D)EK!a5=8MFLI_lpf`HPKl=-Ww%z8H_0$j ztJ||IfFG1lE9nmQ0+jPQy zCBdKkjArH@K7jVcMNz);Q(Q^R{d5G?-kk;Uu_IXSyWB)~KGIizZL(^&qF;|1PI7!E zTP`%l)gpX|OFn&)M%txpQ2F!hdA~hX1Cm5)IrdljqzRg!f{mN%G~H1&oqe`5eJCIF zHdD7O;AX-{XEV(a`gBFJ9ews#CVS2y!&>Cm_dm3C8*n3MA*e67(WC?uP@8TXuMroq z{#w$%z@CBIkRM7?}Xib+>hRjy?%G!fiw8! z8(gB+8J~KOU}yO7UGm&1g_MDJ$IXS!`+*b*QW2x)9>K~Y*E&bYMnjl6h!{17_8d!%&9D`a7r&LKZjC<&XOvTRaKJ1 zUY@hl5^R&kZl3lU3njk`3dPzxj$2foOL26r(9zsVF3n_F#v)s5vv3@dgs|lP#eylq62{<-vczqP!RpVBTgI>@O6&sU>W|do17+#OzQ7o5A$ICH z?GqwqnK^n2%LR;$^oZM;)+>$X3s2n}2jZ7CdWIW0lnGK-b#EG01)P@aU`pg}th&J-TrU`tIpb5t((0eu|!u zQz+3ZiOQ^?RxxK4;zs=l8q!-n7X{@jSwK(iqNFiRColuEOg}!7cyZi`iBX4g1pNBj zAPzL?P^Ljhn;1$r8?bc=#n|Ed7wB&oHcw()&*k#SS#h}jO?ZB246EGItsz*;^&tzp zu^YJ0=lwsi`eP_pU8}6JA7MS;9pfD;DsSsLo~ogzMNP70@@;Fm8f0^;>$Z>~}GWRw!W5J3tNX*^2+1f3hz{~rIzJo z6W%J(H!g-eI_J1>0juX$X4Cl6i+3wbc~k146UIX&G22}WE>0ga#WLsn9tY(&29zBvH1$`iWtTe zG2jYl@P!P)eb<5DsR72BdI7-zP&cZNI{7q3e@?N8IKc4DE#UVr->|-ryuJXk^u^>4 z$3wE~=q390;XuOQP~TNoDR?#|NSPJ%sTMInA6*rJ%go|=YjGe!B>z6u$IhgQSwoV* zjy3F2#I>uK{42{&IqP59)Y(1*Z>>#W8rCf4_eVsH)`v!P#^;BgzKDR`ARGEZzkNX+ zJUQu=*-ol=Xqqt5=`=pA@BIn@6a9G8C{c&`i^(i+BxQO9?YZ3iu%$$da&Kb?2kCCo zo7t$UpSFWqmydXf@l3bVJ=%K?SSw)|?srhJ-1ZdFu*5QhL$~-IQS!K1s@XzAtv6*Y zl8@(5BlWYLt1yAWy?rMD&bwze8bC3-GfNH=p zynNFCdxyX?K&G(ZZ)afguQ2|r;XoV^=^(;Cku#qYn4Lus`UeKt6rAlFo_rU`|Rq z&G?~iWMBio<78of-2X(ZYHx~=U0Vz4btyXkctMKdc9UM!vYr~B-(>)(Hc|D zMzkN4!PBg%tZoh+=Gba!0++d193gbMk2&krfDgcbx0jI92cq?FFESVg0D$>F+bil} zY~$)|>1HZsX=5sAZ2WgPB5P=8X#TI+NQ(M~GqyVB53c6IdX=k>Wu@A0Svf5#?uHaF zsYn|koIi3$(%GZ2+G+7Fv^lHTb#5b8sAHSTnL^qWZLM<(1|9|QFw9pnRU{svj}_Al zL)b9>fN{QiA($8peNEJyy`(a{&uh-T4_kdZFIVsKKVM(?05}76EEz?#W za^fiZOAd14IJ4zLX-n7Lq0qlQ^lW8Cvz4UKkV9~P}>sq0?xD3vg+$4vLm~C(+ zM{-3Z#qnZ09bJ>}j?6ry^h+@PfaD7*jZxBEY4)UG&daWb??6)TP+|3#Z&?GL?1i+280CFsE|vIXQbm| zM}Pk!U`U5NsNbyKzkrul-DzwB{X?n3E6?TUHr{M&+R*2%yOiXdW-_2Yd6?38M9Vy^ z*lE%gA{wwoSR~vN0=no}tP2Ul5Gk5M(Xq`$nw#ndFk`tcpd5A=Idue`XZ!FS>Q zG^0w#>P4pPG+*NC9gLP4x2m=cKP}YuS!l^?sHSFftZy{4CoQrb_ z^20(NnG`wAhMI=eq)SsIE~&Gp9Ne0nD4%Xiu|0Fj1UFk?6avDqjdXz{O1nKao*46y zT8~iA%Exu=G#{x=KD;_C&M+Zx4+n`sHT>^>=-1YM;H<72k>$py1?F3#T1*ef9mLZw z5naLQr?n7K;2l+{_uIw*_1nsTn~I|kkCgrn;|G~##hM;9l7Jy$yJfmk+&}W@JeKcF zx@@Woiz8qdi|D%aH3XTx5*wDlbs?dC1_nrFpm^QbG@wM=i2?Zg;$VK!c^Dp8<}BTI zyRhAq@#%2pGV49*Y5_mV4+OICP|%I(dQ7x=6Ob}>EjnB_-_18*xrY?b%-yEDT(wrO z9RY2QT0`_OpGfMObKHV;QLVnrK%mc?$WAdIT`kJQT^n%GuzE7|9@k3ci5fYOh(287 zuIbg!GB3xLg$YN=n)^pHGB0jH+_iIiC=nUcD;G6LuJsjn2VI1cyZx=a?ShCsF==QK z;q~*m&}L<-cb+mDDXzvvrRsybcgQ;Vg21P(uLv5I+eGc7o7tc6`;OA9{soHFOz zT~2?>Ts}gprIX$wRBb4yE>ot<8+*Bv`qbSDv*VtRi|cyWS>)Fjs>fkNOH-+PX&4(~ z&)T8Zam2L6puQl?;5zg9h<}k4#|yH9czHw;1jw-pwBM*O2hUR6yvHATrI%^mvs9q_ z&ccT0>f#eDG<^WG^q@oVqlJrhxH)dcq2cty@l3~|5#UDdExyXUmLQ}f4#;6fI{f^t zDCsgIJ~0`af%YR%Ma5VQq-p21k`vaBu6WE?66+5=XUd%Ay%D$irN>5LhluRWt7 zov-=f>QbMk*G##&DTQyou$s7UqjjW@k6=!I@!k+S{pP8R(2=e@io;N8E`EOB;OGoI zw6Q+{X1_I{OO0HPpBz!X!@`5YQ2)t{+!?M_iH25X(d~-Zx~cXnS9z>u?+If|iNJbx zyFU2d1!ITX64D|lE0Z{dLRqL1Ajj=CCMfC4lD3&mYR_R_VZ>_7_~|<^o*%_&jevU+ zQ4|qzci=0}Jydw|LXLCrOl1_P6Xf@c0$ieK2^7@A9UbF{@V_0p%lqW|L?5k>bVM8|p5v&2g;~r>B8uo<4N+`B zH{J)h;SYiIVx@#jI&p-v3dwL5QNV1oxPr8J%ooezTnLW>i*3Isb49%5i!&ac_dEXv zvXmVUck^QHmyrF8>CGXijC_R-y(Qr{3Zt~EmW)-nC!tiH`wlw5D*W7Pip;T?&j%kX z6DkZX4&}iw>hE(boLyjOoupf6JpvBG8}jIh!!VhnD0>}KSMMo{1#uU6kiFcA04~|7 zVO8eI&x1`g4CZ<2cYUI(n#wz2MtVFHx47yE5eL~8bot~>EHbevSt}LLMQX?odD{Ux zJMnam{d)W4da{l7&y-JrgiU~qY3$~}_F#G7|MxT)e;G{U`In&?`j<5D->}cb{}{T(4DF0BOk-=1195KB-E*o@c?`>y#4=dMtYtSY=&L{!TAjFVcq0y@AH`vH! z$41+u!Ld&}F^COPgL(EE{0X7LY&%D7-(?!kjFF7=qw<;`V{nwWBq<)1QiGJgUc^Vz ztMUlq1bZqKn17|6x6iAHbWc~l1HcmAxr%$Puv!znW)!JiukwIrqQ00|H$Z)OmGG@= zv%A8*4cq}(?qn4rN6o`$Y))(MyXr8R<2S^J+v(wmFmtac!%VOfN?&(8Nr!T@kV`N; z*Q33V3t`^rN&aBiHet)18wy{*wi1=W!B%B-Q6}SCrUl$~Hl{@!95ydml@FK8P=u4s z4e*7gV2s=YxEvskw2Ju!2%{8h01rx-3`NCPc(O zH&J0VH5etNB2KY6k4R@2Wvl^Ck$MoR3=)|SEclT2ccJ!RI9Nuter7u9@;sWf-%um;GfI!=eEIQ2l2p_YWUd{|6EG ze{yO6;lMc>;2tPrsNdi@&1K6(1;|$xe8vLgiouj%QD%gYk`4p{Ktv9|j+!OF-P?@p z;}SV|oIK)iwlBs+`ROXkhd&NK zzo__r!B>tOXpBJMDcv!Mq54P+n4(@dijL^EpO1wdg~q+!DT3lB<>9AANSe!T1XgC=J^)IP0XEZ()_vpu!!3HQyJhwh?r`Ae%Yr~b% zO*NY9t9#qWa@GCPYOF9aron7thfWT`eujS4`t2uG6)~JRTI;f(ZuoRQwjZjp5Pg34 z)rp$)Kr?R+KdJ;IO;pM{$6|2y=k_siqvp%)2||cHTe|b5Ht8&A{wazGNca zX$Ol?H)E_R@SDi~4{d-|8nGFhZPW;Cts1;08TwUvLLv&_2$O6Vt=M)X;g%HUr$&06 zISZb(6)Q3%?;3r~*3~USIg=HcJhFtHhIV(siOwV&QkQe#J%H9&E21!C*d@ln3E@J* zVqRO^<)V^ky-R|%{(9`l-(JXq9J)1r$`uQ8a}$vr9E^nNiI*thK8=&UZ0dsFN_eSl z(q~lnD?EymWLsNa3|1{CRPW60>DSkY9YQ;$4o3W7Ms&@&lv9eH!tk~N&dhqX&>K@} zi1g~GqglxkZ5pEFkllJ)Ta1I^c&Bt6#r(QLQ02yHTaJB~- zCcE=5tmi`UA>@P=1LBfBiqk)HB4t8D?02;9eXj~kVPwv?m{5&!&TFYhu>3=_ zsGmYZ^mo*-j69-42y&Jj0cBLLEulNRZ9vXE)8~mt9C#;tZs;=#M=1*hebkS;7(aGf zcs7zH(I8Eui9UU4L--))yy`&d&$In&VA2?DAEss4LAPCLd>-$i?lpXvn!gu^JJ$(DoUlc6wE98VLZ*z`QGQov5l4Fm_h?V-;mHLYDVOwKz7>e4+%AzeO>P6v}ndPW| zM>m#6Tnp7K?0mbK=>gV}=@k*0Mr_PVAgGMu$j+pWxzq4MAa&jpCDU&-5eH27Iz>m^ zax1?*HhG%pJ((tkR(V(O(L%7v7L%!_X->IjS3H5kuXQT2!ow(;%FDE>16&3r){!ex zhf==oJ!}YU89C9@mfDq!P3S4yx$aGB?rbtVH?sHpg?J5C->!_FHM%Hl3#D4eplxzQ zRA+<@LD%LKSkTk2NyWCg7u=$%F#;SIL44~S_OGR}JqX}X+=bc@swpiClB`Zbz|f!4 z7Ysah7OkR8liXfI`}IIwtEoL}(URrGe;IM8%{>b1SsqXh)~w}P>yiFRaE>}rEnNkT z!HXZUtxUp1NmFm)Dm@-{FI^aRQqpSkz}ZSyKR%Y}YHNzBk)ZIp} zMtS=aMvkgWKm9&oTcU0?S|L~CDqA+sHpOxwnswF-fEG)cXCzUR?ps@tZa$=O)=L+5 zf%m58cq8g_o}3?Bhh+c!w4(7AjxwQ3>WnVi<{{38g7yFboo>q|+7qs<$8CPXUFAN< zG&}BHbbyQ5n|qqSr?U~GY{@GJ{(Jny{bMaOG{|IkUj7tj^9pa9|FB_<+KHLxSxR;@ zHpS$4V)PP+tx}22fWx(Ku9y+}Ap;VZqD0AZW4gCDTPCG=zgJmF{|x;(rvdM|2|9a}cex6xrMkERnkE;}jvU-kmzd%_J50$M`lIPCKf+^*zL=@LW`1SaEc%=m zQ+lT06Gw+wVwvQ9fZ~#qd430v2HndFsBa9WjD0P}K(rZYdAt^5WQIvb%D^Q|pkVE^ zte$&#~zmULFACGfS#g=2OLOnIf2Of-k!(BIHjs77nr!5Q1*I9 z1%?=~#Oss!rV~?-6Gm~BWJiA4mJ5TY&iPm_$)H1_rTltuU1F3I(qTQ^U$S>%$l z)Wx1}R?ij0idp@8w-p!Oz{&*W;v*IA;JFHA9%nUvVDy7Q8woheC#|8QuDZb-L_5@R zOqHwrh|mVL9b=+$nJxM`3eE{O$sCt$UK^2@L$R(r^-_+z?lOo+me-VW=Zw z-Bn>$4ovfWd%SPY`ab-u9{INc*k2h+yH%toDHIyqQ zO68=u`N}RIIs7lsn1D){)~%>ByF<>i@qFb<-axvu(Z+6t7v<^z&gm9McRB~BIaDn$ z#xSGT!rzgad8o>~kyj#h1?7g96tOcCJniQ+*#=b7wPio>|6a1Z?_(TS{)KrPe}(8j z!#&A=k(&Pj^F;r)CI=Z{LVu>uj!_W1q4b`N1}E(i%;BWjbEcnD=mv$FL$l?zS6bW!{$7j1GR5ocn94P2u{ z70tAAcpqtQo<@cXw~@i-@6B23;317|l~S>CB?hR5qJ%J3EFgyBdJd^fHZu7AzHF(BQ!tyAz^L0`X z23S4Fe{2X$W0$zu9gm%rg~A>ijaE#GlYlrF9$ds^QtaszE#4M(OLVP2O-;XdT(XIC zatwzF*)1c+t~c{L=fMG8Z=k5lv>U0;C{caN1NItnuSMp)6G3mbahu>E#sj&oy94KC zpH}8oEw{G@N3pvHhp{^-YaZeH;K+T_1AUv;IKD<=mv^&Ueegrb!yf`4VlRl$M?wsl zZyFol(2|_QM`e_2lYSABpKR{{NlxlDSYQNkS;J66aT#MSiTx~;tUmvs-b*CrR4w=f z8+0;*th6kfZ3|5!Icx3RV11sp=?`0Jy3Fs0N4GZQMN=8HmT6%x9@{Dza)k}UwL6JT zHRDh;%!XwXr6yuuy`4;Xsn0zlR$k%r%9abS1;_v?`HX_hI|+EibVnlyE@3aL5vhQq zlIG?tN^w@0(v9M*&L+{_+RQZw=o|&BRPGB>e5=ys7H`nc8nx)|-g;s7mRc7hg{GJC zAe^vCIJhajmm7C6g! zL&!WAQ~5d_5)00?w_*|*H>3$loHrvFbitw#WvLB!JASO?#5Ig5$Ys10n>e4|3d;tS zELJ0|R4n3Az(Fl3-r^QiV_C;)lQ1_CW{5bKS15U|E9?ZgLec@%kXr84>5jV2a5v=w z?pB1GPdxD$IQL4)G||B_lI+A=08MUFFR4MxfGOu07vfIm+j=z9tp~5i_6jb`tR>qV z$#`=BQ*jpCjm$F0+F)L%xRlnS%#&gro6PiRfu^l!EVan|r3y}AHJQOORGx4~ z&<)3=K-tx518DZyp%|!EqpU!+X3Et7n2AaC5(AtrkW>_57i}$eqs$rupubg0a1+WO zGHZKLN2L0D;ab%{_S1Plm|hx8R?O14*w*f&2&bB050n!R2by zw!@XOQx$SqZ5I<(Qu$V6g>o#A!JVwErWv#(Pjx=KeS0@hxr4?13zj#oWwPS(7Ro|v z>Mp@Kmxo79q|}!5qtX2-O@U&&@6s~!I&)1WQIl?lTnh6UdKT_1R640S4~f=_xoN3- zI+O)$R@RjV$F=>Ti7BlnG1-cFKCC(t|Qjm{SalS~V-tX#+2ekRhwmN zZr`8{QF6y~Z!D|{=1*2D-JUa<(1Z=;!Ei!KiRNH?o{p5o3crFF=_pX9O-YyJchr$~ zRC`+G+8kx~fD2k*ZIiiIGR<8r&M@3H?%JVOfE>)})7ScOd&?OjgAGT@WVNSCZ8N(p zuQG~76GE3%(%h1*vUXg$vH{ua0b`sQ4f0*y=u~lgyb^!#CcPJa2mkSEHGLsnO^kb$ zru5_l#nu=Y{rSMWiYx?nO{8I!gH+?wEj~UM?IrG}E|bRIBUM>UlY<`T1EHpRr36vv zBi&dG8oxS|J$!zoaq{+JpJy+O^W(nt*|#g32bd&K^w-t>!Vu9N!k9eA8r!Xc{utY> zg9aZ(D2E0gL#W0MdjwES-7~Wa8iubPrd?8-$C4BP?*wok&O8+ykOx{P=Izx+G~hM8 z*9?BYz!T8~dzcZr#ux8kS7u7r@A#DogBH8km8Ry4slyie^n|GrTbO|cLhpqgMdsjX zJ_LdmM#I&4LqqsOUIXK8gW;V0B(7^$y#h3h>J0k^WJfAMeYek%Y-Dcb_+0zPJez!GM zAmJ1u;*rK=FNM0Nf}Y!!P9c4)HIkMnq^b;JFd!S3?_Qi2G#LIQ)TF|iHl~WKK6JmK zbv7rPE6VkYr_%_BT}CK8h=?%pk@3cz(UrZ{@h40%XgThP*-Oeo`T0eq9 zA8BnWZKzCy5e&&_GEsU4*;_k}(8l_&al5K-V*BFM=O~;MgRkYsOs%9eOY6s6AtE*<7GQAR2ulC3RAJrG_P1iQK5Z~&B z&f8X<>yJV6)oDGIlS$Y*D^Rj(cszTy5c81a5IwBr`BtnC6_e`ArI8CaTX_%rx7;cn zR-0?J_LFg*?(#n~G8cXut(1nVF0Oka$A$1FGcERU<^ggx;p@CZc?3UB41RY+wLS`LWFNSs~YP zuw1@DNN3lTd|jDL7gjBsd9}wIw}4xT2+8dBQzI00m<@?c2L%>}QLfK5%r!a-iII`p zX@`VEUH)uj^$;7jVUYdADQ2k*!1O3WdfgF?OMtUXNpQ1}QINamBTKDuv19^{$`8A1 zeq%q*O0mi@(%sZU>Xdb0Ru96CFqk9-L3pzLVsMQ`Xpa~N6CR{9Rm2)A|CI21L(%GW zh&)Y$BNHa=FD+=mBw3{qTgw)j0b!Eahs!rZnpu)z!!E$*eXE~##yaXz`KE5(nQM`s zD!$vW9XH)iMxu9R>r$VlLk9oIR%HxpUiW=BK@4U)|1WNQ=mz9a z^!KkO=>GaJ!GBXm{KJj^;kh-MkUlEQ%lza`-G&}C5y1>La1sR6hT=d*NeCnuK%_LV zOXt$}iP6(YJKc9j-Fxq~*ItVUqljQ8?oaysB-EYtFQp9oxZ|5m0^Hq(qV!S+hq#g( z?|i*H2MIr^Kxgz+3vIljQ*Feejy6S4v~jKEPTF~Qhq!(ms5>NGtRgO5vfPPc4Z^AM zTj!`5xEreIN)vaNxa|q6qWdg>+T`Ol0Uz)ckXBXEGvPNEL3R8hB3=C5`@=SYgAju1 z!)UBr{2~=~xa{b8>x2@C7weRAEuatC)3pkRhT#pMPTpSbA|tan%U7NGMvzmF?c!V8 z=pEWxbdXbTAGtWTyI?Fml%lEr-^AE}w#l(<7OIw;ctw}imYax&vR4UYNJZK6P7ZOd zP87XfhnUHxCUHhM@b*NbTi#(-8|wcv%3BGNs#zRCVV(W?1Qj6^PPQa<{yaBwZ`+<`w|;rqUY_C z&AeyKwwf*q#OW-F()lir=T^<^wjK65Lif$puuU5+tk$;e_EJ;Lu+pH>=-8=PDhkBg z8cWt%@$Sc#C6F$Vd+0507;{OOyT7Hs%nKS88q-W!$f~9*WGBpHGgNp}=C*7!RiZ5s zn1L_DbKF@B8kwhDiLKRB@lsXVVLK|ph=w%_`#owlf@s@V(pa`GY$8h%;-#h@TsO|Y8V=n@*!Rog7<7Cid%apR|x zOjhHCyfbIt%+*PCveTEcuiDi%Wx;O;+K=W?OFUV%)%~6;gl?<0%)?snDDqIvkHF{ zyI02)+lI9ov42^hL>ZRrh*HhjF9B$A@=H94iaBESBF=eC_KT$8A@uB^6$~o?3Wm5t1OIaqF^~><2?4e3c&)@wKn9bD? zoeCs;H>b8DL^F&>Xw-xjZEUFFTv>JD^O#1E#)CMBaG4DX9bD(Wtc8Rzq}9soQ8`jf zeSnHOL}<+WVSKp4kkq&?SbETjq6yr@4%SAqOG=9E(3YeLG9dtV+8vmzq+6PFPk{L; z(&d++iu=^F%b+ea$i2UeTC{R*0Isk;vFK!no<;L+(`y`3&H-~VTdKROkdyowo1iqR zbVW(3`+(PQ2>TKY>N!jGmGo7oeoB8O|P_!Ic@ zZ^;3dnuXo;WJ?S+)%P>{Hcg!Jz#2SI(s&dY4QAy_vRlmOh)QHvs_7c&zkJCmJGVvV zX;Mtb>QE+xp`KyciG$Cn*0?AK%-a|=o!+7x&&yzHQOS>8=B*R=niSnta^Pxp1`=md z#;$pS$4WCT?mbiCYU?FcHGZ#)kHVJTTBt^%XE(Q};aaO=Zik0UgLcc0I(tUpt(>|& zcxB_|fxCF7>&~5eJ=Dpn&5Aj{A^cV^^}(7w#p;HG&Q)EaN~~EqrE1qKrMAc&WXIE;>@<&)5;gD2?={Xf@Mvn@OJKw=8Mgn z!JUFMwD+s==JpjhroT&d{$kQAy%+d`a*XxDEVxy3`NHzmITrE`o!;5ClXNPb4t*8P zzAivdr{j_v!=9!^?T3y?gzmqDWX6mkzhIzJ-3S{T5bcCFMr&RPDryMcdwbBuZbsgN zGrp@^i?rcfN7v0NKGzDPGE#4yszxu=I_`MI%Z|10nFjU-UjQXXA?k8Pk|OE<(?ae) zE%vG#eZAlj*E7_3dx#Zz4kMLj>H^;}33UAankJiDy5ZvEhrjr`!9eMD8COp}U*hP+ zF}KIYx@pkccIgyxFm#LNw~G&`;o&5)2`5aogs`1~7cMZQ7zj!%L4E`2yzlQN6REX20&O<9 zKV6fyr)TScJPPzNTC2gL+0x#=u>(({{D7j)c-%tvqls3#Y?Z1m zV5WUE)zdJ{$p>yX;^P!UcXP?UD~YM;IRa#Rs5~l+*$&nO(;Ers`G=0D!twR(0GF@c zHl9E5DQI}Oz74n zfKP>&$q0($T4y$6w(p=ERAFh+>n%iaeRA%!T%<^+pg?M)@ucY<&59$x9M#n+V&>}=nO9wCV{O~lg&v#+jcUj(tQ z`0u1YH)-`U$15a{pBkGyPL0THv1P|4e@pf@3IBZS4dVJPo#H>pWq%Lr0YS-SeWash z8R7=jb28KPMI|_lo#GEO|5B?N_e``H*23{~a!AmUJ+fb4HX-%QI@lSEUxKlGV7z7Q zSKw@-TR>@1RL%w{x}dW#k1NgW+q4yt2Xf1J62Bx*O^WG8OJ|FqI4&@d3_o8Id@*)4 zYrk=>@!wv~mh7YWv*bZhxqSmFh2Xq)o=m;%n$I?GSz49l1$xRpPu_^N(vZ>*>Z<04 z2+rP70oM=NDysd!@fQdM2OcyT?3T^Eb@lIC-UG=Bw{BjQ&P`KCv$AcJ;?`vdZ4){d z&gkoUK{$!$$K`3*O-jyM1~p-7T*qb)Ys>Myt^;#1&a%O@x8A+E>! zY8=eD`ZG)LVagDLBeHg>=atOG?Kr%h4B%E6m@J^C+U|y)XX@f z8oyJDW|9g=<#f<{JRr{y#~euMnv)`7j=%cHWLc}ngjq~7k**6%4u>Px&W%4D94(r* z+akunK}O0DC2A%Xo9jyF;DobX?!1I(7%}@7F>i%&nk*LMO)bMGg2N+1iqtg+r(70q zF5{Msgsm5GS7DT`kBsjMvOrkx&|EU!{{~gL4d2MWrAT=KBQ-^zQCUq{5PD1orxlIL zq;CvlWx#f1NWvh`hg011I%?T_s!e38l*lWVt|~z-PO4~~1g)SrJ|>*tXh=QfXT)%( z+ex+inPvD&O4Ur;JGz>$sUOnWdpSLcm1X%aQDw4{dB!cnj`^muI$CJ2%p&-kULVCE z>$eMR36kN$wCPR+OFDM3-U(VOrp9k3)lI&YVFqd;Kpz~K)@Fa&FRw}L(SoD z9B4a+hQzZT-BnVltst&=kq6Y(f^S4hIGNKYBgMxGJ^;2yrO}P3;r)(-I-CZ)26Y6? z&rzHI_1GCvGkgy-t1E;r^3Le30|%$ebDRu2+gdLG)r=A~Qz`}~&L@aGJ{}vVs_GE* zVUjFnzHiXfKQbpv&bR&}l2bzIjAooB)=-XNcYmrGmBh(&iu@o!^hn0^#}m2yZZUK8 zufVm7Gq0y`Mj;9b>`c?&PZkU0j4>IL=UL&-Lp3j&47B5pAW4JceG{!XCA)kT<%2nqCxj<)uy6XR_uws~>_MEKPOpAQ!H zkn>FKh)<9DwwS*|Y(q?$^N!6(51O0 z^JM~Ax{AI1Oj$fs-S5d4T7Z_i1?{%0SsIuQ&r8#(JA=2iLcTN+?>wOL532%&dMYkT z*T5xepC+V6zxhS@vNbMoi|i)=rpli@R9~P!39tWbSSb904ekv7D#quKbgFEMTb48P zuq(VJ+&L8aWU(_FCD$3^uD!YM%O^K(dvy~Wm2hUuh6bD|#(I39Xt>N1Y{ZqXL`Fg6 zKQ?T2htHN!(Bx;tV2bfTtIj7e)liN-29s1kew>v(D^@)#v;}C4-G=7x#;-dM4yRWm zyY`cS21ulzMK{PoaQ6xChEZ}o_#}X-o}<&0)$1#3we?+QeLt;aVCjeA)hn!}UaKt< zat1fHEx13y-rXNMvpUUmCVzocPmN~-Y4(YJvQ#db)4|%B!rBsgAe+*yor~}FrNH08 z3V!97S}D7d$zbSD{$z;@IYMxM6aHdypIuS*pr_U6;#Y!_?0i|&yU*@16l z*dcMqDQgfNBf}?quiu4e>H)yTVfsp#f+Du0@=Kc41QockXkCkvu>FBd6Q+@FL!(Yx z2`YuX#eMEiLEDhp+9uFqME_E^faV&~9qjBHJkIp~%$x^bN=N)K@kvSVEMdDuzA0sn z88CBG?`RX1@#hQNd`o^V{37)!w|nA)QfiYBE^m=yQKv-fQF+UCMcuEe1d4BH7$?>b zJl-r9@0^Ie=)guO1vOd=i$_4sz>y3x^R7n4ED!5oXL3@5**h(xr%Hv)_gILarO46q+MaDOF%ChaymKoI6JU5Pg;7#2n9-18|S1;AK+ zgsn6;k6-%!QD>D?cFy}8F;r@z8H9xN1jsOBw2vQONVqBVEbkiNUqgw~*!^##ht>w0 zUOykwH=$LwX2j&nLy=@{hr)2O&-wm-NyjW7n~Zs9UlH;P7iP3 zI}S(r0YFVYacnKH(+{*)Tbw)@;6>%=&Th=+Z6NHo_tR|JCI8TJiXv2N7ei7M^Q+RM z?9o`meH$5Yi;@9XaNR#jIK^&{N|DYNNbtdb)XW1Lv2k{E>;?F`#Pq|&_;gm~&~Zc9 zf+6ZE%{x4|{YdtE?a^gKyzr}dA>OxQv+pq|@IXL%WS0CiX!V zm$fCePA%lU{%pTKD7|5NJHeXg=I0jL@$tOF@K*MI$)f?om)D63K*M|r`gb9edD1~Y zc|w7N)Y%do7=0{RC|AziW7#am$)9jciRJ?IWl9PE{G3U+$%FcyKs_0Cgq`=K3@ttV z9g;M!3z~f_?P%y3-ph%vBMeS@p7P&Ea8M@97+%XEj*(1E6vHj==d zjsoviB>j^$_^OI_DEPvFkVo(BGRo%cJeD){6Uckei=~1}>sp299|IRjhXe)%?uP0I zF5+>?0#Ye}T^Y$u_rc4=lPcq4K^D(TZG-w30-YiEM=dcK+4#o*>lJ8&JLi+3UcpZk z!^?95S^C0ja^jwP`|{<+3cBVog$(mRdQmadS+Vh~z zS@|P}=|z3P6uS+&@QsMp0no9Od&27O&14zHXGAOEy zh~OKpymK5C%;LLb467@KgIiVwYbYd6wFxI{0-~MOGfTq$nBTB!{SrWmL9Hs}C&l&l#m?s*{tA?BHS4mVKHAVMqm63H<|c5n0~k)-kbg zXidai&9ZUy0~WFYYKT;oe~rytRk?)r8bptITsWj(@HLI;@=v5|XUnSls7$uaxFRL+ zRVMGuL3w}NbV1`^=Pw*0?>bm8+xfeY(1PikW*PB>>Tq(FR`91N0c2&>lL2sZo5=VD zQY{>7dh_TX98L2)n{2OV=T10~*YzX27i2Q7W86M4$?gZIXZaBq#sA*{PH8){|GUi;oM>e?ua7eF4WFuFYZSG| zze?srg|5Ti8Og{O zeFxuw9!U+zhyk?@w zjsA6(oKD=Ka;A>Ca)oPORxK+kxH#O@zhC!!XS4@=swnuMk>t+JmLmFiE^1aX3f<)D@`%K0FGK^gg1a1j>zi z2KhV>sjU7AX3F$SEqrXSC}fRx64GDoc%!u2Yag68Lw@w9v;xOONf@o)Lc|Uh3<21ctTYu-mFZuHk*+R{GjXHIGq3p)tFtQp%TYqD=j1&y)>@zxoxUJ!G@ zgI0XKmP6MNzw>nRxK$-Gbzs}dyfFzt>#5;f6oR27ql!%+{tr+(`(>%51|k`ML} zY4eE)Lxq|JMas(;JibNQds1bUB&r}ydMQXBY4x(^&fY_&LlQC)3hylc$~8&~|06-D z#T+%66rYbHX%^KuqJED_wuGB+=h`nWA!>1n0)3wZrBG3%`b^Ozv6__dNa@%V14|!D zQ?o$z5u0^8`giv%qE!BzZ!3j;BlDlJDk)h@9{nSQeEk!z9RGW) z${RSF3phEM*ce*>Xdp}585vj$|40=&S{S-GTiE?Op*vY&Lvr9}BO$XWy80IF+6@%n z5*2ueT_g@ofP#u5pxb7n*fv^Xtt7&?SRc{*2Ka-*!BuOpf}neHGCiHy$@Ka1^Dint z;DkmIL$-e)rj4o2WQV%Gy;Xg(_Bh#qeOsTM2f@KEe~4kJ8kNLQ+;(!j^bgJMcNhvklP5Z6I+9Fq@c&D~8Fb-4rmDT!MB5QC{Dsb;BharP*O;SF4& zc$wj-7Oep7#$WZN!1nznc@Vb<_Dn%ga-O#J(l=OGB`dy=Sy&$(5-n3zzu%d7E#^8`T@}V+5B;PP8J14#4cCPw-SQTdGa2gWL0*zKM z#DfSXs_iWOMt)0*+Y>Lkd=LlyoHjublNLefhKBv@JoC>P7N1_#> zv=mLWe96%EY;!ZGSQDbZWb#;tzqAGgx~uk+-$+2_8U`!ypbwXl z^2E-FkM1?lY@yt8=J3%QK+xaZ6ok=-y%=KXCD^0r!5vUneW>95PzCkOPO*t}p$;-> ze5j-BLT_;)cZQzR2CEsm@rU7GZfFtdp*a|g4wDr%8?2QkIGasRfDWT-Dvy*U{?IHT z*}wGnzdlSptl#ZF^sf)KT|BJs&kLG91^A6ls{CzFprZ6-Y!V0Xysh%9p%iMd7HLsS zN+^Un$tDV)T@i!v?3o0Fsx2qI(AX_$dDkBzQ@fRM%n zRXk6hb9Py#JXUs+7)w@eo;g%QQ95Yq!K_d=z{0dGS+pToEI6=Bo8+{k$7&Z zo4>PH(`ce8E-Ps&uv`NQ;U$%t;w~|@E3WVOCi~R4oj5wP?%<*1C%}Jq%a^q~T7u>K zML5AKfQDv6>PuT`{SrKHRAF+^&edg6+5R_#H?Lz3iGoWo#PCEd0DS;)2U({{X#zU^ zw_xv{4x7|t!S)>44J;KfA|DC?;uQ($l+5Vp7oeqf7{GBF9356nx|&B~gs+@N^gSdd zvb*>&W)|u#F{Z_b`f#GVtQ`pYv3#||N{xj1NgB<#=Odt6{eB%#9RLt5v zIi|0u70`#ai}9fJjKv7dE!9ZrOIX!3{$z_K5FBd-Kp-&e4(J$LD-)NMTp^_pB`RT; zftVVlK2g@+1Ahv2$D){@Y#cL#dUj9*&%#6 zd2m9{1NYp>)6=oAvqdCn5#cx{AJ%S8skUgMglu2*IAtd+z1>B&`MuEAS(D(<6X#Lj z?f4CFx$)M&$=7*>9v1ER4b6!SIz-m0e{o0BfkySREchp?WdVPpQCh!q$t>?rL!&Jg zd#heM;&~A}VEm8Dvy&P|J*eAV&w!&Nx6HFV&B8jJFVTmgLaswn!cx$&%JbTsloz!3 zMEz1d`k==`Ueub_JAy_&`!ogbwx27^ZXgFNAbx=g_I~5nO^r)}&myw~+yY*cJl4$I znNJ32M&K=0(2Dj_>@39`3=FX!v3nZHno_@q^!y}%(yw0PqOo=);6Y@&ylVe>nMOZ~ zd>j#QQSBn3oaWd;qy$&5(5H$Ayi)0haAYO6TH>FR?rhqHmNOO+(})NB zLI@B@v0)eq!ug`>G<@htRlp3n!EpU|n+G+AvXFrWSUsLMBfL*ZB`CRsIVHNTR&b?K zxBgsN0BjfB>UVcJ|x%=-zb%OV7lmZc& zxiupadZVF7)6QuhoY;;FK2b*qL0J-Rn-8!X4ZY$-ZSUXV5DFd7`T41c(#lAeLMoeT z4%g655v@7AqT!i@)Edt5JMbN(=Q-6{=L4iG8RA%}w;&pKmtWvI4?G9pVRp|RTw`g0 zD5c12B&A2&P6Ng~8WM2eIW=wxd?r7A*N+&!Be7PX3s|7~z=APxm=A?5 zt>xB4WG|*Td@VX{Rs)PV0|yK`oI3^xn(4c_j&vgxk_Y3o(-`_5o`V zRTghg6%l@(qodXN;dB#+OKJEEvhfcnc#BeO2|E(5df-!fKDZ!%9!^BJ_4)9P+9Dq5 zK1=(v?KmIp34r?z{NEWnLB3Px{XYwy-akun4F7xTRr2^zeYW{gcK9)>aJDdU5;w5@ zak=<+-PLH-|04pelTb%ULpuuuJC7DgyT@D|p{!V!0v3KpDnRjANN12q6SUR3mb9<- z>2r~IApQGhstZ!3*?5V z8#)hJ0TdZg0M-BK#nGFP>$i=qk82DO z7h;Ft!D5E15OgW)&%lej*?^1~2=*Z5$2VX>V{x8SC+{i10BbtUk9@I#Vi&hX)q
Q!LwySI{Bnv%Sm)yh{^sSVJ8&h_D-BJ_YZe5eCaAWU9b$O2c z$T|{vWVRtOL!xC0DTc(Qbe`ItNtt5hr<)VijD0{U;T#bUEp381_y`%ZIav?kuYG{iyYdEBPW=*xNSc;Rlt6~F4M`5G+VtOjc z*0qGzCb@gME5udTjJA-9O<&TWd~}ysBd(eVT1-H82-doyH9RST)|+Pb{o*;$j9Tjs zhU!IlsPsj8=(x3bAKJTopW3^6AKROHR^7wZ185wJGVhA~hEc|LP;k7NEz-@4p5o}F z`AD6naG3(n=NF9HTH81=F+Q|JOz$7wm9I<+#BSmB@o_cLt2GkW9|?7mM;r!JZp89l zbo!Hp8=n!XH1{GwaDU+k)pGp`C|cXkCU5%vcH)+v@0eK>%7gWxmuMu9YLlChA|_D@ zi#5zovN_!a-0?~pUV-Rj*1P)KwdU-LguR>YM&*Nen+ln8Q$?WFCJg%DY%K}2!!1FE zDv-A%Cbwo^p(lzac&_TZ-l#9kq`mhLcY3h9ZTUVCM(Ad&=EriQY5{jJv<5K&g|*Lk zgV%ILnf1%8V2B0E&;Sp4sYbYOvvMebLwYwzkRQ#F8GpTQq#uv=J`uaSJ34OWITeSGo6+-8Xw znCk*n{kdDEi)Hi&u^)~cs@iyCkFWB2SWZU|Uc%^43ZIZQ-vWNExCCtDWjqHs;;tWf$v{}0{p0Rvxkq``)*>+Akq%|Na zA`@~-Vfe|+(AIlqru+7Ceh4nsVmO9p9jc8}HX^W&ViBDXT+uXbT#R#idPn&L>+#b6 zflC-4C5-X;kUnR~L>PSLh*gvL68}RBsu#2l`s_9KjUWRhiqF`j)`y`2`YU(>3bdBj z?>iyjEhe-~$^I5!nn%B6Wh+I`FvLNvauve~eX<+Ipl&04 zT}};W&1a3%W?dJ2=N#0t?e+aK+%t}5q%jSLvp3jZ%?&F}nOOWr>+{GFIa%wO_2`et z=JzoRR~}iKuuR+azPI8;Gf9)z3kyA4EIOSl!sRR$DlW}0>&?GbgPojmjmnln;cTqCt=ADbE zZ8GAnoM+S1(5$i8^O4t`ue;vO4i}z0wz-QEIVe5_u03;}-!G1NyY8;h^}y;tzY}i5 zqQr#Ur3Fy8sSa$Q0ys+f`!`+>9WbvU_I`Sj;$4{S>O3?#inLHCrtLy~!s#WXV=oVP zeE93*Nc`PBi4q@%Ao$x4lw9vLHM!6mn3-b_cebF|n-2vt-zYVF_&sDE--J-P;2WHo z+@n2areE0o$LjvjlV2X7ZU@j+`{*8zq`JR3gKF#EW|#+{nMyo-a>nFFTg&vhyT=b} zDa8+v0(Dgx0yRL@ZXOYIlVSZ0|MFizy0VPW8;AfA5|pe!#j zX}Py^8fl5SyS4g1WSKKtnyP+_PoOwMMwu`(i@Z)diJp~U54*-miOchy7Z35eL>^M z4p<-aIxH4VUZgS783@H%M7P9hX>t{|RU7$n4T(brCG#h9e9p! z+o`i;EGGq3&pF;~5V~eBD}lC)>if$w%Vf}AFxGqO88|ApfHf&Bvu+xdG)@vuF}Yvk z)o;~k-%+0K0g+L`Wala!$=ZV|z$e%>f0%XoLib%)!R^RoS+{!#X?h-6uu zF&&KxORdZU&EwQFITIRLo(7TA3W}y6X{?Y%y2j0It!ekU#<)$qghZtpcS>L3uh`Uj z7GY;6f$9qKynP#oS3$$a{p^{D+0oJQ71`1?OAn_m8)UGZmj3l*ZI)`V-a>MKGGFG< z&^jg#Ok%(hhm>hSrZ5;Qga4u(?^i>GiW_j9%_7M>j(^|Om$#{k+^*ULnEgzW_1gCICtAD^WpC`A z{9&DXkG#01Xo)U$OC(L5Y$DQ|Q4C6CjUKk1UkPj$nXH##J{c8e#K|&{mA*;b$r0E4 zUNo0jthwA(c&N1l=PEe8Rw_8cEl|-eya9z&H3#n`B$t#+aJ03RFMzrV@gowbe8v(c zIFM60^0&lCFO10NU4w@|61xiZ4CVXeaKjd;d?sv52XM*lS8XiVjgWpRB;&U_C0g+`6B5V&w|O6B*_q zsATxL!M}+$He)1eOWECce#eS@2n^xhlB4<_Nn?yCVEQWDs(r`|@2GqLe<#(|&P0U? z$7V5IgpWf09uIf_RazRwC?qEqRaHyL?iiS05UiGesJy%^>-C{{ypTBI&B0-iUYhk> zIk<5xpsuV@g|z(AZD+C-;A!fTG=df1=<%nxy(a(IS+U{ME4ZbDEBtcD_3V=icT6*_ z)>|J?>&6%nvHhZERBtjK+s4xnut*@>GAmA5m*OTp$!^CHTr}vM4n(X1Q*;{e-Rd2BCF-u@1ZGm z!S8hJ6L=Gl4T_SDa7Xx|-{4mxveJg=ctf`BJ*fy!yF6Dz&?w(Q_6B}WQVtNI!BVBC zKfX<>7vd6C96}XAQmF-Jd?1Q4eTfRB3q7hCh0f!(JkdWT5<{iAE#dKy*Jxq&3a1@~ z8C||Dn2mFNyrUV|<-)C^_y7@8c2Fz+2jrae9deBDu;U}tJ{^xAdxCD248(k;dCJ%o z`y3sADe>U%suxwwv~8A1+R$VB=Q?%U?4joI$um;aH+eCrBqpn- z%79D_7rb;R-;-9RTrwi9dPlg8&@tfWhhZ(Vx&1PQ+6(huX`;M9x~LrW~~#3{j0Bh2kDU$}@!fFQej4VGkJv?M4rU^x!RU zEwhu$!CA_iDjFjrJa`aocySDX16?~;+wgav;}Zut6Mg%C4>}8FL?8)Kgwc(Qlj{@#2Pt0?G`$h7P#M+qoXtlV@d}%c&OzO+QYKK`kyXaK{U(O^2DyIXCZlNQjt0^8~8JzNGrIxhj}}M z&~QZlbx%t;MJ(Vux;2tgNKGlAqphLq%pd}JG9uoVHUo?|hN{pLQ6Em%r*+7t^<);X zm~6=qChlNAVXNN*Sow->*4;}T;l;D1I-5T{Bif@4_}=>l`tK;qqDdt5zvisCKhMAH z#r}`)7VW?LZqfdmXQ%zo5bJ00{Xb9^YKrk0Nf|oIW*K@(=`o2Vndz}ZDyk{!u}PVx zzd--+_WC*U{~DH3{?GI64IB+@On&@9X>EUAo&L+G{L^dozaI4C3G#2wr~hseW@K&g zKWs{uHu-9Je!3;4pE>eBltKUXb^*hG8I&413)$J&{D4N%7PcloU6bn%jPxJyQL?g* z9g+YFFEDiE`8rW^laCNzQmi7CTnPfwyg3VDHRAl>h=In6jeaVOP@!-CP60j3+#vpL zEYmh_oP0{-gTe7Or`L6x)6w?77QVi~jD8lWN@3RHcm80iV%M1A!+Y6iHM)05iC64tb$X2lV_%Txk@0l^hZqi^%Z?#- zE;LE0uFx)R08_S-#(wC=dS&}vj6P4>5ZWjhthP=*Hht&TdLtKDR;rXEX4*z0h74FA zMCINqrh3Vq;s%3MC1YL`{WjIAPkVL#3rj^9Pj9Ss7>7duy!9H0vYF%>1jh)EPqvlr6h%R%CxDsk| z!BACz7E%j?bm=pH6Eaw{+suniuY7C9Ut~1cWfOX9KW9=H><&kQlinPV3h9R>3nJvK z4L9(DRM=x;R&d#a@oFY7mB|m8h4692U5eYfcw|QKwqRsshN(q^v$4$)HgPpAJDJ`I zkqjq(8Cd!K!+wCd=d@w%~e$=gdUgD&wj$LQ1r>-E=O@c ze+Z$x{>6(JA-fNVr)X;*)40Eym1TtUZI1Pwwx1hUi+G1Jlk~vCYeXMNYtr)1?qwyg zsX_e*$h?380O00ou?0R@7-Fc59o$UvyVs4cUbujHUA>sH!}L54>`e` zHUx#Q+Hn&Og#YVOuo*niy*GU3rH;%f``nk#NN5-xrZ34NeH$l`4@t);4(+0|Z#I>Y z)~Kzs#exIAaf--65L0UHT_SvV8O2WYeD>Mq^Y6L!Xu8%vnpofG@w!}R7M28?i1*T&zp3X4^OMCY6(Dg<-! zXmcGQrRgHXGYre7GfTJ)rhl|rs%abKT_Nt24_Q``XH{88NVPW+`x4ZdrMuO0iZ0g` z%p}y};~T5gbb9SeL8BSc`SO#ixC$@QhXxZ=B}L`tP}&k?1oSPS=4%{UOHe0<_XWln zwbl5cn(j-qK`)vGHY5B5C|QZd5)W7c@{bNVXqJ!!n$^ufc?N9C-BF2QK1(kv++h!>$QbAjq)_b$$PcJdV+F7hz0Hu@ zqj+}m0qn{t^tD3DfBb~0B36|Q`bs*xs|$i^G4uNUEBl4g;op-;Wl~iThgga?+dL7s zUP(8lMO?g{GcYpDS{NM!UA8Hco?#}eNEioRBHy4`mq!Pd-9@-97|k$hpEX>xoX+dY zDr$wfm^P&}Wu{!%?)U_(%Mn79$(ywvu*kJ9r4u|MyYLI_67U7%6Gd_vb##Nerf@>& z8W11z$$~xEZt$dPG}+*IZky+os5Ju2eRi;1=rUEeIn>t-AzC_IGM-IXWK3^6QNU+2pe=MBn4I*R@A%-iLDCOHTE-O^wo$sL_h{dcPl=^muAQb`_BRm};=cy{qSkui;`WSsj9%c^+bIDQ z0`_?KX0<-=o!t{u(Ln)v>%VGL z0pC=GB7*AQ?N7N{ut*a%MH-tdtNmNC+Yf$|KS)BW(gQJ*z$d{+{j?(e&hgTy^2|AR9vx1Xre2fagGv0YXWqtNkg*v%40v?BJBt|f9wX5 z{QTlCM}b-0{mV?IG>TW_BdviUKhtosrBqdfq&Frdz>cF~yK{P@(w{Vr7z2qKFwLhc zQuogKO@~YwyS9%+d-zD7mJG~@?EFJLSn!a&mhE5$_4xBl&6QHMzL?CdzEnC~C3$X@ zvY!{_GR06ep5;<#cKCSJ%srxX=+pn?ywDwtJ2{TV;0DKBO2t++B(tIO4)Wh`rD13P z4fE$#%zkd=UzOB74gi=-*CuID&Z3zI^-`4U^S?dHxK8fP*;fE|a(KYMgMUo`THIS1f!*6dOI2 zFjC3O=-AL`6=9pp;`CYPTdVX z8(*?V&%QoipuH0>WKlL8A*zTKckD!paN@~hh zmXzm~qZhMGVdQGd=AG8&20HW0RGV8X{$9LldFZYm zE?}`Q3i?xJRz43S?VFMmqRyvWaS#(~Lempg9nTM$EFDP(Gzx#$r)W&lpFKqcAoJh-AxEw$-bjW>`_+gEi z2w`99#UbFZGiQjS8kj~@PGqpsPX`T{YOj`CaEqTFag;$jY z8_{Wzz>HXx&G*Dx<5skhpETxIdhKH?DtY@b9l8$l?UkM#J-Snmts7bd7xayKTFJ(u zyAT&@6cAYcs{PBfpqZa%sxhJ5nSZBPji?Zlf&}#L?t)vC4X5VLp%~fz2Sx<*oN<7` z?ge=k<=X7r<~F7Tvp9#HB{!mA!QWBOf%EiSJ6KIF8QZNjg&x~-%e*tflL(ji_S^sO ztmib1rp09uon}RcsFi#k)oLs@$?vs(i>5k3YN%$T(5Or(TZ5JW9mA6mIMD08=749$ z!d+l*iu{Il7^Yu}H;lgw=En1sJpCKPSqTCHy4(f&NPelr31^*l%KHq^QE>z>Ks_bH zjbD?({~8Din7IvZeJ>8Ey=e;I?thpzD=zE5UHeO|neioJwG;IyLk?xOz(yO&0DTU~ z^#)xcs|s>Flgmp;SmYJ4g(|HMu3v7#;c*Aa8iF#UZo7CvDq4>8#qLJ|YdZ!AsH%^_7N1IQjCro

K7UpUK$>l@ zw`1S}(D?mUXu_C{wupRS-jiX~w=Uqqhf|Vb3Cm9L=T+w91Cu^ z*&Ty%sN?x*h~mJc4g~k{xD4ZmF%FXZNC;oVDwLZ_WvrnzY|{v8hc1nmx4^}Z;yriXsAf+Lp+OFLbR!&Ox?xABwl zu8w&|5pCxmu#$?Cv2_-Vghl2LZ6m7}VLEfR5o2Ou$x02uA-%QB2$c(c1rH3R9hesc zfpn#oqpbKuVsdfV#cv@5pV4^f_!WS+F>SV6N0JQ9E!T90EX((_{bSSFv9ld%I0&}9 zH&Jd4MEX1e0iqDtq~h?DBrxQX1iI0lIs<|kB$Yrh&cpeK0-^K%=FBsCBT46@h#yi!AyDq1V(#V}^;{{V*@T4WJ&U-NTq43w=|K>z8%pr_nC>%C(Wa_l78Ufib$r8Od)IIN=u>417 z`Hl{9A$mI5A(;+-Q&$F&h-@;NR>Z<2U;Y21>>Z;s@0V@SbkMQQj%_;~+qTuQ?c|AV zcWm3XZQHhP&R%QWarS%mJ!9R^&!_)*s(v+VR@I#QrAT}`17Y+l<`b-nvmDNW`De%y zrwTZ9EJrj1AFA>B`1jYDow}~*dfPs}IZMO3=a{Fy#IOILc8F0;JS4x(k-NSpbN@qM z`@aE_e}5{!$v3+qVs7u?sOV(y@1Os*Fgu`fCW9=G@F_#VQ%xf$hj0~wnnP0$hFI+@ zkQj~v#V>xn)u??YutKsX>pxKCl^p!C-o?+9;!Nug^ z{rP!|+KsP5%uF;ZCa5F;O^9TGac=M|=V z_H(PfkV1rz4jl?gJ(ArXMyWT4y(86d3`$iI4^l9`vLdZkzpznSd5Ikfrs8qcSy&>z zTIZgWZGXw0n9ibQxYWE@gI0(3#KA-dAdPcsL_|hg2@~C!VZDM}5;v_Nykfq!*@*Zf zE_wVgx82GMDryKO{U{D>vSzSc%B~|cjDQrt5BN=Ugpsf8H8f1lR4SGo#hCuXPL;QQ z#~b?C4MoepT3X`qdW2dNn& zo8)K}%Lpu>0tQei+{>*VGErz|qjbK#9 zvtd8rcHplw%YyQCKR{kyo6fgg!)6tHUYT(L>B7er5)41iG`j$qe*kSh$fY!PehLcD zWeKZHn<492B34*JUQh=CY1R~jT9Jt=k=jCU2=SL&&y5QI2uAG2?L8qd2U(^AW#{(x zThSy=C#>k+QMo^7caQcpU?Qn}j-`s?1vXuzG#j8(A+RUAY})F@=r&F(8nI&HspAy4 z4>(M>hI9c7?DCW8rw6|23?qQMSq?*Vx?v30U%luBo)B-k2mkL)Ljk5xUha3pK>EEj z@(;tH|M@xkuN?gsz;*bygizwYR!6=(Xgcg^>WlGtRYCozY<rFX2E>kaZo)O<^J7a`MX8Pf`gBd4vrtD|qKn&B)C&wp0O-x*@-|m*0egT=-t@%dD zgP2D+#WPptnc;_ugD6%zN}Z+X4=c61XNLb7L1gWd8;NHrBXwJ7s0ce#lWnnFUMTR& z1_R9Fin4!d17d4jpKcfh?MKRxxQk$@)*hradH2$3)nyXep5Z;B z?yX+-Bd=TqO2!11?MDtG0n(*T^!CIiF@ZQymqq1wPM_X$Iu9-P=^}v7npvvPBu!d$ z7K?@CsA8H38+zjA@{;{kG)#AHME>Ix<711_iQ@WWMObXyVO)a&^qE1GqpP47Q|_AG zP`(AD&r!V^MXQ^e+*n5~Lp9!B+#y3#f8J^5!iC@3Y@P`;FoUH{G*pj*q7MVV)29+j z>BC`a|1@U_v%%o9VH_HsSnM`jZ-&CDvbiqDg)tQEnV>b%Ptm)T|1?TrpIl)Y$LnG_ zzKi5j2Fx^K^PG1=*?GhK;$(UCF-tM~^=Z*+Wp{FSuy7iHt9#4n(sUuHK??@v+6*|10Csdnyg9hAsC5_OrSL;jVkLlf zHXIPukLqbhs~-*oa^gqgvtpgTk_7GypwH><53riYYL*M=Q@F-yEPLqQ&1Sc zZB%w}T~RO|#jFjMWcKMZccxm-SL)s_ig?OC?y_~gLFj{n8D$J_Kw%{r0oB8?@dWzn zB528d-wUBQzrrSSLq?fR!K%59Zv9J4yCQhhDGwhptpA5O5U?Hjqt>8nOD zi{)0CI|&Gu%zunGI*XFZh(ix)q${jT8wnnzbBMPYVJc4HX*9d^mz|21$=R$J$(y7V zo0dxdbX3N#=F$zjstTf*t8vL)2*{XH!+<2IJ1VVFa67|{?LP&P41h$2i2;?N~RA30LV`BsUcj zfO9#Pg1$t}7zpv#&)8`mis3~o+P(DxOMgz-V*(?wWaxi?R=NhtW}<#^Z?(BhSwyar zG|A#Q7wh4OfK<|DAcl9THc-W4*>J4nTevsD%dkj`U~wSUCh15?_N@uMdF^Kw+{agk zJ`im^wDqj`Ev)W3k3stasP`88-M0ZBs7;B6{-tSm3>I@_e-QfT?7|n0D~0RRqDb^G zyHb=is;IwuQ&ITzL4KsP@Z`b$d%B0Wuhioo1CWttW8yhsER1ZUZzA{F*K=wmi-sb#Ju+j z-l@In^IKnb{bQG}Ps>+Vu_W#grNKNGto+yjA)?>0?~X`4I3T@5G1)RqGUZuP^NJCq&^HykuYtMDD8qq+l8RcZNJsvN(10{ zQ1$XcGt}QH-U^WU!-wRR1d--{B$%vY{JLWIV%P4-KQuxxDeJaF#{eu&&r!3Qu{w}0f--8^H|KwE>)ORrcR+2Qf zb})DRcH>k0zWK8@{RX}NYvTF;E~phK{+F;MkIP$)T$93Ba2R2TvKc>`D??#mv9wg$ zd~|-`Qx5LwwsZ2hb*Rt4S9dsF%Cny5<1fscy~)d;0m2r$f=83<->c~!GNyb!U)PA; zq^!`@@)UaG)Ew(9V?5ZBq#c%dCWZrplmuM`o~TyHjAIMh0*#1{B>K4po-dx$Tk-Cq z=WZDkP5x2W&Os`N8KiYHRH#UY*n|nvd(U>yO=MFI-2BEp?x@=N<~CbLJBf6P)}vLS?xJXYJ2^<3KJUdrwKnJnTp{ zjIi|R=L7rn9b*D#Xxr4*R<3T5AuOS+#U8hNlfo&^9JO{VbH!v9^JbK=TCGR-5EWR@ zN8T-_I|&@A}(hKeL4_*eb!1G8p~&_Im8|wc>Cdir+gg90n1dw?QaXcx6Op_W1r=axRw>4;rM*UOpT#Eb9xU1IiWo@h?|5uP zka>-XW0Ikp@dIe;MN8B01a7+5V@h3WN{J=HJ*pe0uwQ3S&MyWFni47X32Q7SyCTNQ z+sR!_9IZa5!>f&V$`q!%H8ci!a|RMx5}5MA_kr+bhtQy{-^)(hCVa@I!^TV4RBi zAFa!Nsi3y37I5EK;0cqu|9MRj<^r&h1lF}u0KpKQD^5Y+LvFEwM zLU@@v4_Na#Axy6tn3P%sD^5P#<7F;sd$f4a7LBMk zGU^RZHBcxSA%kCx*eH&wgA?Qwazm8>9SCSz_!;MqY-QX<1@p$*T8lc?@`ikEqJ>#w zcG``^CoFMAhdEXT9qt47g0IZkaU)4R7wkGs^Ax}usqJ5HfDYAV$!=6?>J6+Ha1I<5 z|6=9soU4>E))tW$<#>F ziZ$6>KJf0bPfbx_)7-}tMINlc=}|H+$uX)mhC6-Hz+XZxsKd^b?RFB6et}O#+>Wmw9Ec9) z{q}XFWp{3@qmyK*Jvzpyqv57LIR;hPXKsrh{G?&dRjF%Zt5&m20Ll?OyfUYC3WRn{cgQ?^V~UAv+5 z&_m#&nIwffgX1*Z2#5^Kl4DbE#NrD&Hi4|7SPqZ}(>_+JMz=s|k77aEL}<=0Zfb)a z%F(*L3zCA<=xO)2U3B|pcTqDbBoFp>QyAEU(jMu8(jLA61-H!ucI804+B!$E^cQQa z)_ERrW3g!B9iLb3nn3dlkvD7KsY?sRvls3QC0qPi>o<)GHx%4Xb$5a3GBTJ(k@`e@ z$RUa^%S15^1oLEmA=sayrP5;9qtf!Z1*?e$ORVPsXpL{jL<6E)0sj&swP3}NPmR%FM?O>SQgN5XfHE< zo(4#Cv11(%Nnw_{_Ro}r6=gKd{k?NebJ~<~Kv0r(r0qe4n3LFx$5%x(BKvrz$m?LG zjLIc;hbj0FMdb9aH9Lpsof#yG$(0sG2%RL;d(n>;#jb!R_+dad+K;Ccw!|RY?uS(a zj~?=&M!4C(5LnlH6k%aYvz@7?xRa^2gml%vn&eKl$R_lJ+e|xsNfXzr#xuh(>`}9g zLHSyiFwK^-p!;p$yt7$F|3*IfO3Mlu9e>Dpx8O`37?fA`cj`C0B-m9uRhJjs^mRp# zWB;Aj6|G^1V6`jg7#7V9UFvnB4((nIwG?k%c7h`?0tS8J3Bn0t#pb#SA}N-|45$-j z$R>%7cc2ebAClXc(&0UtHX<>pd)akR3Kx_cK+n<}FhzmTx!8e9^u2e4%x{>T6pQ`6 zO182bh$-W5A3^wos0SV_TgPmF4WUP-+D25KjbC{y_6W_9I2_vNKwU(^qSdn&>^=*t z&uvp*@c8#2*paD!ZMCi3;K{Na;I4Q35zw$YrW5U@Kk~)&rw;G?d7Q&c9|x<Hg|CNMsxovmfth*|E*GHezPTWa^Hd^F4!B3sF;)? z(NaPyAhocu1jUe(!5Cy|dh|W2=!@fNmuNOzxi^tE_jAtzNJ0JR-avc_H|ve#KO}#S z#a(8secu|^Tx553d4r@3#6^MHbH)vmiBpn0X^29xEv!Vuh1n(Sr5I0V&`jA2;WS|Y zbf0e}X|)wA-Pf5gBZ>r4YX3Mav1kKY(ulAJ0Q*jB)YhviHK)w!TJsi3^dMa$L@^{` z_De`fF4;M87vM3Ph9SzCoCi$#Fsd38u!^0#*sPful^p5oI(xGU?yeYjn;Hq1!wzFk zG&2w}W3`AX4bxoVm03y>ts{KaDf!}b&7$(P4KAMP=vK5?1In^-YYNtx1f#}+2QK@h zeSeAI@E6Z8a?)>sZ`fbq9_snl6LCu6g>o)rO;ijp3|$vig+4t} zylEo7$SEW<_U+qgVcaVhk+4k+C9THI5V10qV*dOV6pPtAI$)QN{!JRBKh-D zk2^{j@bZ}yqW?<#VVuI_27*cI-V~sJiqQv&m07+10XF+#ZnIJdr8t`9s_EE;T2V;B z4UnQUH9EdX%zwh-5&wflY#ve!IWt0UE-My3?L#^Bh%kcgP1q{&26eXLn zTkjJ*w+(|_>Pq0v8{%nX$QZbf)tbJaLY$03;MO=Ic-uqYUmUCuXD>J>o6BCRF=xa% z3R4SK9#t1!K4I_d>tZgE>&+kZ?Q}1qo4&h%U$GfY058s%*=!kac{0Z+4Hwm!)pFLR zJ+5*OpgWUrm0FPI2ib4NPJ+Sk07j(`diti^i#kh&f}i>P4~|d?RFb#!JN)~D@)beox}bw?4VCf^y*`2{4`-@%SFTry2h z>9VBc9#JxEs1+0i2^LR@B1J`B9Ac=#FW=(?2;5;#U$0E0UNag_!jY$&2diQk_n)bT zl5Me_SUvqUjwCqmVcyb`igygB_4YUB*m$h5oeKv3uIF0sk}~es!{D>4r%PC*F~FN3owq5e0|YeUTSG#Vq%&Gk7uwW z0lDo#_wvflqHeRm*}l?}o;EILszBt|EW*zNPmq#?4A+&i0xx^?9obLyY4xx=Y9&^G;xYXYPxG)DOpPg!i_Ccl#3L}6xAAZzNhPK1XaC_~ z!A|mlo?Be*8Nn=a+FhgpOj@G7yYs(Qk(8&|h@_>w8Y^r&5nCqe0V60rRz?b5%J;GYeBqSAjo|K692GxD4` zRZyM2FdI+-jK2}WAZTZ()w_)V{n5tEb@>+JYluDozCb$fA4H)$bzg(Ux{*hXurjO^ zwAxc+UXu=&JV*E59}h3kzQPG4M)X8E*}#_&}w*KEgtX)cU{vm9b$atHa;s>| z+L6&cn8xUL*OSjx4YGjf6{Eq+Q3{!ZyhrL&^6Vz@jGbI%cAM9GkmFlamTbcQGvOlL zmJ?(FI)c86=JEs|*;?h~o)88>12nXlpMR4@yh%qdwFNpct;vMlc=;{FSo*apJ;p}! zAX~t;3tb~VuP|ZW;z$=IHf->F@Ml)&-&Bnb{iQyE#;GZ@C$PzEf6~q}4D>9jic@mTO5x76ulDz@+XAcm35!VSu zT*Gs>;f0b2TNpjU_BjHZ&S6Sqk6V1370+!eppV2H+FY!q*n=GHQ!9Rn6MjY!Jc77A zG7Y!lFp8?TIHN!LXO?gCnsYM-gQxsm=Ek**VmZu7vnuufD7K~GIxfxbsQ@qv2T zPa`tvHB$fFCyZl>3oYg?_wW)C>^_iDOc^B7klnTOoytQH18WkOk)L2BSD0r%xgRSW zQS9elF^?O=_@|58zKLK;(f77l-Zzu}4{fXed2saq!5k#UZAoDBqYQS{sn@j@Vtp|$ zG%gnZ$U|9@u#w1@11Sjl8ze^Co=)7yS(}=;68a3~g;NDe_X^}yJj;~s8xq9ahQ5_r zxAlTMnep*)w1e(TG%tWsjo3RR;yVGPEO4V{Zp?=a_0R#=V^ioQu4YL=BO4r0$$XTX zZfnw#_$V}sDAIDrezGQ+h?q24St0QNug_?{s-pI(^jg`#JRxM1YBV;a@@JQvH8*>> zIJvku74E0NlXkYe_624>znU0J@L<-c=G#F3k4A_)*;ky!C(^uZfj%WB3-*{*B$?9+ zDm$WFp=0(xnt6`vDQV3Jl5f&R(Mp};;q8d3I%Kn>Kx=^;uSVCw0L=gw53%Bp==8Sw zxtx=cs!^-_+i{2OK`Q;913+AXc_&Z5$@z3<)So0CU3;JAv=H?@Zpi~riQ{z-zLtVL z!oF<}@IgJp)Iyz1zVJ42!SPHSkjYNS4%ulVVIXdRuiZ@5Mx8LJS}J#qD^Zi_xQ@>DKDr-_e#>5h3dtje*NcwH_h;i{Sx7}dkdpuW z(yUCjckQsagv*QGMSi9u1`Z|V^}Wjf7B@q%j2DQXyd0nOyqg%m{CK_lAoKlJ7#8M} z%IvR?Vh$6aDWK2W!=i?*<77q&B8O&3?zP(Cs@kapc)&p7En?J;t-TX9abGT#H?TW? ztO5(lPKRuC7fs}zwcUKbRh=7E8wzTsa#Z{a`WR}?UZ%!HohN}d&xJ=JQhpO1PI#>X zHkb>pW04pU%Bj_mf~U}1F1=wxdBZu1790>3Dm44bQ#F=T4V3&HlOLsGH)+AK$cHk6 zia$=$kog?)07HCL*PI6}DRhpM^*%I*kHM<#1Se+AQ!!xyhcy6j7`iDX7Z-2i73_n# zas*?7LkxS-XSqv;YBa zW_n*32D(HTYQ0$feV_Fru1ZxW0g&iwqixPX3=9t4o)o|kOo79V$?$uh?#8Q8e>4e)V6;_(x&ViUVxma+i25qea;d-oK7ouuDsB^ab{ zu1qjQ%`n56VtxBE#0qAzb7lph`Eb-}TYpXB!H-}3Ykqyp`otprp7{VEuW*^IR2n$Fb99*nAtqT&oOFIf z@w*6>YvOGw@Ja?Pp1=whZqydzx@9X4n^2!n83C5{C?G@|E?&$?p*g68)kNvUTJ)I6 z1Q|(#UuP6pj78GUxq11m-GSszc+)X{C2eo-?8ud9sB=3(D47v?`JAa{V(IF zPZQ_0AY*9M97>Jf<o%#O_%Wq}8>YM=q0|tGY+hlXcpE=Z4Od z`NT7Hu2hnvRoqOw@g1f=bv`+nba{GwA$Ak0INlqI1k<9!x_!sL()h?hEWoWrdU3w` zZ%%)VR+Bc@_v!C#koM1p-3v_^L6)_Ktj4HE>aUh%2XZE@JFMOn)J~c`_7VWNb9c-N z2b|SZMR4Z@E7j&q&9(6H3yjEu6HV7{2!1t0lgizD;mZ9$r(r7W5G$ky@w(T_dFnOD z*p#+z$@pKE+>o@%eT(2-p_C}wbQ5s(%Sn_{$HDN@MB+Ev?t@3dPy`%TZ!z}AThZSu zN<1i$siJhXFdjV zP*y|V<`V8t=h#XTRUR~5`c`Z9^-`*BZf?WAehGdg)E2Je)hqFa!k{V(u+(hTf^Yq& zoruUh2(^3pe)2{bvt4&4Y9CY3js)PUHtd4rVG57}uFJL)D(JfSIo^{P=7liFXG zq5yqgof0V8paQcP!gy+;^pp-DA5pj=gbMN0eW=-eY+N8~y+G>t+x}oa!5r>tW$xhI zPQSv=pi;~653Gvf6~*JcQ%t1xOrH2l3Zy@8AoJ+wz@daW@m7?%LXkr!bw9GY@ns3e zSfuWF_gkWnesv?s3I`@}NgE2xwgs&rj?kH-FEy82=O8`+szN ziHch`vvS`zNfap14!&#i9H@wF7}yIPm=UB%(o(}F{wsZ(wA0nJ2aD^@B41>>o-_U6 zUqD~vdo48S8~FTb^+%#zcbQiiYoDKYcj&$#^;Smmb+Ljp(L=1Kt_J!;0s%1|JK}Wi z;={~oL!foo5n8=}rs6MmUW~R&;SIJO3TL4Ky?kh+b2rT9B1Jl4>#Uh-Bec z`Hsp<==#UEW6pGPhNk8H!!DUQR~#F9jEMI6T*OWfN^Ze&X(4nV$wa8QUJ>oTkruH# zm~O<`J7Wxseo@FqaZMl#Y(mrFW9AHM9Kb|XBMqaZ2a)DvJgYipkDD_VUF_PKd~dT7 z#02}bBfPn9a!X!O#83=lbJSK#E}K&yx-HI#T6ua)6o0{|={*HFusCkHzs|Fn&|C3H zBck1cmfcWVUN&i>X$YU^Sn6k2H;r3zuXbJFz)r5~3$d$tUj(l1?o={MM){kjgqXRO zc5R*#{;V7AQh|G|)jLM@wGAK&rm2~@{Pewv#06pHbKn#wL0P6F1!^qw9g&cW3Z=9} zj)POhOlwsh@eF=>z?#sIs*C-Nl(yU!#DaiaxhEs#iJqQ8w%(?+6lU02MYSeDkr!B- zPjMv+on6OLXgGnAtl(ao>|X2Y8*Hb}GRW5}-IzXnoo-d0!m4Vy$GS!XOLy>3_+UGs z2D|YcQx@M#M|}TDOetGi{9lGo9m-=0-^+nKE^*?$^uHkxZh}I{#UTQd;X!L+W@jm( zDg@N4+lUqI92o_rNk{3P>1gxAL=&O;x)ZT=q1mk0kLlE$WeWuY_$0`0jY-Kkt zP*|m3AF}Ubd=`<>(Xg0har*_@x2YH}bn0Wk*OZz3*e5;Zc;2uBdnl8?&XjupbkOeNZsNh6pvsq_ydmJI+*z**{I{0K)-;p1~k8cpJXL$^t!-`E}=*4G^-E8>H!LjTPxSx zcF+cS`ommfKMhNSbas^@YbTpH1*RFrBuATUR zt{oFWSk^$xU&kbFQ;MCX22RAN5F6eq9UfR$ut`Jw--p2YX)A*J69m^!oYfj2y7NYcH6&r+0~_sH^c^nzeN1AU4Ga7=FlR{S|Mm~MpzY0$Z+p2W(a={b-pR9EO1Rs zB%KY|@wLcAA@)KXi!d2_BxrkhDn`DT1=Dec}V!okd{$+wK z4E{n8R*xKyci1(CnNdhf$Dp2(Jpof0-0%-38X=Dd9PQgT+w%Lshx9+loPS~MOm%ZT zt%2B2iL_KU_ita%N>xjB!#71_3=3c}o zgeW~^U_ZTJQ2!PqXulQd=3b=XOQhwATK$y(9$#1jOQ4}4?~l#&nek)H(04f(Sr=s| zWv7Lu1=%WGk4FSw^;;!8&YPM)pQDCY9DhU`hMty1@sq1=Tj7bFsOOBZOFlpR`W>-J$-(kezWJj;`?x-v>ev{*8V z8p|KXJPV$HyQr1A(9LVrM47u-XpcrIyO`yWvx1pVYc&?154aneRpLqgx)EMvRaa#|9?Wwqs2+W8n5~79G z(}iCiLk;?enn}ew`HzhG+tu+Ru@T+K5juvZN)wY;x6HjvqD!&!)$$;1VAh~7fg0K| zEha#aN=Yv|3^~YFH}cc38ovVb%L|g@9W6fo(JtT6$fa?zf@Ct88e}m?i)b*Jgc{fl zExfdvw-BYDmH6>(4QMt#p0;FUIQqkhD}aH?a7)_%JtA~soqj{ppP_82yi9kaxuK>~ ze_)Zt>1?q=ZH*kF{1iq9sr*tVuy=u>Zev}!gEZx@O6-fjyu9X00gpIl-fS_pzjpqJ z1yqBmf9NF!jaF<+YxgH6oXBdK)sH(>VZ)1siyA$P<#KDt;8NT*l_0{xit~5j1P)FN zI8hhYKhQ)i z37^aP13B~u65?sg+_@2Kr^iWHN=U;EDSZ@2W2!5ALhGNWXnFBY%7W?1 z=HI9JzQ-pLKZDYTv<0-lt|6c-RwhxZ)mU2Os{bsX_i^@*fKUj8*aDO5pks=qn3Dv6 zwggpKLuyRCTVPwmw1r}B#AS}?X7b837UlXwp~E2|PJw2SGVueL7){Y&z!jL!XN=0i zU^Eig`S2`{+gU$68aRdWx?BZ{sU_f=8sn~>s~M?GU~`fH5kCc; z8ICp+INM3(3{#k32RZdv6b9MQYdZXNuk7ed8;G?S2nT+NZBG=Tar^KFl2SvhW$bGW#kdWL-I)s_IqVnCDDM9fm8g;P;8 z7t4yZn3^*NQfx7SwmkzP$=fwdC}bafQSEF@pd&P8@H#`swGy_rz;Z?Ty5mkS%>m#% zp_!m9e<()sfKiY(nF<1zBz&&`ZlJf6QLvLhl`_``%RW&{+O>Xhp;lwSsyRqGf=RWd zpftiR`={2(siiPAS|p}@q=NhVc0ELprt%=fMXO3B)4ryC2LT(o=sLM7hJC!}T1@)E zA3^J$3&1*M6Xq>03FX`R&w*NkrZE?FwU+Muut;>qNhj@bX17ZJxnOlPSZ=Zeiz~T_ zOu#yc3t6ONHB;?|r4w+pI)~KGN;HOGC)txxiUN8#mexj+W(cz%9a4sx|IRG=}ia zuEBuba3AHsV2feqw-3MvuL`I+2|`Ud4~7ZkN=JZ;L20|Oxna5vx1qbIh#k2O4$RQF zo`tL()zxaqibg^GbB+BS5#U{@K;WWQj~GcB1zb}zJkPwH|5hZ9iH2308!>_;%msji zJHSL~s)YHBR=Koa1mLEOHos*`gp=s8KA-C zu0aE+W!#iJ*0xqKm3A`fUGy#O+X+5W36myS>Uh2!R*s$aCU^`K&KKLCCDkejX2p=5 z%o7-fl03x`gaSNyr?3_JLv?2RLS3F*8ub>Jd@^Cc17)v8vYEK4aqo?OS@W9mt%ITJ z9=S2%R8M){CugT@k~~0x`}Vl!svYqX=E)c_oU6o}#Hb^%G1l3BudxA{F*tbjG;W_>=xV73pKY53v%>I)@D36I_@&p$h|Aw zonQS`07z_F#@T-%@-Tb|)7;;anoD_WH>9ewFy(ZcEOM$#Y)8>qi7rCnsH9GO-_7zF zu*C87{Df1P4TEOsnzZ@H%&lvV(3V@;Q!%+OYRp`g05PjY^gL$^$-t0Y>H*CDDs?FZly*oZ&dxvsxaUWF!{em4{A>n@vpXg$dwvt@_rgmHF z-MER`ABa8R-t_H*kv>}CzOpz;!>p^^9ztHMsHL|SRnS<-y5Z*r(_}c4=fXF`l^-i}>e7v!qs_jv zqvWhX^F=2sDNWA9c@P0?lUlr6ecrTKM%pNQ^?*Lq?p-0~?_j50xV%^(+H>sMul#Tw zeciF*1=?a7cI(}352%>LO96pD+?9!fNyl^9v3^v&Y4L)mNGK0FN43&Xf8jUlxW1Bw zyiu2;qW-aGNhs=zbuoxnxiwZ3{PFZM#Kw)9H@(hgX23h(`Wm~m4&TvoZoYp{plb^> z_#?vXcxd>r7K+1HKJvhed>gtK`TAbJUazUWQY6T~t2af%#<+Veyr%7-#*A#@&*;@g58{i|E%6yC_InGXCOd{L0;$)z#?n7M`re zh!kO{6=>7I?*}czyF7_frt#)s1CFJ_XE&VrDA?Dp3XbvF{qsEJgb&OLSNz_5g?HpK z9)8rsr4JN!Af3G9!#Qn(6zaUDqLN(g2g8*M)Djap?WMK9NKlkC)E2|-g|#-rp%!Gz zAHd%`iq|81efi93m3yTBw3g0j#;Yb2X{mhRAI?&KDmbGqou(2xiRNb^sV}%%Wu0?< z?($L>(#BO*)^)rSgyNRni$i`R4v;GhlCZ8$@e^ROX(p=2_v6Y!%^As zu022)fHdv_-~Yu_H6WVPLpHQx!W%^6j)cBhS`O3QBW#x(eX54d&I22op(N59b*&$v zFiSRY6rOc^(dgSV1>a7-5C;(5S5MvKcM2Jm-LD9TGqDpP097%52V+0>Xqq!! zq4e3vj53SE6i8J`XcQB|MZPP8j;PAOnpGnllH6#Ku~vS42xP*Nz@~y%db7Xi8s09P z1)e%8ys6&M8D=Dt6&t`iKG_4X=!kgRQoh%Z`dc&mlOUqXk-k`jKv9@(a^2-Upw>?< zt5*^DV~6Zedbec4NVl($2T{&b)zA@b#dUyd>`2JC0=xa_fIm8{5um zr-!ApXZhC8@=vC2WyxO|!@0Km)h8ep*`^he92$@YwP>VcdoS5OC^s38e#7RPsg4j+ zbVGG}WRSET&ZfrcR(x~k8n1rTP%CnfUNKUonD$P?FtNFF#cn!wEIab-;jU=B1dHK@ z(;(yAQJ`O$sMn>h;pf^8{JISW%d+@v6@CnXh9n5TXGC}?FI9i-D0OMaIg&mAg=0Kn zNJ7oz5*ReJukD55fUsMuaP+H4tDN&V9zfqF@ zr=#ecUk9wu{0;!+gl;3Bw=Vn^)z$ahVhhw)io!na&9}LmWurLb0zubxK=UEnU*{5P z+SP}&*(iBKSO4{alBHaY^)5Q=mZ+2OwIooJ7*Q5XJ+2|q`9#f?6myq!&oz?klihLq z4C)$XP!BNS0G_Z1&TM>?Jk{S~{F3n83ioli=IO6f%wkvCl(RFFw~j0tb{GvXTx>*sB0McY0s&SNvj4+^h`9nJ_wM>F!Uc>X}9PifQekn0sKI2SAJP!a4h z5cyGTuCj3ZBM^&{dRelIlT^9zcfaAuL5Y~bl!ppSf`wZbK$z#6U~rdclk``e+!qhe z6Qspo*%<)eu6?C;Bp<^VuW6JI|Ncvyn+LlSl;Mp22Bl7ARQ0Xc24%29(ZrdsIPw&-=yHQ7_Vle|5h>AST0 zUGX2Zk34vp?U~IHT|;$U86T+UUHl_NE4m|}>E~6q``7hccCaT^#y+?wD##Q%HwPd8 zV3x4L4|qqu`B$4(LXqDJngNy-{&@aFBvVsywt@X^}iH7P%>bR?ciC$I^U-4Foa`YKI^qDyGK7k%E%c_P=yzAi`YnxGA%DeNd++j3*h^ z=rn>oBd0|~lZ<6YvmkKY*ZJlJ;Im0tqgWu&E92eqt;+NYdxx`eS(4Hw_Jb5|yVvBg z*tbdY^!AN;luEyN4VRhS@-_DC{({ziH{&Z}iGElSV~qvT>L-8G%+yEL zX#MFOhj{InyKG=mvW-<1B@c-}x$vA(nU?>S>0*eN#!SLzQ)Ex7fvQ)S4D<8|I#N$3 zT5Ei`Z?cxBODHX8(Xp73v`IsAYC@9b;t}z0wxVuQSY1J^GRwDPN@qbM-ZF48T$GZ< z8WU+;Pqo?{ghI-KZ-i*ydXu`Ep0Xw^McH_KE9J0S7G;x8Fe`DVG?j3Pv=0YzJ}yZR z%2=oqHiUjvuk0~Ca>Kol4CFi0_xQT~;_F?=u+!kIDl-9g`#ZNZ9HCy17Ga1v^Jv9# z{T4Kb1-AzUxq*MutfOWWZgD*HnFfyYg0&e9f(5tZ>krPF6{VikNeHoc{linPPt#Si z&*g>(c54V8rT_AX!J&bNm-!umPvOR}vDai#`CX___J#=zeB*{4<&2WpaDncZsOkp* zsg<%@@rbrMkR_ux9?LsQxzoBa1s%$BBn6vk#{&&zUwcfzeCBJUwFYSF$08qDsB;gWQN*g!p8pxjofWbqNSZOEKOaTx@+* zwdt5*Q47@EOZ~EZL9s?1o?A%9TJT=Ob_13yyugvPg*e&ZU(r6^k4=2+D-@n=Hv5vu zSXG|hM(>h9^zn=eQ=$6`JO&70&2|%V5Lsx>)(%#;pcOfu>*nk_3HB_BNaH$`jM<^S zcSftDU1?nL;jy)+sfonQN}(}gUW?d_ikr*3=^{G)=tjBtEPe>TO|0ddVB zTklrSHiW+!#26frPXQQ(YN8DG$PZo?(po(QUCCf_OJC`pw*uey00%gmH!`WJkrKXj2!#6?`T25mTu9OJp2L8z3! z=arrL$ZqxuE{%yV)14Kd>k}j7pxZ6#$Dz8$@WV5p8kTqN<-7W)Q7Gt2{KoOPK_tZ| zf2WG~O5@{qPI+W<4f_;reuFVdO^5`ADC1!JQE|N`s3cq@(0WB!n0uh@*c{=LAd;~} zyGK@hbF-Oo+!nN)@i*O(`@FA#u?o=~e{`4O#5}z&=UkU*50fOrzi11D^&FOqe>wii z?*k+2|EcUs;Gx{!@KBT~>PAwLrIDT7Th=Utu?~?np@t^gFs?zgX=D${RwOY^WGh-+ z+#4$066ISh8eYW#FXWp~S`<*%O^ZuItL1Tyqt8#tZ zY120E;^VG`!lZn&3sPd$RkdHpU#|w+bYV)pJC|SH9g%|5IkxVTQcBA4CL0}$&}ef@ zW^Vtj%M;;_1xxP9x#ex17&4N*{ksO*_4O}xYu(p*JkL#yr}@7b)t5X?%CY<+s5_MJ zuiqt+N_;A(_)%lumoyRFixWa-M7qK_9s6<1X?JDa9fP!+_6u~~M$5L=ipB=7(j#f< zZ34J%=bs549%~_mA(|={uZNs_0?o7;-LBP(ZRnkd{-^|2|=4vUTmtByHL8 zEph`(LSEzQj68a+`d$V<45J7cyv^#|^|%fD#si1Nx!4NW*`l*{->HEWNh6-|g>-=r zXmQ|-i}Ku$ndUeHQ^&ieT!Lf}vf6GaqW9$DJ2NWrqwPY%%4nip$@vK$nRp*_C-v<| zuKz~ZyN&<%!NS26&x?jhy+@awJipMQ-8(X4#Ae5??U<1QMt1l9R=w9fAnEF}NYu$2 z>6}Vkc zIb*A?G*z8^IvibmBKn_u^5&T_1oey0gZS2~obf(#xk=erZGTEdQnt3DMGM+0oPwss zj5zXD;(oWhB_T@~Ig#9@v)AKtXu3>Inmgf@A|-lD-1U>cNyl3h?ADD9)GG4}zUGPk zZzaXe!~Kf?<~@$G?Uql3t8jy9{2!doq4=J}j9ktTxss{p6!9UdjyDERlA*xZ!=Q)KDs5O)phz>Vq3BNGoM(H|=1*Q4$^2fTZw z(%nq1P|5Rt81}SYJpEEzMPl5VJsV5&4e)ZWKDyoZ>1EwpkHx-AQVQc8%JMz;{H~p{=FXV>jIxvm4X*qv52e?Y-f%DJ zxEA165GikEASQ^fH6K#d!Tpu2HP{sFs%E=e$gYd$aj$+xue6N+Wc(rAz~wUsk2`(b z8Kvmyz%bKQxpP}~baG-rwYcYCvkHOi zlkR<=>ZBTU*8RF_d#Bl@zZsRIhx<%~Z@Z=ik z>adw3!DK(8R|q$vy{FTxw%#xliD~6qXmY^7_9kthVPTF~Xy1CfBqbU~?1QmxmU=+k z(ggxvEuA;0e&+ci-zQR{-f7aO{O(Pz_OsEjLh_K>MbvoZ4nxtk5u{g@nPv)cgW_R} z9}EA4K4@z0?7ue}Z(o~R(X&FjejUI2g~08PH1E4w>9o{)S(?1>Z0XMvTb|;&EuyOE zGvWNpYX)Nv<8|a^;1>bh#&znEcl-r!T#pn= z4$?Yudha6F%4b>*8@=BdtXXY4N+`U4Dmx$}>HeVJk-QdTG@t!tVT#0(LeV0gvqyyw z2sEp^9eY0N`u10Tm4n8No&A=)IeEC|gnmEXoNSzu!1<4R<%-9kY_8~5Ej?zRegMn78wuMs#;i&eUA0Zk_RXQ3b&TT} z;SCI=7-FUB@*&;8|n>(_g^HGf3@QODE3LpmX~ELnymQm{Sx9xrKS zK29p~?v@R$0=v6Dr5aW>-!{+h@?Q58|Kz8{{W`%J+lDAdb&M5VHrX_mDY;1-JLnf)ezmPau$)1;=`-FU=-r-83tX=C`S#}GZufju zQ>sXNT0Ny=k@nc%cFnvA_i4SC)?_ORXHq8B4D%el1uPX`c~uG#S1M7C+*MMqLw78E zhY2dI8@+N^qrMI1+;TUda(vGqGSRyU{Fnm`aqrr7bz42c5xsOO-~oZpkzorD1g}Y<6rk&3>PsSGy}W?MtqFky@A(X# zIuNZK0cK?^=;PUAu>j0#HtjbHCV*6?jzA&OoE$*Jlga*}LF`SF?WLhv1O|zqC<>*> zYB;#lsYKx0&kH@BFpW8n*yDcc6?;_zaJs<-jPSkCsSX-!aV=P5kUgF@Nu<{a%#K*F z134Q{9|YX7X(v$62_cY3^G%t~rD>Q0z@)1|zs)vjJ6Jq9;7#Ki`w+eS**En?7;n&7 zu==V3T&eFboN3ZiMx3D8qYc;VjFUk_H-WWCau(VFXSQf~viH0L$gwD$UfFHqNcgN`x}M+YQ6RnN<+@t>JUp#)9YOkqst-Ga?{FsDpEeX0(5v{0J~SEbWiL zXC2}M4?UH@u&|;%0y`eb33ldo4~z-x8zY!oVmV=c+f$m?RfDC35mdQ2E>Pze7KWP- z>!Bh<&57I+O_^s}9Tg^k)h7{xx@0a0IA~GAOt2yy!X%Q$1rt~LbTB6@Du!_0%HV>N zlf)QI1&gvERKwso23mJ!Ou6ZS#zCS5W`gxE5T>C#E|{i<1D35C222I33?Njaz`On7 zi<+VWFP6D{e-{yiN#M|Jgk<44u1TiMI78S5W`Sdb5f+{zu34s{CfWN7a3Cf^@L%!& zN$?|!!9j2c)j$~+R6n#891w-z8(!oBpL2K=+%a$r2|~8-(vQj5_XT`<0Ksf;oP+tz z9CObS!0m)Tgg`K#xBM8B(|Z)Wb&DYL{WTYv`;A=q6~Nnx2+!lTIXtj8J7dZE!P_{z z#f8w6F}^!?^KE#+ZDv+xd5O&3EmomZzsv?>E-~ygGum45fk!SBN&|eo1rKw^?aZJ4 E2O(~oYXATM literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..f371643ee --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 000000000..4f906e0c8 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/grpc-client-spring-boot-autoconfigure/build.gradle b/grpc-client-spring-boot-autoconfigure/build.gradle new file mode 100644 index 000000000..5ef6ad3d1 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'java-library' +} + +apply from: '../deploy.gradle' + +group = 'net.devh' +version = projectVersion + +compileJava.dependsOn(processResources) + +dependencies { + annotationProcessor 'org.springframework.boot:spring-boot-autoconfigure-processor' + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + + api project(':grpc-common-spring-boot') + api 'org.springframework.boot:spring-boot-starter' + optionalSupportImplementation 'org.springframework.boot:spring-boot-starter-actuator' + optionalSupportImplementation 'org.springframework.cloud:spring-cloud-starter-sleuth' + optionalSupportImplementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery' + optionalSupportImplementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + optionalSupportImplementation 'io.zipkin.brave:brave-instrumentation-grpc' + optionalSupportImplementation 'javax.inject:javax.inject:1' + // api comes from java-library, and allows exposure of the dependency to consumers of the library + // this means it can be used implicitly without specifying the dependency (unless you wish to override with a different version, or exclude) + optionalSupportApi 'io.grpc:grpc-netty' + optionalSupportApi 'io.netty:netty-transport-native-epoll' + api 'io.grpc:grpc-netty-shaded' + api 'io.grpc:grpc-protobuf' + api 'io.grpc:grpc-stub' + + testImplementation 'io.grpc:grpc-testing' + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude module: 'junit-vintage-engine' + exclude module: 'junit' + } + + testImplementation 'org.junit.jupiter:junit-jupiter-api' + + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java new file mode 100644 index 000000000..3bf460210 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java @@ -0,0 +1,191 @@ +/* + * 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.autoconfigure; + +import java.util.Collections; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import io.grpc.NameResolverProvider; +import io.grpc.NameResolverRegistry; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.channelfactory.GrpcChannelConfigurer; +import net.devh.boot.grpc.client.channelfactory.GrpcChannelFactory; +import net.devh.boot.grpc.client.channelfactory.InProcessChannelFactory; +import net.devh.boot.grpc.client.channelfactory.InProcessOrAlternativeChannelFactory; +import net.devh.boot.grpc.client.channelfactory.NettyChannelFactory; +import net.devh.boot.grpc.client.channelfactory.ShadedNettyChannelFactory; +import net.devh.boot.grpc.client.config.GrpcChannelsProperties; +import net.devh.boot.grpc.client.inject.GrpcClientBeanPostProcessor; +import net.devh.boot.grpc.client.interceptor.AnnotationGlobalClientInterceptorConfigurer; +import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorRegistry; +import net.devh.boot.grpc.client.nameresolver.NameResolverRegistration; +import net.devh.boot.grpc.client.stubfactory.AsyncStubFactory; +import net.devh.boot.grpc.client.stubfactory.BlockingStubFactory; +import net.devh.boot.grpc.client.stubfactory.FutureStubFactory; +import net.devh.boot.grpc.common.autoconfigure.GrpcCommonCodecAutoConfiguration; + +/** + * The auto configuration used by Spring-Boot that contains all beans to create and inject grpc clients into beans. + * + * @author Michael (yidongnan@gmail.com) + */ +@Slf4j +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties +@AutoConfigureAfter(name = "org.springframework.cloud.client.CommonsClientAutoConfiguration", + value = GrpcCommonCodecAutoConfiguration.class) +public class GrpcClientAutoConfiguration { + + @Bean + static GrpcClientBeanPostProcessor grpcClientBeanPostProcessor(final ApplicationContext applicationContext) { + return new GrpcClientBeanPostProcessor(applicationContext); + } + + @Bean + AsyncStubFactory asyncStubFactory() { + return new AsyncStubFactory(); + } + + @Bean + BlockingStubFactory blockingStubFactory() { + return new BlockingStubFactory(); + } + + @Bean + FutureStubFactory futureStubFactory() { + return new FutureStubFactory(); + } + + @ConditionalOnMissingBean + @Bean + GrpcChannelsProperties grpcChannelsProperties() { + return new GrpcChannelsProperties(); + } + + @ConditionalOnMissingBean + @Bean + GlobalClientInterceptorRegistry globalClientInterceptorRegistry(final ApplicationContext applicationContext) { + return new GlobalClientInterceptorRegistry(applicationContext); + } + + @Bean + @Lazy + AnnotationGlobalClientInterceptorConfigurer annotationGlobalClientInterceptorConfigurer( + final ApplicationContext applicationContext) { + return new AnnotationGlobalClientInterceptorConfigurer(applicationContext); + } + + /** + * Creates a new NameResolverRegistration. This ensures that the NameResolverProvider's get unregistered when spring + * shuts down. This is mostly required for tests/when running multiple application contexts within the same JVM. + * + * @param nameResolverProviders The spring managed providers to manage. + * @return The newly created NameResolverRegistration bean. + */ + @ConditionalOnMissingBean + @Lazy + @Bean + NameResolverRegistration grpcNameResolverRegistration( + @Autowired(required = false) final List nameResolverProviders) { + final NameResolverRegistration nameResolverRegistration = new NameResolverRegistration(nameResolverProviders); + nameResolverRegistration.register(NameResolverRegistry.getDefaultRegistry()); + return nameResolverRegistration; + } + + @ConditionalOnBean(CompressorRegistry.class) + @Bean + GrpcChannelConfigurer compressionChannelConfigurer(final CompressorRegistry registry) { + return (builder, name) -> builder.compressorRegistry(registry); + } + + @ConditionalOnBean(DecompressorRegistry.class) + @Bean + GrpcChannelConfigurer decompressionChannelConfigurer(final DecompressorRegistry registry) { + return (builder, name) -> builder.decompressorRegistry(registry); + } + + @ConditionalOnMissingBean(GrpcChannelConfigurer.class) + @Bean + List defaultChannelConfigurers() { + return Collections.emptyList(); + } + + // First try the shaded netty channel factory + @ConditionalOnMissingBean(GrpcChannelFactory.class) + @ConditionalOnClass(name = {"io.grpc.netty.shaded.io.netty.channel.Channel", + "io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder"}) + @Bean + @Lazy + GrpcChannelFactory shadedNettyGrpcChannelFactory( + final GrpcChannelsProperties properties, + final GlobalClientInterceptorRegistry globalClientInterceptorRegistry, + final List channelConfigurers) { + + log.info("Detected grpc-netty-shaded: Creating ShadedNettyChannelFactory + InProcessChannelFactory"); + final ShadedNettyChannelFactory channelFactory = + new ShadedNettyChannelFactory(properties, globalClientInterceptorRegistry, channelConfigurers); + final InProcessChannelFactory inProcessChannelFactory = + new InProcessChannelFactory(properties, globalClientInterceptorRegistry, channelConfigurers); + return new InProcessOrAlternativeChannelFactory(properties, inProcessChannelFactory, channelFactory); + } + + // Then try the normal netty channel factory + @ConditionalOnMissingBean(GrpcChannelFactory.class) + @ConditionalOnClass(name = {"io.netty.channel.Channel", "io.grpc.netty.NettyChannelBuilder"}) + @Bean + @Lazy + GrpcChannelFactory nettyGrpcChannelFactory( + final GrpcChannelsProperties properties, + final GlobalClientInterceptorRegistry globalClientInterceptorRegistry, + final List channelConfigurers) { + + log.info("Detected grpc-netty: Creating NettyChannelFactory + InProcessChannelFactory"); + final NettyChannelFactory channelFactory = + new NettyChannelFactory(properties, globalClientInterceptorRegistry, channelConfigurers); + final InProcessChannelFactory inProcessChannelFactory = + new InProcessChannelFactory(properties, globalClientInterceptorRegistry, channelConfigurers); + return new InProcessOrAlternativeChannelFactory(properties, inProcessChannelFactory, channelFactory); + } + + // Finally try the in process channel factory + @ConditionalOnMissingBean(GrpcChannelFactory.class) + @Bean + @Lazy + GrpcChannelFactory inProcessGrpcChannelFactory( + final GrpcChannelsProperties properties, + final GlobalClientInterceptorRegistry globalClientInterceptorRegistry, + final List channelConfigurers) { + + log.warn("Could not find a GrpcChannelFactory on the classpath: Creating InProcessChannelFactory as fallback"); + return new InProcessChannelFactory(properties, globalClientInterceptorRegistry, channelConfigurers); + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientHealthAutoConfiguration.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientHealthAutoConfiguration.java new file mode 100644 index 000000000..6db4064bd --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientHealthAutoConfiguration.java @@ -0,0 +1,67 @@ +/* + * 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.autoconfigure; + +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +import com.google.common.collect.ImmutableMap; + +import io.grpc.ConnectivityState; +import net.devh.boot.grpc.client.channelfactory.GrpcChannelFactory; + +/** + * Auto configuration class for Spring-Boot. This allows zero config client health status updates for gRPC services. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter(GrpcClientAutoConfiguration.class) +@ConditionalOnClass(name = "org.springframework.boot.actuate.health.HealthIndicator") +public class GrpcClientHealthAutoConfiguration { + + /** + * Creates a HealthIndicator based on the channels' {@link ConnectivityState}s from the underlying + * {@link GrpcChannelFactory}. + * + * @param factory The factory to derive the connectivity states from. + * @return A health indicator bean, that uses the following assumption + * DOWN == states.contains(TRANSIENT_FAILURE). + */ + @Bean + @Lazy + public HealthIndicator grpcChannelHealthIndicator(final GrpcChannelFactory factory) { + return () -> { + final ImmutableMap states = ImmutableMap.copyOf(factory.getConnectivityState()); + final Health.Builder health; + if (states.containsValue(ConnectivityState.TRANSIENT_FAILURE)) { + health = Health.outOfService(); + } else { + health = Health.up(); + } + return health.withDetails(states) + .build(); + }; + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientMetricAutoConfiguration.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientMetricAutoConfiguration.java new file mode 100644 index 000000000..31c94e611 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientMetricAutoConfiguration.java @@ -0,0 +1,55 @@ +/* + * 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.autoconfigure; + +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.grpc.ClientInterceptor; +import io.micrometer.core.instrument.MeterRegistry; +import net.devh.boot.grpc.client.metric.MetricCollectingClientInterceptor; + +/** + * Auto configuration class for Spring-Boot. This allows zero config client metrics for gRPC services. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter(CompositeMeterRegistryAutoConfiguration.class) +@AutoConfigureBefore(GrpcClientAutoConfiguration.class) +@ConditionalOnBean(MeterRegistry.class) +public class GrpcClientMetricAutoConfiguration { + + /** + * Creates a {@link ClientInterceptor} that collects metrics about incoming and outgoing requests and responses. + * + * @param registry The registry used to create the metrics. + * @return The newly created MetricCollectingClientInterceptor bean. + */ + @Bean + @ConditionalOnMissingBean + public MetricCollectingClientInterceptor metricCollectingClientInterceptor(final MeterRegistry registry) { + return new MetricCollectingClientInterceptor(registry); + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientSecurityAutoConfiguration.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientSecurityAutoConfiguration.java new file mode 100644 index 000000000..93a446a7b --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientSecurityAutoConfiguration.java @@ -0,0 +1,69 @@ +/* + * 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.autoconfigure; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.grpc.CallCredentials; +import io.grpc.stub.AbstractStub; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.inject.StubTransformer; +import net.devh.boot.grpc.client.security.CallCredentialsHelper; + +/** + * The security auto configuration for the client. + * + *

+ * You can disable this config by using: + *

+ * + *
+ * @ImportAutoConfiguration(exclude = GrpcClientSecurityAutoConfiguration.class)
+ * 
+ * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@Configuration(proxyBeanMethods = false) +@AutoConfigureBefore(GrpcClientAutoConfiguration.class) +public class GrpcClientSecurityAutoConfiguration { + + /** + * Creates a {@link StubTransformer} bean that will add the call credentials to the created stubs. + * + *

+ * Note: This method will only be applied if exactly one {@link CallCredentials} is in the application + * context. + *

+ * + * @param credentials The call credentials to configure in the stubs. + * @return The StubTransformer bean that will add the given credentials. + * @see AbstractStub#withCallCredentials(CallCredentials) + * @sse {@link CallCredentialsHelper#fixedCredentialsStubTransformer(CallCredentials)} + */ + @ConditionalOnSingleCandidate(CallCredentials.class) + @Bean + StubTransformer stubCallCredentialsTransformer(final CallCredentials credentials) { + log.info("Found single CallCredentials in the context, automatically using it for all stubs"); + return CallCredentialsHelper.fixedCredentialsStubTransformer(credentials); + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientTraceAutoConfiguration.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientTraceAutoConfiguration.java new file mode 100644 index 000000000..d4d6b7768 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcClientTraceAutoConfiguration.java @@ -0,0 +1,56 @@ +/* + * 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.autoconfigure; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; + +import brave.grpc.GrpcTracing; +import io.grpc.ClientInterceptor; +import net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor; +import net.devh.boot.grpc.client.interceptor.OrderedClientInterceptor; +import net.devh.boot.grpc.common.autoconfigure.GrpcCommonTraceAutoConfiguration; +import net.devh.boot.grpc.common.util.InterceptorOrder; + +/** + * The configuration used to configure brave's tracing for grpc. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(value = "spring.sleuth.grpc.enabled", matchIfMissing = true) +@AutoConfigureAfter(GrpcCommonTraceAutoConfiguration.class) +@ConditionalOnBean(GrpcTracing.class) +public class GrpcClientTraceAutoConfiguration { + + /** + * Configures a global client interceptor that applies brave's tracing logic to the requests. + * + * @param grpcTracing The grpc tracing bean. + * @return The tracing client interceptor bean. + */ + @GrpcGlobalClientInterceptor + ClientInterceptor globalTraceClientInterceptorConfigurer(final GrpcTracing grpcTracing) { + return new OrderedClientInterceptor( + grpcTracing.newClientInterceptor(), + InterceptorOrder.ORDER_TRACING_METRICS + 1); + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcDiscoveryClientAutoConfiguration.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcDiscoveryClientAutoConfiguration.java new file mode 100644 index 000000000..b09fb9abc --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/GrpcDiscoveryClientAutoConfiguration.java @@ -0,0 +1,40 @@ +/* + * 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.autoconfigure; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +import net.devh.boot.grpc.client.nameresolver.DiscoveryClientResolverFactory; + +@Configuration(proxyBeanMethods = false) +@ConditionalOnBean(DiscoveryClient.class) +public class GrpcDiscoveryClientAutoConfiguration { + + @ConditionalOnMissingBean + @Lazy // Not needed for InProcessChannelFactories + @Bean + DiscoveryClientResolverFactory grpcDiscoveryClientResolverFactory(final DiscoveryClient client) { + return new DiscoveryClientResolverFactory(client); + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/package-info.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/package-info.java new file mode 100644 index 000000000..0aeb35de0 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/autoconfigure/package-info.java @@ -0,0 +1,5 @@ +/** + * The Spring-Boot auto configuration classes that setup the gRPC client environment. + */ + +package net.devh.boot.grpc.client.autoconfigure; diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java new file mode 100644 index 000000000..ad6c3d04b --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/AbstractChannelFactory.java @@ -0,0 +1,370 @@ +/* + * 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.channelfactory; + +import static java.util.Comparator.comparingLong; +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import javax.annotation.PreDestroy; +import javax.annotation.concurrent.GuardedBy; + +import org.springframework.util.unit.DataSize; + +import com.google.common.collect.Lists; + +import io.grpc.Channel; +import io.grpc.ClientInterceptor; +import io.grpc.ClientInterceptors; +import io.grpc.ConnectivityState; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.config.GrpcChannelProperties; +import net.devh.boot.grpc.client.config.GrpcChannelProperties.Security; +import net.devh.boot.grpc.client.config.GrpcChannelsProperties; +import net.devh.boot.grpc.client.config.NegotiationType; +import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorRegistry; + +/** + * This abstract channel factory contains some shared code for other {@link GrpcChannelFactory}s. This class utilizes + * connection pooling and thus needs to be {@link #close() closed} after usage. + * + * @param The type of builder used by this channel factory. + * + * @author Michael (yidongnan@gmail.com) + * @author Daniel Theuke (daniel.theuke@heuboe.de) + * @since 5/17/16 + */ +@Slf4j +public abstract class AbstractChannelFactory> implements GrpcChannelFactory { + + private final GrpcChannelsProperties properties; + protected final GlobalClientInterceptorRegistry globalClientInterceptorRegistry; + protected final List channelConfigurers; + /** + * According to Thread safety in Grpc java + * clients: {@link ManagedChannel}s should be reused to allow connection reuse. + */ + @GuardedBy("this") + private final Map channels = new ConcurrentHashMap<>(); + private final Map channelStates = new ConcurrentHashMap<>(); + private boolean shutdown = false; + + /** + * Creates a new AbstractChannelFactory with eager initialized references. + * + * @param properties The properties for the channels to create. + * @param globalClientInterceptorRegistry The interceptor registry to use. + * @param channelConfigurers The channel configurers to use. Can be empty. + */ + public AbstractChannelFactory(final GrpcChannelsProperties properties, + final GlobalClientInterceptorRegistry globalClientInterceptorRegistry, + final List channelConfigurers) { + this.properties = requireNonNull(properties, "properties"); + this.globalClientInterceptorRegistry = + requireNonNull(globalClientInterceptorRegistry, "globalClientInterceptorRegistry"); + this.channelConfigurers = requireNonNull(channelConfigurers, "channelConfigurers"); + } + + @Override + public final Channel createChannel(final String name) { + return createChannel(name, Collections.emptyList()); + } + + @Override + public Channel createChannel(final String name, final List customInterceptors, + final boolean sortInterceptors) { + final Channel channel; + synchronized (this) { + if (this.shutdown) { + throw new IllegalStateException("GrpcChannelFactory is already closed!"); + } + channel = this.channels.computeIfAbsent(name, this::newManagedChannel); + } + final List interceptors = + Lists.newArrayList(this.globalClientInterceptorRegistry.getClientInterceptors()); + interceptors.addAll(customInterceptors); + if (sortInterceptors) { + this.globalClientInterceptorRegistry.sortInterceptors(interceptors); + } + return ClientInterceptors.interceptForward(channel, interceptors); + } + + /** + * Creates a new {@link ManagedChannelBuilder} for the given client name. + * + * @param name The name to create the channel builder for. + * @return The newly created channel builder. + */ + protected abstract T newChannelBuilder(String name); + + /** + * Creates a new {@link ManagedChannel} for the given client name. The name will be used to determine the properties + * for the new channel. The calling method is responsible for lifecycle management of the created channel. + * ManagedChannels should be reused if possible to allow connection reuse. + * + * @param name The name to create the channel for. + * @return The newly created channel. + * @see #newChannelBuilder(String) + * @see #configure(ManagedChannelBuilder, String) + */ + protected ManagedChannel newManagedChannel(final String name) { + final T builder = newChannelBuilder(name); + configure(builder, name); + final ManagedChannel channel = builder.build(); + final Duration timeout = this.properties.getChannel(name).getImmediateConnectTimeout(); + if (!timeout.isZero()) { + connectOnStartup(name, channel, timeout); + } + watchConnectivityState(name, channel); + return channel; + } + + /** + * Gets the channel properties for the given client name. + * + * @param name The client name to use. + * @return The properties for the given client name. + */ + protected final GrpcChannelProperties getPropertiesFor(final String name) { + return this.properties.getChannel(name); + } + + /** + * Configures the given channel builder. This method can be overwritten to add features that are not yet supported + * by this library. + * + * @param builder The channel builder to configure. + * @param name The name of the client to configure. + */ + protected void configure(final T builder, final String name) { + configureKeepAlive(builder, name); + configureSecurity(builder, name); + configureLimits(builder, name); + configureCompression(builder, name); + for (final GrpcChannelConfigurer channelConfigurer : this.channelConfigurers) { + channelConfigurer.accept(builder, name); + } + } + + /** + * Configures the keep alive options that should be used by the channel. + * + * @param builder The channel builder to configure. + * @param name The name of the client to configure. + */ + protected void configureKeepAlive(final T builder, final String name) { + final GrpcChannelProperties properties = getPropertiesFor(name); + if (properties.isEnableKeepAlive()) { + builder.keepAliveTime(properties.getKeepAliveTime().toNanos(), TimeUnit.NANOSECONDS) + .keepAliveTimeout(properties.getKeepAliveTimeout().toNanos(), TimeUnit.NANOSECONDS) + .keepAliveWithoutCalls(properties.isKeepAliveWithoutCalls()); + } + } + + /** + * Configures the security options that should be used by the channel. + * + * @param builder The channel builder to configure. + * @param name The name of the client to configure. + */ + protected void configureSecurity(final T builder, final String name) { + final GrpcChannelProperties properties = getPropertiesFor(name); + final Security security = properties.getSecurity(); + + if (properties.getNegotiationType() != NegotiationType.TLS // non-default + || isNonNullAndNonBlank(security.getAuthorityOverride()) + || security.getCertificateChain() != null + || security.getPrivateKey() != null + || security.getTrustCertCollection() != null) { + throw new IllegalStateException( + "Security is configured but this implementation does not support security!"); + } + } + + /** + * Checks whether the given value is non null and non blank. + * + * @param value The value to check. + * @return True, if the given value was neither null nor blank. False otherwise. + */ + protected boolean isNonNullAndNonBlank(final String value) { + return value != null && !value.trim().isEmpty(); + } + + /** + * Configures limits such as max message sizes that should be used by the channel. + * + * @param builder The channel builder to configure. + * @param name The name of the client to configure. + */ + protected void configureLimits(final T builder, final String name) { + final GrpcChannelProperties properties = getPropertiesFor(name); + final DataSize maxInboundMessageSize = properties.getMaxInboundMessageSize(); + if (maxInboundMessageSize != null) { + builder.maxInboundMessageSize((int) maxInboundMessageSize.toBytes()); + } + } + + /** + * Configures the compression options that should be used by the channel. + * + * @param builder The channel builder to configure. + * @param name The name of the client to configure. + */ + protected void configureCompression(final T builder, final String name) { + final GrpcChannelProperties properties = getPropertiesFor(name); + if (properties.isFullStreamDecompression()) { + builder.enableFullStreamDecompression(); + } + } + + @Override + public Map getConnectivityState() { + return Collections.unmodifiableMap(this.channelStates); + } + + /** + * Watch the given channel for connectivity changes. + * + * @param name The name of the channel in the state overview. + * @param channel The channel to watch the state of. + */ + protected void watchConnectivityState(final String name, final ManagedChannel channel) { + final ConnectivityState state = channel.getState(false); + this.channelStates.put(name, state); + if (state != ConnectivityState.SHUTDOWN) { + channel.notifyWhenStateChanged(state, () -> watchConnectivityState(name, channel)); + } + } + + private void connectOnStartup(final String name, final ManagedChannel channel, final Duration timeout) { + log.debug("Initiating connection to channel {}", name); + channel.getState(true); + + final CountDownLatch readyLatch = new CountDownLatch(1); + waitForReady(channel, readyLatch); + boolean connected; + try { + log.debug("Waiting for connection to channel {}", name); + connected = !readyLatch.await(timeout.toMillis(), TimeUnit.MILLISECONDS); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + connected = false; + } + if (connected) { + throw new IllegalStateException("Can't connect to channel " + name); + } + log.info("Successfully connected to channel {}", name); + } + + private void waitForReady(final ManagedChannel channel, final CountDownLatch readySignal) { + final ConnectivityState state = channel.getState(false); + log.debug("Waiting for ready state. Currently in {}", state); + if (state == ConnectivityState.READY) { + readySignal.countDown(); + } else { + channel.notifyWhenStateChanged(state, () -> waitForReady(channel, readySignal)); + } + } + + /** + * Closes this channel factory and the channels created by this instance. The shutdown happens in two phases, first + * an orderly shutdown is initiated on all channels and then the method waits for all channels to terminate. If the + * channels don't have terminated after 60 seconds then they will be forcefully shutdown. + */ + @Override + @PreDestroy + public synchronized void close() { + if (this.shutdown) { + return; + } + this.shutdown = true; + final List shutdownEntries = new ArrayList<>(); + for (final Entry entry : this.channels.entrySet()) { + final ManagedChannel channel = entry.getValue(); + channel.shutdown(); + final long gracePeriod = this.properties.getChannel(entry.getKey()).getShutdownGracePeriod().toMillis(); + shutdownEntries.add(new ShutdownRecord(entry.getKey(), channel, gracePeriod)); + } + try { + final long start = System.currentTimeMillis(); + shutdownEntries.sort(comparingLong(ShutdownRecord::getGracePeriod)); + + for (final ShutdownRecord entry : shutdownEntries) { + if (!entry.channel.isTerminated()) { + log.debug("Awaiting channel termination: {}", entry.name); + + final long waitedTime = System.currentTimeMillis() - start; + final long waitTime = entry.gracePeriod - waitedTime; + + if (waitTime > 0) { + entry.channel.awaitTermination(waitTime, MILLISECONDS); + } + entry.channel.shutdownNow(); + } + log.debug("Completed channel termination: {}", entry.name); + } + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + log.debug("We got interrupted - Speeding up shutdown process"); + } finally { + for (final ManagedChannel channel : this.channels.values()) { + if (!channel.isTerminated()) { + log.debug("Channel not terminated yet - force shutdown now: {} ", channel); + channel.shutdownNow(); + } + } + } + final int channelCount = this.channels.size(); + this.channels.clear(); + this.channelStates.clear(); + log.debug("GrpcChannelFactory closed (including {} channels)", channelCount); + } + + private static class ShutdownRecord { + + private final String name; + private final ManagedChannel channel; + private final long gracePeriod; + + public ShutdownRecord(final String name, final ManagedChannel channel, final long gracePeriod) { + this.name = name; + this.channel = channel; + // gracePeriod < 0 => Infinite + this.gracePeriod = gracePeriod < 0 ? Long.MAX_VALUE : gracePeriod; + } + + long getGracePeriod() { + return this.gracePeriod; + } + + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/GrpcChannelConfigurer.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/GrpcChannelConfigurer.java new file mode 100644 index 000000000..b5b338ac9 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/GrpcChannelConfigurer.java @@ -0,0 +1,44 @@ +/* + * 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.channelfactory; + +import static java.util.Objects.requireNonNull; + +import java.util.function.BiConsumer; + +import io.grpc.ManagedChannelBuilder; + +/** + * A configurer for {@link ManagedChannelBuilder}s which can be used by {@link GrpcChannelFactory} to customize the + * created channels. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@FunctionalInterface +public interface GrpcChannelConfigurer extends BiConsumer, String> { + + @Override + default GrpcChannelConfigurer andThen(final BiConsumer, ? super String> after) { + requireNonNull(after, "after"); + return (l, r) -> { + accept(l, r); + after.accept(l, r); + }; + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/GrpcChannelFactory.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/GrpcChannelFactory.java new file mode 100644 index 000000000..57d69b913 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/GrpcChannelFactory.java @@ -0,0 +1,108 @@ +/* + * 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.channelfactory; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import io.grpc.Channel; +import io.grpc.ClientInterceptor; +import io.grpc.ClientInterceptors; +import io.grpc.ConnectivityState; +import io.grpc.ManagedChannel; + +/** + * This factory creates grpc {@link Channel}s for a given service name. Implementations are encouraged to utilize + * connection pooling and thus {@link #close() close} should be called before disposing it. + * + * @author Michael (yidongnan@gmail.com) + * @since 5/17/16 + */ +public interface GrpcChannelFactory extends AutoCloseable { + + /** + * Creates a new channel for the given service name. The returned channel will use all globally registered + * {@link ClientInterceptor}s. + * + *

+ * Note: The underlying implementation might reuse existing {@link ManagedChannel}s allow connection reuse. + *

+ * + * @param name The name of the service. + * @return The newly created channel for the given service. + */ + default Channel createChannel(final String name) { + return createChannel(name, Collections.emptyList()); + } + + /** + * Creates a new channel for the given service name. The returned channel will use all globally registered + * {@link ClientInterceptor}s. + * + *

+ * Note: The underlying implementation might reuse existing {@link ManagedChannel}s allow connection reuse. + *

+ * + *

+ * Note: The given interceptors will be appended to the global interceptors and applied using + * {@link ClientInterceptors#interceptForward(Channel, ClientInterceptor...)}. + *

+ * + * @param name The name of the service. + * @param interceptors A list of additional client interceptors that should be added to the channel. + * @return The newly created channel for the given service. + */ + default Channel createChannel(final String name, final List interceptors) { + return createChannel(name, interceptors, false); + } + + /** + * Creates a new channel for the given service name. The returned channel will use all globally registered + * {@link ClientInterceptor}s. + * + *

+ * Note: The underlying implementation might reuse existing {@link ManagedChannel}s allow connection reuse. + *

+ * + *

+ * Note: The given interceptors will be appended to the global interceptors and applied using + * {@link ClientInterceptors#interceptForward(Channel, ClientInterceptor...)}. + *

+ * + * @param name The name of the service. + * @param interceptors A list of additional client interceptors that should be added to the channel. + * @param sortInterceptors Whether the interceptors (both global and custom) should be sorted before being applied. + * @return The newly created channel for the given service. + */ + Channel createChannel(String name, List interceptors, boolean sortInterceptors); + + /** + * Gets an unmodifiable map that contains the names of the created channel with their current + * {@link ConnectivityState}. This method will return an empty map, if the feature is not supported. + * + * @return A map with the channel names and their connectivity state. + */ + default Map getConnectivityState() { + return Collections.emptyMap(); + } + + @Override + void close(); + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/InProcessChannelFactory.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/InProcessChannelFactory.java new file mode 100644 index 000000000..2e5b992fc --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/InProcessChannelFactory.java @@ -0,0 +1,76 @@ +/* + * 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.channelfactory; + +import java.util.Collections; +import java.util.List; + +import io.grpc.inprocess.InProcessChannelBuilder; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.config.GrpcChannelsProperties; +import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorRegistry; + +/** + * This channel factory creates and manages in-process {@link GrpcChannelFactory}s. + * + *

+ * This class utilizes connection pooling and thus needs to be {@link #close() closed} after usage. + *

+ * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +public class InProcessChannelFactory extends AbstractChannelFactory { + + /** + * Creates a new InProcessChannelFactory with the given properties. + * + * @param properties The properties for the channels to create. + * @param globalClientInterceptorRegistry The interceptor registry to use. + */ + public InProcessChannelFactory(final GrpcChannelsProperties properties, + final GlobalClientInterceptorRegistry globalClientInterceptorRegistry) { + this(properties, globalClientInterceptorRegistry, Collections.emptyList()); + } + + /** + * Creates a new InProcessChannelFactory with the given properties. + * + * @param properties The properties for the channels to create. + * @param globalClientInterceptorRegistry The interceptor registry to use. + * @param channelConfigurers The channel configurers to use. Can be empty. + */ + public InProcessChannelFactory(final GrpcChannelsProperties properties, + final GlobalClientInterceptorRegistry globalClientInterceptorRegistry, + final List channelConfigurers) { + super(properties, globalClientInterceptorRegistry, channelConfigurers); + } + + @Override + protected InProcessChannelBuilder newChannelBuilder(final String name) { + log.debug("Creating new channel: {}", name); + return InProcessChannelBuilder.forName(name); + } + + @Override + protected void configureSecurity(final InProcessChannelBuilder builder, final String name) { + // No need to configure security as we are in process only. + // There is also no need to throw exceptions if transport security is configured. + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/InProcessOrAlternativeChannelFactory.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/InProcessOrAlternativeChannelFactory.java new file mode 100644 index 000000000..f809f491a --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/InProcessOrAlternativeChannelFactory.java @@ -0,0 +1,103 @@ +/* + * 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.channelfactory; + +import static java.util.Objects.requireNonNull; + +import java.net.URI; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableMap; + +import io.grpc.Channel; +import io.grpc.ClientInterceptor; +import io.grpc.ConnectivityState; +import net.devh.boot.grpc.client.config.GrpcChannelsProperties; + +/** + * This channel factory is a switch between the {@link InProcessChannelFactory} and an alternative implementation. All + * channels that are configured with the {@code in-process} scheme will be handled by the in-process-channel-factory, + * the other channels will be handled by the alternative implementation. + * + *

+ * The following examples show how the configured address will be mapped to an actual channel: + *

+ * + *
    + *
  • in-process:foobar -> will use the foobar in-process-channel.
  • + *
  • in-process:foo/bar -> will use the foo/bar in-process-channel.
  • + *
  • static://127.0.0.1 -> will be handled by the alternative grpc channel factory.
  • + *
+ * + *

+ * Using this class does not incur any additional performance or resource costs, as the actual channels (in-process or + * other) are only created on demand. + *

+ */ +public class InProcessOrAlternativeChannelFactory implements GrpcChannelFactory { + + private static final String IN_PROCESS_SCHEME = "in-process"; + + private final GrpcChannelsProperties properties; + private final InProcessChannelFactory inProcessChannelFactory; + private final GrpcChannelFactory alternativeChannelFactory; + + /** + * Creates a new InProcessOrAlternativeChannelFactory with the given properties and channel factories. + * + * @param properties The properties used to resolved the target scheme + * @param inProcessChannelFactory The in process channel factory implementation to use. + * @param alternativeChannelFactory The alternative channel factory implementation to use. + */ + public InProcessOrAlternativeChannelFactory(final GrpcChannelsProperties properties, + final InProcessChannelFactory inProcessChannelFactory, final GrpcChannelFactory alternativeChannelFactory) { + this.properties = requireNonNull(properties, "properties"); + this.inProcessChannelFactory = requireNonNull(inProcessChannelFactory, "inProcessChannelFactory"); + this.alternativeChannelFactory = requireNonNull(alternativeChannelFactory, "alternativeChannelFactory"); + } + + @Override + public Channel createChannel(final String name, final List interceptors, + boolean sortInterceptors) { + final URI address = this.properties.getChannel(name).getAddress(); + if (address != null && IN_PROCESS_SCHEME.equals(address.getScheme())) { + return this.inProcessChannelFactory.createChannel(address.getSchemeSpecificPart(), interceptors, + sortInterceptors); + } + return this.alternativeChannelFactory.createChannel(name, interceptors, sortInterceptors); + } + + @Override + public Map getConnectivityState() { + return ImmutableMap.builder() + .putAll(inProcessChannelFactory.getConnectivityState()) + .putAll(alternativeChannelFactory.getConnectivityState()) + .build(); + } + + @Override + public void close() { + try { + this.inProcessChannelFactory.close(); + } finally { + this.alternativeChannelFactory.close(); + } + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/NettyChannelFactory.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/NettyChannelFactory.java new file mode 100644 index 000000000..c8a14f7b6 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/NettyChannelFactory.java @@ -0,0 +1,164 @@ +/* + * 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.channelfactory; + +import static java.util.Objects.requireNonNull; +import static net.devh.boot.grpc.common.util.GrpcUtils.DOMAIN_SOCKET_ADDRESS_SCHEME; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.List; + +import javax.net.ssl.SSLException; + +import org.springframework.core.io.Resource; + +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.NettyChannelBuilder; +import io.netty.channel.epoll.EpollDomainSocketChannel; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.unix.DomainSocketAddress; +import io.netty.handler.ssl.SslContextBuilder; +import net.devh.boot.grpc.client.config.GrpcChannelProperties; +import net.devh.boot.grpc.client.config.GrpcChannelProperties.Security; +import net.devh.boot.grpc.client.config.GrpcChannelsProperties; +import net.devh.boot.grpc.client.config.NegotiationType; +import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorRegistry; +import net.devh.boot.grpc.common.util.GrpcUtils; + +/** + * This channel factory creates and manages netty based {@link GrpcChannelFactory}s. + * + *

+ * This class utilizes connection pooling and thus needs to be {@link #close() closed} after usage. + *

+ * + * @author Michael (yidongnan@gmail.com) + * @author Daniel Theuke (daniel.theuke@heuboe.de) + * @since 5/17/16 + */ +// Keep this file in sync with ShadedNettyChannelFactory +public class NettyChannelFactory extends AbstractChannelFactory { + + /** + * Creates a new GrpcChannelFactory for netty with the given options. + * + * @param properties The properties for the channels to create. + * @param globalClientInterceptorRegistry The interceptor registry to use. + * @param channelConfigurers The channel configurers to use. Can be empty. + */ + public NettyChannelFactory(final GrpcChannelsProperties properties, + final GlobalClientInterceptorRegistry globalClientInterceptorRegistry, + final List channelConfigurers) { + super(properties, globalClientInterceptorRegistry, channelConfigurers); + } + + @Override + protected NettyChannelBuilder newChannelBuilder(final String name) { + final GrpcChannelProperties properties = getPropertiesFor(name); + URI address = properties.getAddress(); + if (address == null) { + address = URI.create(name); + } + if (DOMAIN_SOCKET_ADDRESS_SCHEME.equals(address.getScheme())) { + final String path = GrpcUtils.extractDomainSocketAddressPath(address.toString()); + return NettyChannelBuilder.forAddress(new DomainSocketAddress(path)) + .channelType(EpollDomainSocketChannel.class) + .eventLoopGroup(new EpollEventLoopGroup()); + } else { + return NettyChannelBuilder.forTarget(address.toString()) + .defaultLoadBalancingPolicy(properties.getDefaultLoadBalancingPolicy()); + } + } + + @Override + protected void configureSecurity(final NettyChannelBuilder builder, final String name) { + final GrpcChannelProperties properties = getPropertiesFor(name); + + final NegotiationType negotiationType = properties.getNegotiationType(); + builder.negotiationType(of(negotiationType)); + + if (negotiationType == NegotiationType.TLS) { + final Security security = properties.getSecurity(); + + final String authorityOverwrite = security.getAuthorityOverride(); + if (authorityOverwrite != null && !authorityOverwrite.isEmpty()) { + builder.overrideAuthority(authorityOverwrite); + } + + final SslContextBuilder sslContextBuilder = GrpcSslContexts.forClient(); + + if (security.isClientAuthEnabled()) { + final Resource certificateChain = + requireNonNull(security.getCertificateChain(), "certificateChain not configured"); + final Resource privateKey = requireNonNull(security.getPrivateKey(), "privateKey not configured"); + try (InputStream certificateChainStream = certificateChain.getInputStream(); + InputStream privateKeyStream = privateKey.getInputStream()) { + sslContextBuilder.keyManager(certificateChainStream, privateKeyStream, + security.getPrivateKeyPassword()); + } catch (IOException | RuntimeException e) { + throw new IllegalArgumentException("Failed to create SSLContext (PK/Cert)", e); + } + } + + final Resource trustCertCollection = security.getTrustCertCollection(); + if (trustCertCollection != null) { + try (InputStream trustCertCollectionStream = trustCertCollection.getInputStream()) { + sslContextBuilder.trustManager(trustCertCollectionStream); + } catch (IOException | RuntimeException e) { + throw new IllegalArgumentException("Failed to create SSLContext (TrustStore)", e); + } + } + + if (security.getCiphers() != null && !security.getCiphers().isEmpty()) { + sslContextBuilder.ciphers(security.getCiphers()); + } + + if (security.getProtocols() != null && security.getProtocols().length > 0) { + sslContextBuilder.protocols(security.getProtocols()); + } + + try { + builder.sslContext(sslContextBuilder.build()); + } catch (final SSLException e) { + throw new IllegalStateException("Failed to create ssl context for grpc client", e); + } + } + } + + /** + * Converts the given negotiation type to netty's negotiation type. + * + * @param negotiationType The negotiation type to convert. + * @return The converted negotiation type. + */ + protected static io.grpc.netty.NegotiationType of(final NegotiationType negotiationType) { + switch (negotiationType) { + case PLAINTEXT: + return io.grpc.netty.NegotiationType.PLAINTEXT; + case PLAINTEXT_UPGRADE: + return io.grpc.netty.NegotiationType.PLAINTEXT_UPGRADE; + case TLS: + return io.grpc.netty.NegotiationType.TLS; + default: + throw new IllegalArgumentException("Unsupported NegotiationType: " + negotiationType); + } + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/ShadedNettyChannelFactory.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/ShadedNettyChannelFactory.java new file mode 100644 index 000000000..a1e3ba269 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/ShadedNettyChannelFactory.java @@ -0,0 +1,162 @@ +/* + * 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.channelfactory; + +import static java.util.Objects.requireNonNull; +import static net.devh.boot.grpc.common.util.GrpcUtils.DOMAIN_SOCKET_ADDRESS_SCHEME; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.util.List; + +import javax.net.ssl.SSLException; + +import org.springframework.core.io.Resource; + +import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import io.grpc.netty.shaded.io.netty.channel.epoll.EpollDomainSocketChannel; +import io.grpc.netty.shaded.io.netty.channel.epoll.EpollEventLoopGroup; +import io.grpc.netty.shaded.io.netty.channel.unix.DomainSocketAddress; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; +import net.devh.boot.grpc.client.config.GrpcChannelProperties; +import net.devh.boot.grpc.client.config.GrpcChannelProperties.Security; +import net.devh.boot.grpc.client.config.GrpcChannelsProperties; +import net.devh.boot.grpc.client.config.NegotiationType; +import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorRegistry; +import net.devh.boot.grpc.common.util.GrpcUtils; + +/** + * This channel factory creates and manages shaded netty based {@link GrpcChannelFactory}s. + * + *

+ * This class utilizes connection pooling and thus needs to be {@link #close() closed} after usage. + *

+ * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +// Keep this file in sync with NettyChannelFactory +public class ShadedNettyChannelFactory extends AbstractChannelFactory { + + /** + * Creates a new GrpcChannelFactory for shaded netty with the given options. + * + * @param properties The properties for the channels to create. + * @param globalClientInterceptorRegistry The interceptor registry to use. + * @param channelConfigurers The channel configurers to use. Can be empty. + */ + public ShadedNettyChannelFactory(final GrpcChannelsProperties properties, + final GlobalClientInterceptorRegistry globalClientInterceptorRegistry, + final List channelConfigurers) { + super(properties, globalClientInterceptorRegistry, channelConfigurers); + } + + @Override + protected NettyChannelBuilder newChannelBuilder(final String name) { + final GrpcChannelProperties properties = getPropertiesFor(name); + URI address = properties.getAddress(); + if (address == null) { + address = URI.create(name); + } + if (DOMAIN_SOCKET_ADDRESS_SCHEME.equals(address.getScheme())) { + final String path = GrpcUtils.extractDomainSocketAddressPath(address.toString()); + return NettyChannelBuilder.forAddress(new DomainSocketAddress(path)) + .channelType(EpollDomainSocketChannel.class) + .eventLoopGroup(new EpollEventLoopGroup()); + } else { + return NettyChannelBuilder.forTarget(address.toString()) + .defaultLoadBalancingPolicy(properties.getDefaultLoadBalancingPolicy()); + } + } + + @Override + protected void configureSecurity(final NettyChannelBuilder builder, final String name) { + final GrpcChannelProperties properties = getPropertiesFor(name); + + final NegotiationType negotiationType = properties.getNegotiationType(); + builder.negotiationType(of(negotiationType)); + + if (negotiationType == NegotiationType.TLS) { + final Security security = properties.getSecurity(); + + final String authorityOverwrite = security.getAuthorityOverride(); + if (authorityOverwrite != null && !authorityOverwrite.isEmpty()) { + builder.overrideAuthority(authorityOverwrite); + } + + final SslContextBuilder sslContextBuilder = GrpcSslContexts.forClient(); + + if (security.isClientAuthEnabled()) { + final Resource certificateChain = + requireNonNull(security.getCertificateChain(), "certificateChain not configured"); + final Resource privateKey = requireNonNull(security.getPrivateKey(), "privateKey not configured"); + try (InputStream certificateChainStream = certificateChain.getInputStream(); + InputStream privateKeyStream = privateKey.getInputStream()) { + sslContextBuilder.keyManager(certificateChainStream, privateKeyStream, + security.getPrivateKeyPassword()); + } catch (IOException | RuntimeException e) { + throw new IllegalArgumentException("Failed to create SSLContext (PK/Cert)", e); + } + } + + final Resource trustCertCollection = security.getTrustCertCollection(); + if (trustCertCollection != null) { + try (InputStream trustCertCollectionStream = trustCertCollection.getInputStream()) { + sslContextBuilder.trustManager(trustCertCollectionStream); + } catch (IOException | RuntimeException e) { + throw new IllegalArgumentException("Failed to create SSLContext (TrustStore)", e); + } + } + + if (security.getCiphers() != null && !security.getCiphers().isEmpty()) { + sslContextBuilder.ciphers(security.getCiphers()); + } + + if (security.getProtocols() != null && security.getProtocols().length > 0) { + sslContextBuilder.protocols(security.getProtocols()); + } + + try { + builder.sslContext(sslContextBuilder.build()); + } catch (final SSLException e) { + throw new IllegalStateException("Failed to create ssl context for grpc client", e); + } + } + } + + /** + * Converts the given negotiation type to netty's negotiation type. + * + * @param negotiationType The negotiation type to convert. + * @return The converted negotiation type. + */ + protected static io.grpc.netty.shaded.io.grpc.netty.NegotiationType of(final NegotiationType negotiationType) { + switch (negotiationType) { + case PLAINTEXT: + return io.grpc.netty.shaded.io.grpc.netty.NegotiationType.PLAINTEXT; + case PLAINTEXT_UPGRADE: + return io.grpc.netty.shaded.io.grpc.netty.NegotiationType.PLAINTEXT_UPGRADE; + case TLS: + return io.grpc.netty.shaded.io.grpc.netty.NegotiationType.TLS; + default: + throw new IllegalArgumentException("Unsupported NegotiationType: " + negotiationType); + } + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/package-info.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/package-info.java new file mode 100644 index 000000000..0f1ab8492 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/channelfactory/package-info.java @@ -0,0 +1,5 @@ +/** + * Contains factories and related classes to setup the client's connection channels to the servers. + */ + +package net.devh.boot.grpc.client.channelfactory; diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java new file mode 100644 index 000000000..42cdb6ecb --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelProperties.java @@ -0,0 +1,742 @@ +/* + * 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.config; + +import java.io.File; +import java.io.InputStream; +import java.net.URI; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.springframework.boot.convert.DataSizeUnit; +import org.springframework.boot.convert.DurationUnit; +import org.springframework.core.io.Resource; +import org.springframework.util.unit.DataSize; +import org.springframework.util.unit.DataUnit; + +import io.grpc.LoadBalancerRegistry; +import io.grpc.ManagedChannelBuilder; +import io.grpc.NameResolverProvider; +import io.grpc.internal.GrpcUtil; +import io.grpc.netty.shaded.io.grpc.netty.NettyChannelBuilder; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * The channel properties for a single named gRPC channel or service reference. + * + * @author Michael (yidongnan@gmail.com) + * @author Daniel Theuke (daniel.theuke@heuboe.de) + * @since 5/17/16 + */ +@ToString +@EqualsAndHashCode +public class GrpcChannelProperties { + + // -------------------------------------------------- + // Target Address + // -------------------------------------------------- + + private URI address = null; + + /** + * Gets the target address uri. + * + * @return The address to connect to or null + * @see #setAddress(String) + */ + public URI getAddress() { + return this.address; + } + + /** + * Set the address uri for the channel. If nothing is configured then the name of the client will be used along with + * the default scheme. We recommend explicitly configuring the scheme used for the address resolutions such as + * {@code dns:/}. + * + * @param address The address to use for the channel or null to default to the fallback. + * + * @see #setAddress(String) + */ + public void setAddress(final URI address) { + this.address = address; + } + + /** + * Sets the target address uri for the channel. The target uri must be in the format: + * {@code schema:[//[authority]][/path]}. If nothing is configured then the name of the client will be used along + * with the default scheme. We recommend explicitly configuring the scheme used for the address resolutions such as + * {@code dns:/}. + * + *

+ * Examples + *

+ * + *
    + *
  • {@code static://localhost:9090} (refers to exactly one IPv4 or IPv6 address, dependent on the jre + * configuration, it does not check whether there is actually someone listening on that network interface)
  • + *
  • {@code static://10.0.0.10}
  • + *
  • {@code static://10.0.0.10,10.11.12.11}
  • + *
  • {@code static://10.0.0.10:9090,10.0.0.11:80,10.0.0.12:1234,[::1]:8080}
  • + *
  • {@code dns:/localhost (might refer to the IPv4 or the IPv6 address or both, dependent on the system + * configuration, it does not check whether there is actually someone listening on that network interface)}
  • + *
  • {@code dns:/example.com}
  • + *
  • {@code dns:/example.com:9090}
  • + *
  • {@code dns:///example.com:9090}
  • + *
  • {@code discovery:/foo-service}
  • + *
  • {@code discovery:///foo-service}
  • + *
  • {@code unix:} (Additional dependencies may be required)
  • + *
  • {@code unix://} (Additional dependencies may be required)
  • + *
+ * + * @param address The string representation of an uri to use as target address or null to use a fallback. + * + * @see gRPC Name Resolution + * @see NameResolverProvider + */ + public void setAddress(final String address) { + this.address = address == null ? null : URI.create(address); + } + + // -------------------------------------------------- + // defaultLoadBalancingPolicy + // -------------------------------------------------- + + private String defaultLoadBalancingPolicy; + private static final String DEFAULT_DEFAULT_LOAD_BALANCING_POLICY = "round_robin"; + + /** + * Gets the default load balancing policy this channel should use. + * + * @return The default load balancing policy. + * @see ManagedChannelBuilder#defaultLoadBalancingPolicy(String) + */ + public String getDefaultLoadBalancingPolicy() { + return this.defaultLoadBalancingPolicy == null ? DEFAULT_DEFAULT_LOAD_BALANCING_POLICY + : this.defaultLoadBalancingPolicy; + } + + /** + * Sets the default load balancing policy for this channel. This config might be overwritten by the service config + * received from the target address. The names have to be resolvable from the {@link LoadBalancerRegistry}. By + * default this the {@code round_robin} policy. Please note that this policy is different from the normal grpc-java + * default policy {@code pick_first}. + * + * @param defaultLoadBalancingPolicy The default load balancing policy to use or null to use the fallback. + */ + public void setDefaultLoadBalancingPolicy(final String defaultLoadBalancingPolicy) { + this.defaultLoadBalancingPolicy = defaultLoadBalancingPolicy; + } + + // -------------------------------------------------- + // KeepAlive + // -------------------------------------------------- + + private Boolean enableKeepAlive; + private static final boolean DEFAULT_ENABLE_KEEP_ALIVE = false; + + /** + * Gets whether keepAlive is enabled. + * + * @return True, if keep alive should be enabled. False otherwise. + * + * @see #setEnableKeepAlive(Boolean) + */ + public boolean isEnableKeepAlive() { + return this.enableKeepAlive == null ? DEFAULT_ENABLE_KEEP_ALIVE : this.enableKeepAlive; + } + + /** + * Sets whether keepAlive should be enabled. Defaults to false. + * + * @param enableKeepAlive True, to enable. False, to disable. Null, to use the fallback. + */ + public void setEnableKeepAlive(final Boolean enableKeepAlive) { + this.enableKeepAlive = enableKeepAlive; + } + + // -------------------------------------------------- + + @DurationUnit(ChronoUnit.SECONDS) + private Duration keepAliveTime; + private static final Duration DEFAULT_KEEP_ALIVE_TIME = Duration.of(5, ChronoUnit.MINUTES); + + /** + * Gets the default delay before we send a keepAlive. + * + * @return The default delay before sending keepAlives. + * + * @see #setKeepAliveTime(Duration) + */ + public Duration getKeepAliveTime() { + return this.keepAliveTime == null ? DEFAULT_KEEP_ALIVE_TIME : this.keepAliveTime; + } + + /** + * The default delay before we send a keepAlives. Defaults to {@code 5min}. Default unit {@link ChronoUnit#SECONDS + * SECONDS}. Please note that shorter intervals increase the network burden for the server. Cannot be lower than + * permitKeepAliveTime on server (default 5min). + * + * @param keepAliveTime The new default delay before sending keepAlives, or null to use the fallback. + * + * @see #setEnableKeepAlive(Boolean) + * @see NettyChannelBuilder#keepAliveTime(long, TimeUnit) + */ + public void setKeepAliveTime(final Duration keepAliveTime) { + this.keepAliveTime = keepAliveTime; + } + + // -------------------------------------------------- + + @DurationUnit(ChronoUnit.SECONDS) + private Duration keepAliveTimeout; + private static final Duration DEFAULT_KEEP_ALIVE_TIMEOUT = Duration.of(20, ChronoUnit.SECONDS); + + /** + * The default timeout for a keepAlives ping request. + * + * @return The default timeout for a keepAlives ping request. + * + * @see #setKeepAliveTimeout(Duration) + */ + public Duration getKeepAliveTimeout() { + return this.keepAliveTimeout == null ? DEFAULT_KEEP_ALIVE_TIMEOUT : this.keepAliveTimeout; + } + + /** + * The default timeout for a keepAlives ping request. Defaults to {@code 20s}. Default unit + * {@link ChronoUnit#SECONDS SECONDS}. + * + * @param keepAliveTimeout The default timeout for a keepAlives ping request. + * + * @see #setEnableKeepAlive(Boolean) + * @see NettyChannelBuilder#keepAliveTimeout(long, TimeUnit) + */ + public void setKeepAliveTimeout(final Duration keepAliveTimeout) { + this.keepAliveTimeout = keepAliveTimeout; + } + + // -------------------------------------------------- + + private Boolean keepAliveWithoutCalls; + private static final boolean DEFAULT_KEEP_ALIVE_WITHOUT_CALLS = false; + + /** + * Gets whether keepAlive will be performed when there are no outstanding RPC on a connection. + * + * @return True, if keepAlives should be performed even when there are no RPCs. False otherwise. + * + * @see #setKeepAliveWithoutCalls(Boolean) + */ + public boolean isKeepAliveWithoutCalls() { + return this.keepAliveWithoutCalls == null ? DEFAULT_KEEP_ALIVE_WITHOUT_CALLS : this.keepAliveWithoutCalls; + } + + /** + * Sets whether keepAlive will be performed when there are no outstanding RPC on a connection. Defaults to + * {@code false}. + * + * @param keepAliveWithoutCalls whether keepAlive will be performed when there are no outstanding RPC on a + * connection. + * + * @see #setEnableKeepAlive(Boolean) + * @see NettyChannelBuilder#keepAliveWithoutCalls(boolean) + */ + public void setKeepAliveWithoutCalls(final Boolean keepAliveWithoutCalls) { + this.keepAliveWithoutCalls = keepAliveWithoutCalls; + } + + // -------------------------------------------------- + + @DurationUnit(ChronoUnit.SECONDS) + private Duration shutdownGracePeriod; + private static final Duration DEFAULT_SHUTDOWN_GRACE_PERIOD = Duration.ofSeconds(30); + + /** + * Gets the time to wait for the channel to gracefully shutdown. If set to a negative value, the channel waits + * forever. If set to {@code 0} the channel will force shutdown immediately. Defaults to {@code 30s}. + * + * @return The time to wait for a graceful shutdown. + */ + public Duration getShutdownGracePeriod() { + return this.shutdownGracePeriod == null ? DEFAULT_SHUTDOWN_GRACE_PERIOD : this.shutdownGracePeriod; + } + + /** + * Sets the time to wait for the channel to gracefully shutdown (completing all requests). If set to a negative + * value, the channel waits forever. If set to {@code 0} the channel will force shutdown immediately. Defaults to + * {@code 30s}. + * + * @param shutdownGracePeriod The time to wait for a graceful shutdown. + */ + public void setShutdownGracePeriod(final Duration shutdownGracePeriod) { + this.shutdownGracePeriod = shutdownGracePeriod; + } + + // -------------------------------------------------- + // Message Transfer + // -------------------------------------------------- + + @DataSizeUnit(DataUnit.BYTES) + private DataSize maxInboundMessageSize = null; + + /** + * Gets the maximum message size allowed to be received by the channel. If not set ({@code null}) then + * {@link GrpcUtil#DEFAULT_MAX_MESSAGE_SIZE gRPC's default} should be used. If set to {@code -1} then it will use + * the highest possible limit (not recommended). + * + * @return The maximum message size allowed or null if the default should be used. + * + * @see #setMaxInboundMessageSize(DataSize) + */ + public DataSize getMaxInboundMessageSize() { + return this.maxInboundMessageSize; + } + + /** + * Sets the maximum message size in bytes allowed to be received by the channel. If not set ({@code null}) then it + * will default to {@link GrpcUtil#DEFAULT_MAX_MESSAGE_SIZE gRPC's default}. If set to {@code -1} then it will use + * the highest possible limit (not recommended). + * + * @param maxInboundMessageSize The new maximum size in bytes allowed for incoming messages. {@code -1} for max + * possible. Null to use the gRPC's default. + * + * @see ManagedChannelBuilder#maxInboundMessageSize(int) + */ + public void setMaxInboundMessageSize(final DataSize maxInboundMessageSize) { + if (maxInboundMessageSize == null || maxInboundMessageSize.toBytes() >= 0) { + this.maxInboundMessageSize = maxInboundMessageSize; + } else if (maxInboundMessageSize.toBytes() == -1) { + this.maxInboundMessageSize = DataSize.ofBytes(Integer.MAX_VALUE); + } else { + throw new IllegalArgumentException("Unsupported maxInboundMessageSize: " + maxInboundMessageSize); + } + } + + // -------------------------------------------------- + + private Boolean fullStreamDecompression; + private static final boolean DEFAULT_FULL_STREAM_DECOMPRESSION = false; + + /** + * Gets whether full-stream decompression of inbound streams should be enabled. + * + * @return True, if full-stream decompression of inbound streams should be enabled. False otherwise. + * + * @see #setFullStreamDecompression(Boolean) + */ + public boolean isFullStreamDecompression() { + return this.fullStreamDecompression == null ? DEFAULT_FULL_STREAM_DECOMPRESSION : this.fullStreamDecompression; + } + + /** + * Sets whether full-stream decompression of inbound streams should be enabled. This will cause the channel's + * outbound headers to advertise support for GZIP compressed streams, and gRPC servers which support the feature may + * respond with a GZIP compressed stream. + * + * @param fullStreamDecompression Whether full stream decompression should be enabled or null to use the fallback. + * + * @see ManagedChannelBuilder#enableFullStreamDecompression() + */ + public void setFullStreamDecompression(final Boolean fullStreamDecompression) { + this.fullStreamDecompression = fullStreamDecompression; + } + + // -------------------------------------------------- + + private NegotiationType negotiationType; + private static final NegotiationType DEFAULT_NEGOTIATION_TYPE = NegotiationType.TLS; + + /** + * Gets the negotiation type to use on the connection. + * + * @return The negotiation type that the channel will use. + * + * @see #setNegotiationType(NegotiationType) + */ + public NegotiationType getNegotiationType() { + return this.negotiationType == null ? DEFAULT_NEGOTIATION_TYPE : this.negotiationType; + } + + /** + * Sets the negotiation type to use on the connection. Either of {@link NegotiationType#TLS TLS} (recommended), + * {@link NegotiationType#PLAINTEXT_UPGRADE PLAINTEXT_UPGRADE} or {@link NegotiationType#PLAINTEXT PLAINTEXT}. + * Defaults to TLS. + * + * @param negotiationType The negotiation type to use or null to use the fallback. + */ + public void setNegotiationType(final NegotiationType negotiationType) { + this.negotiationType = negotiationType; + } + + // -------------------------------------------------- + + private Duration immediateConnectTimeout; + private static final Duration DEFAULT_IMMEDIATE_CONNECT = Duration.ZERO; + + /** + * Get the connection timeout at application startup. + * + * @return connection timeout at application startup. + * + * @see #setImmediateConnectTimeout(Duration) + */ + public Duration getImmediateConnectTimeout() { + return this.immediateConnectTimeout == null ? DEFAULT_IMMEDIATE_CONNECT : this.immediateConnectTimeout; + } + + /** + * If set to a positive duration instructs the client to connect to the gRPC endpoint when the GRPC stub is created. + * As a result the application startup will be slightly slower due to connection process being executed + * synchronously up to the maximum to connection timeout. If the connection fails, the stub will fail to create with + * an exception which in turn causes the application context startup to fail. Defaults to {@code 0}. + * + * @param immediateConnectTimeout Connection timeout at application startup. + */ + public void setImmediateConnectTimeout(final Duration immediateConnectTimeout) { + if (immediateConnectTimeout.isNegative()) { + throw new IllegalArgumentException("Timeout can't be negative"); + } + this.immediateConnectTimeout = immediateConnectTimeout; + } + + // -------------------------------------------------- + + private final Security security = new Security(); + + /** + * Gets the options for transport security. + * + * @return The options for transport security. + */ + public Security getSecurity() { + return this.security; + } + + /** + * Copies the defaults from the given configuration. Values are considered "default" if they are null. Please note + * that the getters might return fallback values instead. + * + * @param config The config to copy the defaults from. + */ + public void copyDefaultsFrom(final GrpcChannelProperties config) { + if (this == config) { + return; + } + if (this.address == null) { + this.address = config.address; + } + if (this.defaultLoadBalancingPolicy == null) { + this.defaultLoadBalancingPolicy = config.defaultLoadBalancingPolicy; + } + if (this.enableKeepAlive == null) { + this.enableKeepAlive = config.enableKeepAlive; + } + if (this.keepAliveTime == null) { + this.keepAliveTime = config.keepAliveTime; + } + if (this.keepAliveTimeout == null) { + this.keepAliveTimeout = config.keepAliveTimeout; + } + if (this.keepAliveWithoutCalls == null) { + this.keepAliveWithoutCalls = config.keepAliveWithoutCalls; + } + if (this.shutdownGracePeriod == null) { + this.shutdownGracePeriod = config.shutdownGracePeriod; + } + if (this.maxInboundMessageSize == null) { + this.maxInboundMessageSize = config.maxInboundMessageSize; + } + if (this.fullStreamDecompression == null) { + this.fullStreamDecompression = config.fullStreamDecompression; + } + if (this.negotiationType == null) { + this.negotiationType = config.negotiationType; + } + if (this.immediateConnectTimeout == null) { + this.immediateConnectTimeout = config.immediateConnectTimeout; + } + this.security.copyDefaultsFrom(config.security); + } + + /** + * A container with options for the channel's transport security. + */ + @ToString + @EqualsAndHashCode + public static class Security { + + private Boolean clientAuthEnabled; + private static final boolean DEFAULT_CLIENT_AUTH_ENABLED = false; + + /** + * Gets whether client can authenticate using certificates. + * + * @return True, if the client can authenticate itself using certificates. + * + * @see #setClientAuthEnabled(Boolean) + */ + public boolean isClientAuthEnabled() { + return this.clientAuthEnabled == null ? DEFAULT_CLIENT_AUTH_ENABLED : this.clientAuthEnabled; + } + + /** + * Set whether client can authenticate using certificates. Defaults to {@code false}. + * + * @param clientAuthEnabled Whether the client can authenticate itself using certificates. + */ + public void setClientAuthEnabled(final Boolean clientAuthEnabled) { + this.clientAuthEnabled = clientAuthEnabled; + } + + // -------------------------------------------------- + + private Resource certificateChain = null; + + /** + * Gets the resource containing the SSL certificate chain. + * + * @return The certificate chain resource or null, if security is not enabled. + * @see #setCertificateChain(Resource) + */ + public Resource getCertificateChain() { + return this.certificateChain; + } + + /** + * Sets the resource containing the SSL certificate chain. Required if {@link #isClientAuthEnabled()} is true. + * The linked certificate will be used to authenticate the client. + * + * @param certificateChain The certificate chain. + * + * @see SslContextBuilder#keyManager(InputStream, InputStream, String) + */ + public void setCertificateChain(final Resource certificateChain) { + this.certificateChain = certificateChain; + } + + // -------------------------------------------------- + + private Resource privateKey = null; + + /** + * Gets resource containing the private key. + * + * @return The private key resource or null, if security is not enabled. + * + * @see #setPrivateKey(Resource) + */ + public Resource getPrivateKey() { + return this.privateKey; + } + + /** + * Sets the resource containing the private key. Required if {@link #isClientAuthEnabled} is true. + * + * @param privateKey The private key resource. + * + * @see SslContextBuilder#keyManager(InputStream, InputStream, String) + */ + public void setPrivateKey(final Resource privateKey) { + this.privateKey = privateKey; + } + + // -------------------------------------------------- + + private String privateKeyPassword = null; + + /** + * Gets the password for the private key. + * + * @return The password for the private key or null, if the private key is not set or not encrypted. + * + * @see #setPrivateKeyPassword(String) + */ + public String getPrivateKeyPassword() { + return this.privateKeyPassword; + } + + /** + * Sets the password for the private key. + * + * @param privateKeyPassword The password for the private key. + * + * @see SslContextBuilder#keyManager(File, File, String) + */ + public void setPrivateKeyPassword(final String privateKeyPassword) { + this.privateKeyPassword = privateKeyPassword; + } + + // -------------------------------------------------- + + private Resource trustCertCollection = null; + + /** + * Gets the resource containing the the trusted certificate collection. If {@code null} or empty the use the + * system's default collection should be used. + * + * @return The trusted certificate collection resource or null. + * + * @see #setTrustCertCollection(Resource) + */ + public Resource getTrustCertCollection() { + return this.trustCertCollection; + } + + /** + * Sets the resource containing the trusted certificate collection. If not set ({@code null}) it will use the + * system's default collection (Default). This collection will be used to verify server certificates. + * + * @param trustCertCollection The path to the trusted certificate collection. + * + * @see SslContextBuilder#trustManager(InputStream) + */ + public void setTrustCertCollection(final Resource trustCertCollection) { + this.trustCertCollection = trustCertCollection; + } + + // -------------------------------------------------- + + private String authorityOverride = null; + + /** + * Gets the authority to check for during server certificate verification. + * + * @return The override for the authority to check for or null, there is no override configured. + * + * @see #setAuthorityOverride(String) + */ + public String getAuthorityOverride() { + return this.authorityOverride; + } + + /** + * Sets the authority to check for during server certificate verification. By default the clients will use the + * name of the client to check the server certificate's common + alternative names. + * + * @param authorityOverride The authority to check for in the certificate, or null to use the default checks. + * + * @see NettyChannelBuilder#overrideAuthority(String) + */ + public void setAuthorityOverride(final String authorityOverride) { + this.authorityOverride = authorityOverride; + } + + // -------------------------------------------------- + + private List ciphers = null; + + /** + * Gets the cipher suite accepted for secure connections (in the order of preference). + * + * @return The cipher suite accepted for secure connections or null. + */ + public List getCiphers() { + return this.ciphers; + } + + /** + * Sets the cipher suite accepted for secure connections (in the order of preference). If not specified (null), + * then the default suites should be used. + * + * @param ciphers Cipher suite consisting of one or more cipher strings separated by colons, commas or spaces + * + * @see SslContextBuilder#ciphers(Iterable) + */ + public void setCiphers(final String ciphers) { + if (ciphers == null) { + this.ciphers = null; + } else { + this.ciphers = Arrays.asList(ciphers.split("[ :,]")); + } + } + + // -------------------------------------------------- + + private String[] protocols = null; + + /** + * Gets the TLS protocols accepted for secure connections + * + * @return The protocols accepted for secure connections or null. + */ + public String[] getProtocols() { + return this.protocols; + } + + /** + * Sets the TLS protocols accepted for secure connections. If not specified (null), then the default ones will + * be used. + * + * @param protocols Protocol list consisting of one or more protocols separated by colons, commas or spaces. + * + * @see SslContextBuilder#protocols(String...) + */ + public void setProtocols(final String protocols) { + if (protocols == null) { + this.protocols = null; + } else { + this.protocols = protocols.split("[ :,]"); + } + } + + // -------------------------------------------------- + + /** + * Copies the defaults from the given configuration. Values are considered "default" if they are null. Please + * note that the getters might return fallback values instead. + * + * @param config The config to copy the defaults from. + */ + public void copyDefaultsFrom(final Security config) { + if (this == config) { + return; + } + if (this.clientAuthEnabled == null) { + this.clientAuthEnabled = config.clientAuthEnabled; + } + if (this.certificateChain == null) { + this.certificateChain = config.certificateChain; + } + if (this.privateKey == null) { + this.privateKey = config.privateKey; + } + if (this.privateKeyPassword == null) { + this.privateKeyPassword = config.privateKeyPassword; + } + if (this.trustCertCollection == null) { + this.trustCertCollection = config.trustCertCollection; + } + if (this.authorityOverride == null) { + this.authorityOverride = config.authorityOverride; + } + if (this.ciphers == null) { + this.ciphers = config.ciphers; + } + if (this.protocols == null) { + this.protocols = config.protocols; + } + } + + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelsProperties.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelsProperties.java new file mode 100644 index 000000000..9ba097404 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/GrpcChannelsProperties.java @@ -0,0 +1,117 @@ +/* + * 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.config; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * A container for named channel properties. Each channel has its own configuration. If you try to get a channel that + * does not have a configuration yet, it will be created. If something is not configured in the channel properties, it + * will be copied from the global config during the first retrieval. If some property is configured in neither the + * channel properties nor the global properties then a default value will be used. + * + * @author Michael (yidongnan@gmail.com) + * @author Daniel Theuke (daniel.theuke@heuboe.de) + * @since 5/17/16 + */ +@ToString +@EqualsAndHashCode +@ConfigurationProperties("grpc") +public class GrpcChannelsProperties { + + /** + * The key that will be used for the {@code GLOBAL} properties. + */ + public static final String GLOBAL_PROPERTIES_KEY = "GLOBAL"; + + private final Map client = new ConcurrentHashMap<>(); + + /** + * Gets the configuration mapping for each client. + * + * @return The client configuration mappings. + */ + public final Map getClient() { + return this.client; + } + + /** + * Gets the properties for the given channel. If the properties for the specified channel name do not yet exist, + * they are created automatically. Before the instance is returned, the unset values are filled with values from the + * global properties. + * + * @param name The name of the channel to get the properties for. + * @return The properties for the given channel name. + */ + public GrpcChannelProperties getChannel(final String name) { + final GrpcChannelProperties properties = getRawChannel(name); + properties.copyDefaultsFrom(getGlobalChannel()); + return properties; + } + + /** + * Gets the global channel properties. Global properties are used, if the channel properties don't overwrite them. + * If neither the global nor the per client properties are set then default values will be used. + * + * @return The global channel properties. + */ + public final GrpcChannelProperties getGlobalChannel() { + // This cannot be moved to its own field, + // as Spring replaces the instance in the map and inconsistencies would occur. + return getRawChannel(GLOBAL_PROPERTIES_KEY); + } + + /** + * Gets or creates the channel properties for the given client. + * + * @param name The name of the channel to get the properties for. + * @return The properties for the given channel name. + */ + private GrpcChannelProperties getRawChannel(final String name) { + return this.client.computeIfAbsent(name, key -> new GrpcChannelProperties()); + } + + private String defaultScheme; + + /** + * Get the default scheme that should be used, if the client doesn't specify a scheme/address. + * + * @return The default scheme to use or null. + * @see #setDefaultScheme(String) + */ + public String getDefaultScheme() { + return this.defaultScheme; + } + + /** + * Sets the default scheme to use, if the client doesn't specify a scheme/address. If not specified it will default + * to the default scheme of the {@link io.grpc.NameResolver.Factory}. Examples: {@code dns}, {@code discovery}. + * + * @param defaultScheme The default scheme to use or null. + */ + public void setDefaultScheme(String defaultScheme) { + this.defaultScheme = defaultScheme; + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/NegotiationType.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/NegotiationType.java new file mode 100644 index 000000000..8e09e0bc1 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/NegotiationType.java @@ -0,0 +1,44 @@ +/* + * 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.config; + +/** + * Identifies the negotiation used for starting up HTTP/2. + * + * @see io.grpc.netty.shaded.io.grpc.netty.NegotiationType NegotiationType + */ +// This class needs to be duplicated to avoid direct dependencies to either of the grpc-netty (shaded) libraries. +public enum NegotiationType { + + /** + * Uses TLS ALPN/NPN negotiation, assumes an SSL connection. + */ + TLS, + + /** + * Use the HTTP UPGRADE protocol for a plaintext (non-SSL) upgrade from HTTP/1.1 to HTTP/2. + */ + PLAINTEXT_UPGRADE, + + /** + * Just assume the connection is plaintext (non-SSL) and the remote endpoint supports HTTP/2 directly without an + * upgrade. + */ + PLAINTEXT; + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/package-info.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/package-info.java new file mode 100644 index 000000000..14dff6b7b --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/config/package-info.java @@ -0,0 +1,5 @@ +/** + * Classes related to the gRPC client configuration. + */ + +package net.devh.boot.grpc.client.config; 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 new file mode 100644 index 000000000..b01330909 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClient.java @@ -0,0 +1,123 @@ +/* + * 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.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.inject.Inject; + +import org.springframework.beans.factory.annotation.Autowired; + +import io.grpc.CallCredentials; +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientInterceptor; +import io.grpc.ClientInterceptors; +import io.grpc.stub.AbstractStub; +import net.devh.boot.grpc.client.config.GrpcChannelProperties; +import net.devh.boot.grpc.client.config.GrpcChannelProperties.Security; + +/** + * 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 + * automatically populated/invoked by Spring. + * + *

+ * Note: Fields/Methods that are annotated with this annotation should NOT be annotated with {@link Autowired} or + * {@link Inject} (conflict). + *

+ * + *

+ * Note: If you annotate an AbstractStub with this annotation the bean processing will also apply the + * {@link StubTransformer}s in the application context. These can be used, for example, to configure {@link CallOptions} + * such as {@link CallCredentials}. Please note that these transformations aren't applied if you inject a + * {@link Channel} only. + *

+ * + *

+ * Note: These annotation allows the specification of custom interceptors. These will be appended to the global + * interceptors and applied using {@link ClientInterceptors#interceptForward(Channel, ClientInterceptor...)}. + *

+ * + * @author Michael (yidongnan@gmail.com) + * @since 2016/12/7 + */ +@Target({ElementType.FIELD, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +public @interface GrpcClient { + + /** + * The name of the grpc client. This name will be used to get the {@link GrpcChannelProperties config options} for + * this client. + * + *

+ * Example: @GrpcClient("myClient") <-> + * {@code grpc.client.myClient.address=static://localhost:9090} + *

+ * + *

+ * Note: This value might also be used to check the common / alternative names in server certificate, you can + * overwrite this value with the {@link Security security.authorityOverride} property. + *

+ * + * @return The name of the grpc client. + */ + String value(); + + /** + * A list of {@link ClientInterceptor} classes that should be used with this client in addition to the globally + * defined ones. If a bean of the given type exists, it will be used; otherwise a new instance of that class will be + * created via no-args constructor. + * + *

+ * Note: Please read the javadocs regarding the ordering of interceptors. + *

+ * + * @return A list of ClientInterceptor classes that should be used. + */ + Class[] interceptors() default {}; + + /** + * A list of {@link ClientInterceptor} beans that should be used with this client in addition to the globally + * defined ones. + * + *

+ * Note: Please read the javadocs regarding the ordering of interceptors. + *

+ * + * @return A list of ClientInterceptor beans that should be used. + */ + String[] interceptorNames() default {}; + + /** + * Whether the custom interceptors should be mixed with the global interceptors and sorted afterwards. Use this + * option if you want to add a custom interceptor between global interceptors. + * + * @return True, if the custom interceptors should be merged with the global ones and sorted afterwards. False + * otherwise. + */ + boolean sortInterceptors() default false; + +} 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 new file mode 100644 index 000000000..eaa0e6049 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/GrpcClientBeanPostProcessor.java @@ -0,0 +1,268 @@ +/* + * 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 static java.util.Objects.requireNonNull; + +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.BeansException; +import org.springframework.beans.InvalidPropertyException; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ReflectionUtils; + +import com.google.common.collect.Lists; + +import io.grpc.Channel; +import io.grpc.ClientInterceptor; +import io.grpc.stub.AbstractStub; +import net.devh.boot.grpc.client.channelfactory.GrpcChannelFactory; +import net.devh.boot.grpc.client.nameresolver.NameResolverRegistration; +import net.devh.boot.grpc.client.stubfactory.FallbackStubFactory; +import net.devh.boot.grpc.client.stubfactory.StubFactory; + +/** + * This {@link BeanPostProcessor} searches for fields and methods in beans that are annotated with {@link GrpcClient} + * and sets them. + * + * @author Michael (yidongnan@gmail.com) + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public class GrpcClientBeanPostProcessor implements BeanPostProcessor { + + private final ApplicationContext applicationContext; + + // Is only retrieved when needed to avoid too early initialization of these components, + // which could lead to problems with the correct bean setup. + private GrpcChannelFactory channelFactory = null; + private List stubTransformers = null; + private List stubFactories = null; + + /** + * Creates a new GrpcClientBeanPostProcessor with the given ApplicationContext. + * + * @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"); + } + + @Override + 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)); + } + } + clazz = clazz.getSuperclass(); + } while (clazz != null); + return bean; + } + + /** + * Processes the given injection point and computes the appropriate value for the injection. + * + * @param The type of the value to be injected. + * @param injectionTarget The target of the injection. + * @param injectionType The class that will be used to compute injection. + * @param annotation The annotation on the target with the metadata for the injection. + * @return The value to be injected for the given injection point. + */ + protected T processInjectionPoint(final Member injectionTarget, final Class injectionType, + final GrpcClient annotation) { + final List interceptors = interceptorsFromAnnotation(annotation); + final String name = annotation.value(); + final Channel channel; + try { + channel = getChannelFactory().createChannel(name, interceptors, annotation.sortInterceptors()); + if (channel == null) { + throw new IllegalStateException("Channel factory created a null channel for " + name); + } + } catch (final RuntimeException e) { + throw new IllegalStateException("Failed to create channel: " + name, e); + } + + final T value = valueForMember(name, injectionTarget, injectionType, channel); + if (value == null) { + throw new IllegalStateException( + "Injection value is null unexpectedly for " + name + " at " + injectionTarget); + } + return value; + } + + /** + * Lazy getter for the {@link GrpcChannelFactory}. + * + * @return The grpc channel factory to use. + */ + private GrpcChannelFactory getChannelFactory() { + if (this.channelFactory == null) { + // Ensure that the NameResolverProviders have been registered + this.applicationContext.getBean(NameResolverRegistration.class); + final GrpcChannelFactory factory = this.applicationContext.getBean(GrpcChannelFactory.class); + this.channelFactory = factory; + return factory; + } + return this.channelFactory; + } + + /** + * Lazy getter for the {@link StubTransformer}s. + * + * @return The stub transformers to use. + */ + private List getStubTransformers() { + if (this.stubTransformers == null) { + final Collection transformers = + this.applicationContext.getBeansOfType(StubTransformer.class).values(); + this.stubTransformers = new ArrayList<>(transformers); + return this.stubTransformers; + } + return this.stubTransformers; + } + + /** + * Gets or creates the {@link ClientInterceptor}s that are referenced in the given annotation. + * + *

+ * Note: This methods return value does not contain the global client interceptors because they are handled + * by the {@link GrpcChannelFactory}. + *

+ * + * @param annotation The annotation to get the interceptors for. + * @return A list containing the interceptors for the given annotation. + * @throws BeansException If the referenced interceptors weren't found or could not be created. + */ + protected List interceptorsFromAnnotation(final GrpcClient annotation) throws BeansException { + final List list = Lists.newArrayList(); + for (final Class interceptorClass : annotation.interceptors()) { + final ClientInterceptor clientInterceptor; + if (this.applicationContext.getBeanNamesForType(interceptorClass).length > 0) { + clientInterceptor = this.applicationContext.getBean(interceptorClass); + } else { + try { + clientInterceptor = interceptorClass.getConstructor().newInstance(); + } catch (final Exception e) { + throw new BeanCreationException("Failed to create interceptor instance", e); + } + } + list.add(clientInterceptor); + } + for (final String interceptorName : annotation.interceptorNames()) { + list.add(this.applicationContext.getBean(interceptorName, ClientInterceptor.class)); + } + return list; + } + + /** + * Creates the instance to be injected for the given member. + * + * @param The type of the instance to be injected. + * @param name The name that was used to create the channel. + * @param injectionTarget The target member for the injection. + * @param injectionType The class that should injected. + * @param channel The channel that should be used to create the instance. + * @return The value that matches the type of the given field. + * @throws BeansException If the value of the field could not be created or the type of the field is unsupported. + */ + protected T valueForMember(final String name, final Member injectionTarget, + final Class injectionType, + final Channel channel) throws BeansException { + if (Channel.class.equals(injectionType)) { + return injectionType.cast(channel); + } else if (AbstractStub.class.isAssignableFrom(injectionType)) { + + @SuppressWarnings("unchecked") // Eclipse incorrectly marks this as not required + AbstractStub stub = createStub( + (Class>) injectionType.asSubclass(AbstractStub.class), channel); + for (final StubTransformer stubTransformer : getStubTransformers()) { + stub = stubTransformer.transform(name, stub); + } + return injectionType.cast(stub); + } else { + throw new InvalidPropertyException(injectionTarget.getDeclaringClass(), injectionTarget.getName(), + "Unsupported type " + injectionType.getName()); + } + } + + /** + * Creates a stub instance for the specified stub type using the resolved {@link StubFactory}. + * + * @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. + * @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() + .filter(stubFactory -> stubFactory.isApplicable(stubClass)) + .findFirst() + .orElseThrow(() -> new BeanInstantiationException(stubClass, + "Unsupported stub type: " + stubClass.getName() + " -> Please report this issue.")); + + try { + return factory.createStub(stubClass, channel); + } catch (final Exception exception) { + throw new BeanInstantiationException(stubClass, "Failed to create gRPC stub of type " + stubClass.getName(), + exception); + } + } + + /** + * Lazy getter for the list of defined {@link StubFactory} beans. + * + * @return A list of all defined {@link StubFactory} beans. + */ + private List getStubFactories() { + if (this.stubFactories == null) { + this.stubFactories = new ArrayList<>(this.applicationContext.getBeansOfType(StubFactory.class).values()); + this.stubFactories.add(new FallbackStubFactory()); + } + return this.stubFactories; + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/StubTransformer.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/StubTransformer.java new file mode 100644 index 000000000..a1feedd32 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/StubTransformer.java @@ -0,0 +1,49 @@ +/* + * 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 io.grpc.Channel; +import io.grpc.stub.AbstractStub; +import net.devh.boot.grpc.client.channelfactory.GrpcChannelFactory; + +/** + * A stub transformer will be used by the {@link GrpcClientBeanPostProcessor} to configure the stubs before they are + * assigned to their fields. Implementations should only call the {@code AbstractStub#with...} methods on the given + * stubs and return that result. Implementations should not use this transformer to replace the stub with a unrelated + * other instance. + * + *

+ * Note: StubTransformer will only transform {@link AbstractStub}s and NOT {@link Channel}s. To configure + * channels use the {@link GrpcChannelFactory}. + *

+ * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@FunctionalInterface +public interface StubTransformer { + + /** + * Transform the given stub using {@code AbstractStub#with...} methods. + * + * @param name The name that was used to create the stub. + * @param stub The stub that should be transformed. + * @return The transformed stub. + */ + AbstractStub transform(String name, AbstractStub stub); + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/package-info.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/package-info.java new file mode 100644 index 000000000..b3271e0fd --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/inject/package-info.java @@ -0,0 +1,5 @@ +/** + * Classes used to inject the client references into beans. + */ + +package net.devh.boot.grpc.client.inject; diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/AnnotationGlobalClientInterceptorConfigurer.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/AnnotationGlobalClientInterceptorConfigurer.java new file mode 100644 index 000000000..351d93cdd --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/AnnotationGlobalClientInterceptorConfigurer.java @@ -0,0 +1,72 @@ +/* + * 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.interceptor; + +import static com.google.common.collect.Maps.transformValues; +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.context.ApplicationContext; + +import io.grpc.ClientInterceptor; +import lombok.extern.slf4j.Slf4j; + +/** + * Automatically find and configure {@link GrpcGlobalClientInterceptor annotated} global {@link ClientInterceptor}s. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +public class AnnotationGlobalClientInterceptorConfigurer implements GlobalClientInterceptorConfigurer { + + private final ApplicationContext applicationContext; + + /** + * Creates a new AnnotationGlobalClientInterceptorConfigurer. + * + * @param applicationContext The application context to fetch the {@link GrpcGlobalClientInterceptor} annotated + * {@link ClientInterceptor} beans from. + */ + public AnnotationGlobalClientInterceptorConfigurer(final ApplicationContext applicationContext) { + this.applicationContext = requireNonNull(applicationContext, "applicationContext"); + } + + /** + * Helper method used to get the {@link GrpcGlobalClientInterceptor} annotated {@link ClientInterceptor}s from the + * application context. + * + * @return A map containing the global interceptor beans. + */ + protected Map getClientInterceptorBeans() { + return transformValues(this.applicationContext.getBeansWithAnnotation(GrpcGlobalClientInterceptor.class), + ClientInterceptor.class::cast); + } + + @Override + public void configureClientInterceptors(final List interceptors) { + for (final Entry entry : getClientInterceptorBeans().entrySet()) { + final ClientInterceptor interceptor = entry.getValue(); + log.debug("Registering GlobalClientInterceptor: {} ({})", entry.getKey(), interceptor); + interceptors.add(interceptor); + } + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/GlobalClientInterceptorConfigurer.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/GlobalClientInterceptorConfigurer.java new file mode 100644 index 000000000..17efc3c79 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/GlobalClientInterceptorConfigurer.java @@ -0,0 +1,40 @@ +/* + * 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.interceptor; + +import java.util.List; + +import io.grpc.ClientInterceptor; + +/** + * A bean that can be used to configure global {@link ClientInterceptor}s. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@FunctionalInterface +public interface GlobalClientInterceptorConfigurer { + + /** + * Configures the given list of client interceptors, possibly adding new elements, removing unwanted elements, or + * reordering the existing ones. + * + * @param interceptors A mutable list of client interceptors to configure. + */ + void configureClientInterceptors(List interceptors); + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/GlobalClientInterceptorRegistry.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/GlobalClientInterceptorRegistry.java new file mode 100644 index 000000000..552de9c7c --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/GlobalClientInterceptorRegistry.java @@ -0,0 +1,99 @@ +/* + * 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.interceptor; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; + +import com.google.common.collect.ImmutableList; + +import io.grpc.Channel; +import io.grpc.ClientInterceptor; +import io.grpc.ClientInterceptors; + +/** + * The global client interceptor registry keeps references to all {@link ClientInterceptor}s that should be registered + * to all client channels. The interceptors will be applied in the same order they as specified by the + * {@link #sortInterceptors(List)} method. + * + *

+ * Note: Custom interceptors will be appended to the global interceptors and applied using + * {@link ClientInterceptors#interceptForward(Channel, ClientInterceptor...)}. + *

+ * + * @author Michael (yidongnan@gmail.com) + */ +public class GlobalClientInterceptorRegistry { + + private final ApplicationContext applicationContext; + + private ImmutableList sortedClientInterceptors; + + /** + * Creates a new GlobalClientInterceptorRegistry. + * + * @param applicationContext The application context to fetch the {@link GlobalClientInterceptorConfigurer} beans + * from. + */ + public GlobalClientInterceptorRegistry(final ApplicationContext applicationContext) { + this.applicationContext = requireNonNull(applicationContext, "applicationContext"); + } + + /** + * Gets the immutable list of global server interceptors. + * + * @return The list of globally registered server interceptors. + */ + public ImmutableList getClientInterceptors() { + if (this.sortedClientInterceptors == null) { + this.sortedClientInterceptors = ImmutableList.copyOf(initClientInterceptors()); + } + return this.sortedClientInterceptors; + } + + /** + * Initializes the list of client interceptors. + * + * @return The list of global client interceptors. + */ + protected List initClientInterceptors() { + final List interceptors = new ArrayList<>(); + for (final GlobalClientInterceptorConfigurer configurer : this.applicationContext + .getBeansOfType(GlobalClientInterceptorConfigurer.class).values()) { + configurer.configureClientInterceptors(interceptors); + } + sortInterceptors(interceptors); + return interceptors; + } + + /** + * Sorts the given list of interceptors. Use this method if you want to sort custom interceptors. The default + * implementation will sort them by using then {@link AnnotationAwareOrderComparator}. + * + * @param interceptors The interceptors to sort. + */ + public void sortInterceptors(final List interceptors) { + interceptors.sort(AnnotationAwareOrderComparator.INSTANCE); + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/GrpcGlobalClientInterceptor.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/GrpcGlobalClientInterceptor.java new file mode 100644 index 000000000..2b19dc37d --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/GrpcGlobalClientInterceptor.java @@ -0,0 +1,42 @@ +/* + * 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.interceptor; + +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 org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +import io.grpc.ClientInterceptor; + +/** + * Annotation for gRPC {@link ClientInterceptor}s to apply them globally. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +@Bean +public @interface GrpcGlobalClientInterceptor { +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/OrderedClientInterceptor.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/OrderedClientInterceptor.java new file mode 100644 index 000000000..1bc6b1b77 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/OrderedClientInterceptor.java @@ -0,0 +1,67 @@ +/* + * 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.interceptor; + +import static java.util.Objects.requireNonNull; + +import org.springframework.core.Ordered; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; + +/** + * A client interceptor wrapper that assigns an order to the underlying client interceptor. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public class OrderedClientInterceptor implements ClientInterceptor, Ordered { + + private final ClientInterceptor clientInterceptor; + private final int order; + + /** + * Creates a new OrderedClientInterceptor with the given client interceptor and order. + * + * @param clientInterceptor The client interceptor to delegate to. + * @param order The order of this interceptor. + */ + public OrderedClientInterceptor(ClientInterceptor clientInterceptor, int order) { + this.clientInterceptor = requireNonNull(clientInterceptor, "clientInterceptor"); + this.order = order; + } + + @Override + public ClientCall interceptCall(MethodDescriptor method, + CallOptions callOptions, Channel next) { + return this.clientInterceptor.interceptCall(method, callOptions, next); + } + + @Override + public int getOrder() { + return this.order; + } + + @Override + public String toString() { + return "OrderedClientInterceptor [interceptor=" + this.clientInterceptor + ", order=" + this.order + "]"; + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/package-info.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/package-info.java new file mode 100644 index 000000000..5373ba6c7 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/interceptor/package-info.java @@ -0,0 +1,5 @@ +/** + * Classes related to the gRPC (global) client interceptors and their discovery. + */ + +package net.devh.boot.grpc.client.interceptor; diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metric/MetricCollectingClientCall.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metric/MetricCollectingClientCall.java new file mode 100644 index 000000000..96889c7f4 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metric/MetricCollectingClientCall.java @@ -0,0 +1,77 @@ +/* + * 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.metric; + +import java.util.function.Consumer; + +import io.grpc.ClientCall; +import io.grpc.ForwardingClientCall.SimpleForwardingClientCall; +import io.grpc.Metadata; +import io.grpc.Status; +import io.micrometer.core.instrument.Counter; + +/** + * A simple forwarding client call that collects metrics for micrometer. + * + * @param The type of message sent one or more times to the server. + * @param The type of message received one or more times from the server. + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +class MetricCollectingClientCall extends SimpleForwardingClientCall { + + private final Counter requestCounter; + private final Counter responseCounter; + private final Consumer processingDurationTiming; + + /** + * Creates a new delegating ClientCall that will wrap the given client call to collect metrics. + * + * @param delegate The original call to wrap. + * @param requestCounter The counter for outgoing requests. + * @param responseCounter The counter for incoming responses. + * @param processingDurationTiming The consumer used to time the processing duration along with a response status. + */ + public MetricCollectingClientCall( + final ClientCall delegate, + final Counter requestCounter, + final Counter responseCounter, + final Consumer processingDurationTiming) { + + super(delegate); + this.requestCounter = requestCounter; + this.responseCounter = responseCounter; + this.processingDurationTiming = processingDurationTiming; + } + + @Override + public void start(final ClientCall.Listener responseListener, final Metadata metadata) { + super.start( + new MetricCollectingClientCallListener<>( + responseListener, + this.responseCounter, + this.processingDurationTiming), + metadata); + } + + @Override + public void sendMessage(final Q requestMessage) { + this.requestCounter.increment(); + super.sendMessage(requestMessage); + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metric/MetricCollectingClientCallListener.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metric/MetricCollectingClientCallListener.java new file mode 100644 index 000000000..50c91fd3d --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metric/MetricCollectingClientCallListener.java @@ -0,0 +1,68 @@ +/* + * 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.metric; + +import java.util.function.Consumer; + +import io.grpc.ClientCall; +import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener; +import io.grpc.Metadata; +import io.grpc.Status; +import io.micrometer.core.instrument.Counter; + +/** + * A simple forwarding client call listener that collects metrics for micrometer. + * + * @param The type of message received one or more times from the server. + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +class MetricCollectingClientCallListener extends SimpleForwardingClientCallListener { + + private final Counter responseCounter; + private final Consumer processingDurationTiming; + + /** + * Creates a new delegating ClientCallListener that will wrap the given client call listener to collect metrics. + * + * @param delegate The original call to wrap. + * @param responseCounter The counter for incoming responses. + * @param processingDurationTiming The consumer used to time the processing duration along with a response status. + */ + public MetricCollectingClientCallListener( + final ClientCall.Listener delegate, + final Counter responseCounter, + final Consumer processingDurationTiming) { + + super(delegate); + this.responseCounter = responseCounter; + this.processingDurationTiming = processingDurationTiming; + } + + @Override + public void onClose(final Status status, final Metadata metadata) { + this.processingDurationTiming.accept(status.getCode()); + super.onClose(status, metadata); + } + + @Override + public void onMessage(final A responseMessage) { + this.responseCounter.increment(); + super.onMessage(responseMessage); + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metric/MetricCollectingClientInterceptor.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metric/MetricCollectingClientInterceptor.java new file mode 100644 index 000000000..7134819ce --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metric/MetricCollectingClientInterceptor.java @@ -0,0 +1,123 @@ +/* + * 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.metric; + +import static net.devh.boot.grpc.common.metric.MetricConstants.METRIC_NAME_CLIENT_PROCESSING_DURATION; +import static net.devh.boot.grpc.common.metric.MetricConstants.METRIC_NAME_CLIENT_REQUESTS_SENT; +import static net.devh.boot.grpc.common.metric.MetricConstants.METRIC_NAME_CLIENT_RESPONSES_RECEIVED; +import static net.devh.boot.grpc.common.metric.MetricUtils.prepareCounterFor; +import static net.devh.boot.grpc.common.metric.MetricUtils.prepareTimerFor; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.Order; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; +import io.grpc.Status.Code; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor; +import net.devh.boot.grpc.common.metric.AbstractMetricCollectingInterceptor; +import net.devh.boot.grpc.common.util.InterceptorOrder; + +/** + * A gRPC client interceptor that will collect metrics for micrometer. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@GrpcGlobalClientInterceptor +@Order(InterceptorOrder.ORDER_TRACING_METRICS) +public class MetricCollectingClientInterceptor extends AbstractMetricCollectingInterceptor + implements ClientInterceptor { + + /** + * Creates a new gRPC client interceptor that will collect metrics into the given {@link MeterRegistry}. + * + * @param registry The registry to use. + */ + @Autowired + public MetricCollectingClientInterceptor(final MeterRegistry registry) { + super(registry); + } + + /** + * Creates a new gRPC client interceptor that will collect metrics into the given {@link MeterRegistry} and uses the + * given customizer to configure the {@link Counter}s and {@link Timer}s. + * + * @param registry The registry to use. + * @param counterCustomizer The unary function that can be used to customize the created counters. + * @param timerCustomizer The unary function that can be used to customize the created timers. + * @param eagerInitializedCodes The status codes that should be eager initialized. + */ + public MetricCollectingClientInterceptor(final MeterRegistry registry, + final UnaryOperator counterCustomizer, + final UnaryOperator timerCustomizer, final Code... eagerInitializedCodes) { + super(registry, counterCustomizer, timerCustomizer, eagerInitializedCodes); + } + + @Override + protected Counter newRequestCounterFor(final MethodDescriptor method) { + return this.counterCustomizer.apply( + prepareCounterFor(method, + METRIC_NAME_CLIENT_REQUESTS_SENT, + "The total number of requests sent")) + .register(this.registry); + } + + @Override + protected Counter newResponseCounterFor(final MethodDescriptor method) { + return this.counterCustomizer.apply( + prepareCounterFor(method, + METRIC_NAME_CLIENT_RESPONSES_RECEIVED, + "The total number of responses received")) + .register(this.registry); + } + + @Override + protected Function newTimerFunction(final MethodDescriptor method) { + return asTimerFunction(() -> this.timerCustomizer.apply( + prepareTimerFor(method, + METRIC_NAME_CLIENT_PROCESSING_DURATION, + "The total time taken for the client to complete the call, including network delay"))); + } + + @Override + public ClientCall interceptCall( + final MethodDescriptor methodDescriptor, + final CallOptions callOptions, + final Channel channel) { + + final MetricSet metrics = metricsFor(methodDescriptor); + final Consumer processingDurationTiming = metrics.newProcessingDurationTiming(this.registry); + + return new MetricCollectingClientCall<>( + channel.newCall(methodDescriptor, callOptions), + metrics.getRequestCounter(), + metrics.getResponseCounter(), + processingDurationTiming); + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metric/package-info.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metric/package-info.java new file mode 100644 index 000000000..ab8cf38a9 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/metric/package-info.java @@ -0,0 +1,5 @@ +/** + * A package containing the client side classes for grpc metric collection. + */ + +package net.devh.boot.grpc.client.metric; diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/DiscoveryClientNameResolver.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/DiscoveryClientNameResolver.java new file mode 100644 index 000000000..e2288aff7 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/DiscoveryClientNameResolver.java @@ -0,0 +1,297 @@ +/* + * 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.nameresolver; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +import java.net.InetSocketAddress; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.util.CollectionUtils; + +import com.google.common.collect.Lists; + +import io.grpc.Attributes; +import io.grpc.EquivalentAddressGroup; +import io.grpc.NameResolver; +import io.grpc.Status; +import io.grpc.SynchronizationContext; +import io.grpc.internal.SharedResourceHolder; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.common.util.GrpcUtils; + +/** + * The DiscoveryClientNameResolver resolves the service hosts and their associated gRPC port using the channel's name + * and spring's cloud {@link DiscoveryClient}. The ports are extracted from the {@code gRPC_port} metadata. + * + * @author Michael (yidongnan@gmail.com) + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +public class DiscoveryClientNameResolver extends NameResolver { + + @Deprecated + private static final String LEGACY_CLOUD_DISCOVERY_METADATA_PORT = "gRPC.port"; + private static final List KEEP_PREVIOUS = null; + + private final String name; + private final DiscoveryClient client; + private final SynchronizationContext syncContext; + private final Runnable externalCleaner; + private final SharedResourceHolder.Resource executorResource; + private final boolean usingExecutorResource; + + // The field must be accessed from syncContext, although the methods on an Listener2 can be called + // from any thread. + private Listener2 listener; + // Following fields must be accessed from syncContext + private Executor executor; + private boolean resolving; + private List instanceList = Lists.newArrayList(); + + /** + * Creates a new DiscoveryClientNameResolver. + * + * @param name The name of the service to look up. + * @param client The client used to look up the service addresses. + * @param args The name resolver args. + * @param executorResource The executor resource. + * @param externalCleaner The optional cleaner used during {@link #shutdown()} + */ + public DiscoveryClientNameResolver(final String name, final DiscoveryClient client, final Args args, + final SharedResourceHolder.Resource executorResource, final Runnable externalCleaner) { + this.name = name; + this.client = client; + this.syncContext = requireNonNull(args.getSynchronizationContext(), "syncContext"); + this.externalCleaner = externalCleaner; + this.executor = args.getOffloadExecutor(); + this.usingExecutorResource = this.executor == null; + this.executorResource = executorResource; + } + + @Override + public final String getServiceAuthority() { + return this.name; + } + + @Override + public void start(final Listener2 listener) { + checkState(this.listener == null, "already started"); + if (this.usingExecutorResource) { + this.executor = SharedResourceHolder.get(this.executorResource); + } + this.listener = checkNotNull(listener, "listener"); + resolve(); + } + + @Override + public void refresh() { + checkState(this.listener != null, "not started"); + resolve(); + } + + /** + * Triggers a refresh on the listener from non-grpc threads. This method can safely be called, even if the listener + * hasn't been started yet. + * + * @see #refresh() + */ + public void refreshFromExternal() { + this.syncContext.execute(() -> { + if (this.listener != null) { + resolve(); + } + }); + } + + private void resolve() { + log.debug("Scheduled resolve for {}", this.name); + if (this.resolving) { + return; + } + this.resolving = true; + this.executor.execute(new Resolve(this.listener, this.instanceList)); + } + + @Override + public void shutdown() { + this.listener = null; + if (this.executor != null && this.usingExecutorResource) { + this.executor = SharedResourceHolder.release(this.executorResource, this.executor); + } + this.instanceList = Lists.newArrayList(); + if (this.externalCleaner != null) { + this.externalCleaner.run(); + } + } + + @Override + public String toString() { + return "DiscoveryClientNameResolver [name=" + this.name + ", discoveryClient=" + this.client + "]"; + } + + /** + * The logic for updating the gRPC server list using a discovery client. + */ + private final class Resolve implements Runnable { + + private final Listener2 savedListener; + private final List savedInstanceList; + + /** + * Creates a new Resolve that stores a snapshot of the relevant states of the resolver. + * + * @param listener The listener to send the results to. + * @param instanceList The current server instance list. + */ + Resolve(final Listener2 listener, final List instanceList) { + this.savedListener = requireNonNull(listener, "listener"); + this.savedInstanceList = requireNonNull(instanceList, "instanceList"); + } + + @Override + public void run() { + final AtomicReference> resultContainer = new AtomicReference<>(); + try { + resultContainer.set(resolveInternal()); + } catch (final Exception e) { + this.savedListener.onError(Status.UNAVAILABLE.withCause(e) + .withDescription("Failed to update server list for " + DiscoveryClientNameResolver.this.name)); + resultContainer.set(Lists.newArrayList()); + } finally { + DiscoveryClientNameResolver.this.syncContext.execute(() -> { + DiscoveryClientNameResolver.this.resolving = false; + final List result = resultContainer.get(); + if (result != KEEP_PREVIOUS && DiscoveryClientNameResolver.this.listener != null) { + DiscoveryClientNameResolver.this.instanceList = result; + } + }); + } + } + + /** + * Do the actual update checks and resolving logic. + * + * @return The new service instance list that is used to connect to the gRPC server or null if the old ones + * should be used. + */ + private List resolveInternal() { + final String name = DiscoveryClientNameResolver.this.name; + final List newInstanceList = + DiscoveryClientNameResolver.this.client.getInstances(name); + log.debug("Got {} candidate servers for {}", newInstanceList.size(), name); + if (CollectionUtils.isEmpty(newInstanceList)) { + log.error("No servers found for {}", name); + this.savedListener.onError(Status.UNAVAILABLE.withDescription("No servers found for " + name)); + return Lists.newArrayList(); + } + if (!needsToUpdateConnections(newInstanceList)) { + log.debug("Nothing has changed... skipping update for {}", name); + return KEEP_PREVIOUS; + } + log.debug("Ready to update server list for {}", name); + final List targets = Lists.newArrayList(); + for (final ServiceInstance instance : newInstanceList) { + final int port = getGRPCPort(instance); + log.debug("Found gRPC server {}:{} for {}", instance.getHost(), port, name); + targets.add(new EquivalentAddressGroup( + new InetSocketAddress(instance.getHost(), port), Attributes.EMPTY)); + } + if (targets.isEmpty()) { + log.error("None of the servers for {} specified a gRPC port", name); + this.savedListener.onError(Status.UNAVAILABLE + .withDescription("None of the servers for " + name + " specified a gRPC port")); + return Lists.newArrayList(); + } else { + this.savedListener.onResult(ResolutionResult.newBuilder() + .setAddresses(targets) + .build()); + log.info("Done updating server list for {}", name); + return newInstanceList; + } + } + + /** + * Extracts the gRPC server port from the given service instance. + * + * @param instance The instance to extract the port from. + * @return The gRPC server port. + * @throws IllegalArgumentException If the specified port definition couldn't be parsed. + */ + private int getGRPCPort(final ServiceInstance instance) { + final Map metadata = instance.getMetadata(); + if (metadata == null) { + return instance.getPort(); + } + String portString = metadata.get(GrpcUtils.CLOUD_DISCOVERY_METADATA_PORT); + if (portString == null) { + portString = metadata.get(LEGACY_CLOUD_DISCOVERY_METADATA_PORT); + if (portString == null) { + return instance.getPort(); + } else { + log.warn("Found legacy grpc port metadata '{}' for client '{}' use '{}' instead", + LEGACY_CLOUD_DISCOVERY_METADATA_PORT, DiscoveryClientNameResolver.this.name, + GrpcUtils.CLOUD_DISCOVERY_METADATA_PORT); + } + } + try { + return Integer.parseInt(portString); + } catch (final NumberFormatException e) { + // TODO: How to handle this case? + throw new IllegalArgumentException("Failed to parse gRPC port information from: " + instance, e); + } + } + + /** + * Checks whether this instance should update its connections. + * + * @param newInstanceList The new instances that should be compared to the stored ones. + * @return True, if the given instance list contains different entries than the stored ones. + */ + private boolean needsToUpdateConnections(final List newInstanceList) { + if (this.savedInstanceList.size() != newInstanceList.size()) { + return true; + } + for (final ServiceInstance instance : this.savedInstanceList) { + final int port = getGRPCPort(instance); + boolean isSame = false; + for (final ServiceInstance newInstance : newInstanceList) { + final int newPort = getGRPCPort(newInstance); + if (newInstance.getHost().equals(instance.getHost()) + && port == newPort) { + isSame = true; + break; + } + } + if (!isSame) { + return true; + } + } + return false; + } + + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/DiscoveryClientResolverFactory.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/DiscoveryClientResolverFactory.java new file mode 100644 index 000000000..c5047dc46 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/DiscoveryClientResolverFactory.java @@ -0,0 +1,131 @@ +/* + * 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.nameresolver; + +import static java.util.Objects.requireNonNull; + +import java.net.URI; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; + +import javax.annotation.Nullable; +import javax.annotation.PreDestroy; + +import org.springframework.cloud.client.discovery.DiscoveryClient; +import org.springframework.cloud.client.discovery.event.HeartbeatEvent; +import org.springframework.cloud.client.discovery.event.HeartbeatMonitor; +import org.springframework.context.event.EventListener; + +import io.grpc.NameResolver; +import io.grpc.NameResolverProvider; +import io.grpc.internal.GrpcUtil; + +/** + * A name resolver factory that will create a {@link DiscoveryClientNameResolver} based on the target uri. + * + * @author Michael (yidongnan@gmail.com) + */ +// Do not add this to the NameResolverProvider service loader list +public class DiscoveryClientResolverFactory extends NameResolverProvider { + + /** + * The constant containing the scheme that will be used by this factory. + */ + public static final String DISCOVERY_SCHEME = "discovery"; + + private final Set discoveryClientNameResolvers = ConcurrentHashMap.newKeySet(); + private final HeartbeatMonitor monitor = new HeartbeatMonitor(); + + private final DiscoveryClient client; + + /** + * Creates a new discovery client based name resolver factory. + * + * @param client The client to use for the address discovery. + */ + public DiscoveryClientResolverFactory(final DiscoveryClient client) { + this.client = requireNonNull(client, "client"); + } + + @Nullable + @Override + public NameResolver newNameResolver(final URI targetUri, final NameResolver.Args args) { + if (DISCOVERY_SCHEME.equals(targetUri.getScheme())) { + final String serviceName = targetUri.getPath(); + if (serviceName == null || serviceName.length() <= 1 || !serviceName.startsWith("/")) { + throw new IllegalArgumentException("Incorrectly formatted target uri; " + + "expected: '" + DISCOVERY_SCHEME + ":[//]/'; " + + "but was '" + targetUri.toString() + "'"); + } + final AtomicReference reference = new AtomicReference<>(); + final DiscoveryClientNameResolver discoveryClientNameResolver = + new DiscoveryClientNameResolver(serviceName.substring(1), this.client, args, + GrpcUtil.SHARED_CHANNEL_EXECUTOR, + () -> this.discoveryClientNameResolvers.remove(reference.get())); + reference.set(discoveryClientNameResolver); + this.discoveryClientNameResolvers.add(discoveryClientNameResolver); + return discoveryClientNameResolver; + } + return null; + } + + @Override + public String getDefaultScheme() { + return DISCOVERY_SCHEME; + } + + @Override + protected boolean isAvailable() { + return true; + } + + @Override + protected int priority() { + return 6; // More important than DNS + } + + /** + * Triggers a refresh of the registered name resolvers. + * + * @param event The event that triggered the update. + */ + @EventListener(HeartbeatEvent.class) + public void heartbeat(final HeartbeatEvent event) { + if (this.monitor.update(event.getValue())) { + for (final DiscoveryClientNameResolver discoveryClientNameResolver : this.discoveryClientNameResolvers) { + discoveryClientNameResolver.refreshFromExternal(); + } + } + } + + /** + * Cleans up the name resolvers. + */ + @PreDestroy + public void destroy() { + this.discoveryClientNameResolvers.clear(); + } + + @Override + public String toString() { + return "DiscoveryClientResolverFactory [scheme=" + getDefaultScheme() + + ", discoveryClient=" + this.client + "]"; + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/NameResolverRegistration.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/NameResolverRegistration.java new file mode 100644 index 000000000..022644b03 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/NameResolverRegistration.java @@ -0,0 +1,79 @@ +/* + * 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.nameresolver; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.DisposableBean; + +import com.google.common.collect.ImmutableList; + +import io.grpc.NameResolverProvider; +import io.grpc.NameResolverRegistry; +import lombok.extern.slf4j.Slf4j; + +/** + * The NameResolverRegistration manages the registration and de-registration of Spring managed name resolvers. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +public class NameResolverRegistration implements DisposableBean { + + private final List registries = new ArrayList<>(1); + private final List providers; + + /** + * Creates a new NameResolverRegistration with the given list of providers. + * + * @param providers The providers that should be managed. + */ + public NameResolverRegistration(List providers) { + this.providers = providers == null ? ImmutableList.of() : ImmutableList.copyOf(providers); + } + + /** + * Register all NameResolverProviders in the given registry and store a reference to it for later de-registration. + * + * @param registry The registry to add the providers to. + */ + public void register(NameResolverRegistry registry) { + this.registries.add(registry); + for (NameResolverProvider provider : this.providers) { + try { + registry.register(provider); + log.debug("{} is available -> Added to the NameResolverRegistry", provider); + } catch (IllegalArgumentException e) { + log.debug("{} is not available -> Not added to the NameResolverRegistry", provider); + } + } + } + + @Override + public void destroy() { + for (NameResolverRegistry registry : this.registries) { + for (NameResolverProvider provider : this.providers) { + registry.deregister(provider); + log.debug("{} was removed from the NameResolverRegistry", provider); + } + } + this.registries.clear(); + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/StaticNameResolver.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/StaticNameResolver.java new file mode 100644 index 000000000..b2409a6bc --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/StaticNameResolver.java @@ -0,0 +1,99 @@ +/* + * 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.nameresolver; + +import static java.util.Objects.requireNonNull; + +import java.util.Collection; + +import com.google.common.collect.ImmutableList; + +import io.grpc.EquivalentAddressGroup; +import io.grpc.NameResolver; + +/** + * A {@link NameResolver} that will always respond with a static set of target addresses. + */ +public class StaticNameResolver extends NameResolver { + + private final String authority; + private final ResolutionResult result; + + /** + * Creates a static name resolver with only a single target server. + * + * @param authority The authority this name resolver was created for. + * @param target The target address of the server to use. + */ + public StaticNameResolver(final String authority, final EquivalentAddressGroup target) { + this(authority, ImmutableList.of(requireNonNull(target, "target"))); + } + + /** + * Creates a static name resolver with multiple target servers. + * + * @param authority The authority this name resolver was created for. + * @param targets The target addresses of the servers to use. + */ + public StaticNameResolver(final String authority, final Collection targets) { + this.authority = requireNonNull(authority, "authority"); + if (requireNonNull(targets, "targets").isEmpty()) { + throw new IllegalArgumentException("Must have at least one target"); + } + this.result = ResolutionResult.newBuilder() + .setAddresses(ImmutableList.copyOf(targets)) + .build(); + } + + /** + * Creates a static name resolver with multiple target servers. + * + * @param authority The authority this name resolver was created for. + * @param result The resolution result to use.. + */ + public StaticNameResolver(final String authority, final ResolutionResult result) { + this.authority = requireNonNull(authority, "authority"); + this.result = requireNonNull(result, "result"); + } + + @Override + public String getServiceAuthority() { + return this.authority; + } + + @Override + public void start(final Listener2 listener) { + listener.onResult(this.result); + } + + @Override + public void refresh() { + // Does nothing + } + + @Override + public void shutdown() { + // Does nothing + } + + @Override + public String toString() { + return "StaticNameResolver [authority=" + this.authority + ", result=" + this.result + "]"; + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/StaticNameResolverProvider.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/StaticNameResolverProvider.java new file mode 100644 index 000000000..db9b7b8a9 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/StaticNameResolverProvider.java @@ -0,0 +1,104 @@ +/* + * 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.nameresolver; + +import static java.util.Objects.requireNonNull; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import javax.annotation.Nullable; + +import io.grpc.EquivalentAddressGroup; +import io.grpc.NameResolver; +import io.grpc.NameResolverProvider; + +/** + * A name resolver provider that will create a {@link NameResolver} with static addresses. This factory uses the + * {@link #STATIC_SCHEME "static" scheme}. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public class StaticNameResolverProvider extends NameResolverProvider { + + /** + * The constant containing the scheme that will be used by this factory. + */ + public static final String STATIC_SCHEME = "static"; + + private static final Pattern PATTERN_COMMA = Pattern.compile(","); + + @Nullable + @Override + public NameResolver newNameResolver(final URI targetUri, final NameResolver.Args args) { + if (STATIC_SCHEME.equals(targetUri.getScheme())) { + return of(targetUri.getAuthority(), args.getDefaultPort()); + } + return null; + } + + /** + * Creates a new {@link NameResolver} for the given authority and attributes. + * + * @param targetAuthority The authority to connect to. + * @param defaultPort The default port to use, if none is specified. + * @return The newly created name resolver for the given target. + */ + private NameResolver of(final String targetAuthority, int defaultPort) { + requireNonNull(targetAuthority, "targetAuthority"); + // Determine target ips + final String[] hosts = PATTERN_COMMA.split(targetAuthority); + final List targets = new ArrayList<>(hosts.length); + for (final String host : hosts) { + final URI uri = URI.create("//" + host); + int port = uri.getPort(); + if (port == -1) { + port = defaultPort; + } + targets.add(new EquivalentAddressGroup(new InetSocketAddress(uri.getHost(), port))); + } + if (targets.isEmpty()) { + throw new IllegalArgumentException("Must have at least one target, but was: " + targetAuthority); + } + return new StaticNameResolver(targetAuthority, targets); + } + + @Override + public String getDefaultScheme() { + return STATIC_SCHEME; + } + + @Override + protected boolean isAvailable() { + return true; + } + + @Override + protected int priority() { + return 4; // Less important than DNS + } + + @Override + public String toString() { + return "StaticNameResolverProvider [scheme=" + getDefaultScheme() + "]"; + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/package-info.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/package-info.java new file mode 100644 index 000000000..e2cf4dee1 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/nameresolver/package-info.java @@ -0,0 +1,5 @@ +/** + * Classes used to resolve the client name into the actual service addresses. + */ + +package net.devh.boot.grpc.client.nameresolver; diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/security/CallCredentialsHelper.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/security/CallCredentialsHelper.java new file mode 100644 index 000000000..e3b95dfcf --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/security/CallCredentialsHelper.java @@ -0,0 +1,390 @@ +/* + * 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.security; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; +import static net.devh.boot.grpc.common.security.SecurityConstants.AUTHORIZATION_HEADER; +import static net.devh.boot.grpc.common.security.SecurityConstants.BASIC_AUTH_PREFIX; +import static net.devh.boot.grpc.common.security.SecurityConstants.BEARER_AUTH_PREFIX; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; +import java.util.concurrent.Executor; + +import javax.annotation.Nullable; + +import io.grpc.CallCredentials; +import io.grpc.Channel; +import io.grpc.Metadata; +import io.grpc.SecurityLevel; +import io.grpc.Status; +import io.grpc.stub.AbstractStub; +import net.devh.boot.grpc.client.autoconfigure.GrpcClientSecurityAutoConfiguration; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.client.inject.StubTransformer; +import net.devh.boot.grpc.common.security.SecurityConstants; + +/** + * Helper class with useful methods to create and configure some commonly used authentication schemes such as + * {@code Basic-Auth}. + * + *

+ * Note: If you have exactly one {@link CallCredentials} bean in your application context then it will be used + * for all {@link AbstractStub} that are annotation with {@link GrpcClient}. If you have none or multiple + * {@link CallCredentials} in the application context or use {@link Channel}s, then you have to configure the + * credentials yourself (See {@link GrpcClientSecurityAutoConfiguration}). + *

+ * + *

+ * Currently the following {@link CallCredentials} are supported by this class: + *

+ *
    + *
  • {@link #basicAuth(String, String) Basic-Auth}
  • + *
  • {@link #requirePrivacy(CallCredentials) Require privacy for the connection} (Wrapper)
  • + *
  • {@link #includeWhenPrivate(CallCredentials) Include credentials only if connection is private} (Wrapper)
  • + *
+ * + *

+ * Usage: + *

+ * + *
    + *
  • If you need only a single CallCredentials for all services, then it suffices to declare it as bean in your + * application context/configuration. + * + *
    + * @Bean
    + * CallCredentials myCallCredentials() {
    + *     return CallCredentialsHelper#basicAuth("user", "password")}
    + * }
    + * 
    + * + *
  • + *
  • If you need multiple/different CallCredentials for the services or only need them for a subset, then you should + * either add none of them or all of them (two ore more) to your application context to prevent the automatic credential + * selection. You can use a {@link StubTransformer} to select a CallCredential based on the client name instead. + * + *
    + * @Bean
    + * StubTransformer myCallCredentialsTransformer() {
    + *     return CallCredentialsHelper#mappedCredentialsStubTransformer(Map.of(
    + *         "myService1", basicAuth("user1", "password1"),
    + *         "theService2", basicAuth("foo", "bar"),
    + *         "publicApi", null // No credentials needed
    + *     ));
    + * }
    + * 
    + * + *
  • + *
  • If you need different CallCredentials for each call, then you have to define it in the method yourself. + * + *
    + * stub.withCallCredentials(CallCredentialsHelper#basicAuth("user", "password")).doStuff(request);
    + * 
    + * + *
  • + *
+ * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +// @ExperimentalApi("https://github.com/grpc/grpc-java/issues/1914") +// @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4901") +public class CallCredentialsHelper { + + /** + * Creates a new {@link StubTransformer} that will assign the given credentials to the given {@link AbstractStub}. + * + * @param credentials The call credentials to assign. + * @return The transformed stub. + * @see AbstractStub#withCallCredentials(CallCredentials) + */ + public static StubTransformer fixedCredentialsStubTransformer(final CallCredentials credentials) { + requireNonNull(credentials, "credentials"); + return (name, stub) -> stub.withCallCredentials(credentials); + } + + /** + * Creates a new {@link StubTransformer} that will assign credentials to the given {@link AbstractStub} based on the + * name. If the given map does not contain a value for the given name, then the call credentials will be omitted. + * + * @param credentialsByName The map that contains the call credentials. + * @return The transformed stub. + * @see #mappedCredentialsStubTransformer(Map, CallCredentials) + * @see AbstractStub#withCallCredentials(CallCredentials) + */ + public static StubTransformer mappedCredentialsStubTransformer( + final Map credentialsByName) { + return mappedCredentialsStubTransformer(credentialsByName, null); + } + + /** + * Creates a new {@link StubTransformer} that will assign credentials to the given {@link AbstractStub} based on the + * name. If the given map does not contain a value for the given name, then the optional fallback will be used + * otherwise the call credentials will be omitted. + * + * @param credentialsByName The map that contains the call credentials. + * @param fallback The optional fallback to use. + * @return The transformed stub. + * @see AbstractStub#withCallCredentials(CallCredentials) + */ + public static StubTransformer mappedCredentialsStubTransformer( + final Map credentialsByName, + @Nullable final CallCredentials fallback) { + requireNonNull(credentialsByName, "credentials"); + return (name, stub) -> { + final CallCredentials credentials = credentialsByName.getOrDefault(name, fallback); + if (credentials == null) { + return stub; + } else { + return stub.withCallCredentials(credentials); + } + }; + } + + /** + * Creates new call credentials with the given token for bearer auth. + * + *

+ * Note: This method uses experimental grpc-java-API features. + *

+ * + * @param token the bearer token to use + * @return The newly created bearer auth credentials. + * @see SecurityConstants#BEARER_AUTH_PREFIX + * @see #authorizationHeader(String) + */ + public static CallCredentials bearerAuth(final String token) { + return authorizationHeader(BEARER_AUTH_PREFIX + token); + } + + /** + * Creates new call credentials with the given username and password for basic auth. + * + *

+ * Note: This method uses experimental grpc-java-API features. + *

+ * + * @param username The username to use. + * @param password The password to use. + * @return The newly created basic auth credentials. + * @see SecurityConstants#BASIC_AUTH_PREFIX + * @see #encodeBasicAuth(String, String) + * @see #authorizationHeader(String) + */ + public static CallCredentials basicAuth(final String username, final String password) { + return authorizationHeader(encodeBasicAuth(username, password)); + } + + /** + * Encodes the given username and password as basic auth. The header value will be encoded with + * {@link StandardCharsets#UTF_8 UTF_8}. + * + * @param username The username to use. + * @param password The password to use. + * @return The encoded basic auth header value. + * @see SecurityConstants#BASIC_AUTH_PREFIX + */ + public static String encodeBasicAuth(final String username, final String password) { + requireNonNull(username, "username"); + requireNonNull(password, "password"); + final String auth = username + ':' + password; + byte[] encoded; + try { + encoded = Base64.getEncoder().encode(auth.getBytes(UTF_8)); + } catch (final IllegalArgumentException e) { + throw new IllegalArgumentException("Failed to encode basic authentication token", e); + } + return BASIC_AUTH_PREFIX + new String(encoded, UTF_8); + } + + /** + * Creates new call credentials with the given static authorization information. + * + *

+ * Note: This method uses experimental grpc-java-API features. + *

+ * + * @param authorization The authorization to use. The authorization usually starts with the scheme such as as + * {@code "Basic "} or {@code "Bearer "} followed by the actual authentication information. + * @return The newly created call credentials. + * @see SecurityConstants#AUTHORIZATION_HEADER + * @see #authorizationHeaders(Metadata) + */ + public static CallCredentials authorizationHeader(final String authorization) { + requireNonNull(authorization); + final Metadata extraHeaders = new Metadata(); + extraHeaders.put(AUTHORIZATION_HEADER, authorization); + return authorizationHeaders(extraHeaders); + } + + /** + * Creates new call credentials with the given static authorization headers. + * + * @param authorizationHeaders The authorization headers to use. + * @return The newly created call credentials. + */ + public static CallCredentials authorizationHeaders(final Metadata authorizationHeaders) { + return new StaticSecurityHeaderCallCredentials(requireNonNull(authorizationHeaders)); + } + + /** + * The static security header {@link CallCredentials} simply add a set of predefined headers to the call. Their + * specific meaning is server specific. This implementation can be used, for example, for BasicAuth. + */ + private static final class StaticSecurityHeaderCallCredentials extends CallCredentials { + + private final Metadata extraHeaders; + + StaticSecurityHeaderCallCredentials(final Metadata extraHeaders) { + this.extraHeaders = requireNonNull(extraHeaders, "extraHeaders"); + } + + @Override + public void applyRequestMetadata(final RequestInfo requestInfo, final Executor appExecutor, + final MetadataApplier applier) { + applier.apply(this.extraHeaders); + } + + @Override + public void thisUsesUnstableApi() {} // API evolution in progress + + @Override + public String toString() { + return "StaticSecurityHeaderCallCredentials [extraHeaders.keys=" + this.extraHeaders.keys() + "]"; + } + + } + + /** + * Checks whether the given security level provides privacy for all data being send on the connection. + * + *

+ * Note: This method uses experimental grpc-java-API features. + *

+ * + * @param securityLevel The security level to check. + * @return True, if and only if the given security level ensures privacy. False otherwise. + */ + public static boolean isPrivacyGuaranteed(final SecurityLevel securityLevel) { + return SecurityLevel.PRIVACY_AND_INTEGRITY == securityLevel; + } + + /** + * Wraps the given call credentials in a new layer, which ensures that the credentials are only send, if the + * connection guarantees privacy. If the connection doesn't do that, the call will be aborted before sending any + * data. + * + *

+ * Note: This method uses experimental grpc-java-API features. + *

+ * + * @param callCredentials The call credentials to wrap. + * @return The newly created call credentials. + */ + public static CallCredentials requirePrivacy(final CallCredentials callCredentials) { + return new RequirePrivacyCallCredentials(callCredentials); + } + + /** + * A call credentials implementation with slightly increased security requirements. It ensures that the credentials + * aren't send via an insecure connection. However, it does not prevent requests via insecure connections. This + * wrapper does not have any other influence on the security of the underlying {@link CallCredentials} + * implementation. + */ + private static final class RequirePrivacyCallCredentials extends CallCredentials { + + private static final Status STATUS_LACKING_PRIVACY = Status.UNAUTHENTICATED + .withDescription("Connection security level does not ensure credential privacy"); + + private final CallCredentials callCredentials; + + RequirePrivacyCallCredentials(final CallCredentials callCredentials) { + this.callCredentials = callCredentials; + } + + @Override + public void applyRequestMetadata(final RequestInfo requestInfo, final Executor appExecutor, + final MetadataApplier applier) { + if (isPrivacyGuaranteed(requestInfo.getSecurityLevel())) { + this.callCredentials.applyRequestMetadata(requestInfo, appExecutor, applier); + } else { + applier.fail(STATUS_LACKING_PRIVACY); + } + } + + @Override + public void thisUsesUnstableApi() {} // API evolution in progress + + @Override + public String toString() { + return "RequirePrivacyCallCredentials [callCredentials=" + this.callCredentials + "]"; + } + + } + + /** + * Wraps the given call credentials in a new layer, that will only include the credentials if the connection + * guarantees privacy. If the connection doesn't do that, the call will continue without the credentials. + * + *

+ * Note: This method uses experimental grpc-java-API features. + *

+ * + * @param callCredentials The call credentials to wrap. + * @return The newly created call credentials. + */ + public static CallCredentials includeWhenPrivate(final CallCredentials callCredentials) { + return new IncludeWhenPrivateCallCredentials(callCredentials); + } + + /** + * A call credentials implementation with increased security requirements. It ensures that the credentials and + * requests aren't send via an insecure connection. This wrapper does not have any other influence on the security + * of the underlying {@link CallCredentials} implementation. + */ + private static final class IncludeWhenPrivateCallCredentials extends CallCredentials { + + private final CallCredentials callCredentials; + + IncludeWhenPrivateCallCredentials(final CallCredentials callCredentials) { + this.callCredentials = callCredentials; + } + + @Override + public void applyRequestMetadata(final RequestInfo requestInfo, final Executor appExecutor, + final MetadataApplier applier) { + if (isPrivacyGuaranteed(requestInfo.getSecurityLevel())) { + this.callCredentials.applyRequestMetadata(requestInfo, appExecutor, applier); + } + } + + @Override + public void thisUsesUnstableApi() {} // API evolution in progress + + @Override + public String toString() { + return "IncludeWhenPrivateCallCredentials [callCredentials=" + this.callCredentials + "]"; + } + + } + + private CallCredentialsHelper() {} + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/security/package-info.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/security/package-info.java new file mode 100644 index 000000000..38f9ca24a --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/security/package-info.java @@ -0,0 +1,10 @@ +/** + * Contains classes and utilities that help with the user authentication. + * + *
+ */ + +package net.devh.boot.grpc.client.security; diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/AsyncStubFactory.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/AsyncStubFactory.java new file mode 100644 index 000000000..b8497c1fd --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/AsyncStubFactory.java @@ -0,0 +1,34 @@ +/* + * 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.stubfactory; + +import io.grpc.stub.AbstractAsyncStub; +import io.grpc.stub.AbstractStub; + +public class AsyncStubFactory extends StandardJavaGrpcStubFactory { + + @Override + public boolean isApplicable(Class> stubType) { + return AbstractAsyncStub.class.isAssignableFrom(stubType); + } + + @Override + protected String getFactoryMethodName() { + return "newStub"; + } +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/BlockingStubFactory.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/BlockingStubFactory.java new file mode 100644 index 000000000..9e615be5a --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/BlockingStubFactory.java @@ -0,0 +1,34 @@ +/* + * 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.stubfactory; + +import io.grpc.stub.AbstractBlockingStub; +import io.grpc.stub.AbstractStub; + +public class BlockingStubFactory extends StandardJavaGrpcStubFactory { + + @Override + public boolean isApplicable(Class> stubType) { + return AbstractBlockingStub.class.isAssignableFrom(stubType); + } + + @Override + protected String getFactoryMethodName() { + return "newBlockingStub"; + } +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/FallbackStubFactory.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/FallbackStubFactory.java new file mode 100644 index 000000000..2fd50d5f3 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/FallbackStubFactory.java @@ -0,0 +1,72 @@ +/* + * 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.stubfactory; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.Parameter; + +import org.springframework.beans.BeanInstantiationException; + +import io.grpc.Channel; +import io.grpc.stub.AbstractStub; + +/** + * The StubFactory which tries to find a suitable factory method or constructor as a last resort. This factory will + * always be the last one that is attempted. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public final class FallbackStubFactory implements StubFactory { + + @Override + public boolean isApplicable(final Class> stubType) { + return true; + } + + @Override + public AbstractStub createStub(final Class> stubType, final Channel channel) { + try { + // Search for public static *Grpc#new*Stub(Channel) + final Class declaringClass = stubType.getDeclaringClass(); + if (declaringClass != null) { + for (final Method method : declaringClass.getMethods()) { + final String name = method.getName(); + final int modifiers = method.getModifiers(); + final Parameter[] parameters = method.getParameters(); + if (name.startsWith("new") && name.endsWith("Stub") + && Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers) + && method.getReturnType().isAssignableFrom(stubType) + && parameters.length == 1 + && Channel.class.equals(parameters[0].getType())) { + return AbstractStub.class.cast(method.invoke(null, channel)); + } + } + } + + // Search for a public constructor *Stub(Channel) + final Constructor> constructor = stubType.getConstructor(Channel.class); + return constructor.newInstance(channel); + + } catch (final Exception e) { + throw new BeanInstantiationException(stubType, "Failed to create gRPC client via FallbackStubFactory", e); + } + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/FutureStubFactory.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/FutureStubFactory.java new file mode 100644 index 000000000..4f40f6b20 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/FutureStubFactory.java @@ -0,0 +1,34 @@ +/* + * 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.stubfactory; + +import io.grpc.stub.AbstractFutureStub; +import io.grpc.stub.AbstractStub; + +public class FutureStubFactory extends StandardJavaGrpcStubFactory { + + @Override + public boolean isApplicable(Class> stubType) { + return AbstractFutureStub.class.isAssignableFrom(stubType); + } + + @Override + protected String getFactoryMethodName() { + return "newFutureStub"; + } +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/StandardJavaGrpcStubFactory.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/StandardJavaGrpcStubFactory.java new file mode 100644 index 000000000..8fae00cdb --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/StandardJavaGrpcStubFactory.java @@ -0,0 +1,52 @@ +/* + * 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.stubfactory; + +import java.lang.reflect.Method; + +import org.springframework.beans.BeanInstantiationException; + +import io.grpc.Channel; +import io.grpc.stub.AbstractStub; + +/** + * A factory for creating stubs provided by standard grpc Java library. This is an abstract super-type that can be + * extended to support the different provided types. + */ +public abstract class StandardJavaGrpcStubFactory implements StubFactory { + + @Override + public AbstractStub createStub(final Class> stubType, final Channel channel) { + try { + // Use the public static factory method + final String methodName = getFactoryMethodName(); + final Class enclosingClass = stubType.getEnclosingClass(); + final Method factoryMethod = enclosingClass.getMethod(methodName, Channel.class); + return stubType.cast(factoryMethod.invoke(null, channel)); + } catch (final Exception e) { + throw new BeanInstantiationException(stubType, "Failed to create gRPC client", e); + } + } + + /** + * Derives the name of the factory method from the given stub type. + * + * @return The name of the factory method. + */ + protected abstract String getFactoryMethodName(); +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/StubFactory.java b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/StubFactory.java new file mode 100644 index 000000000..58ff38e5c --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/client/stubfactory/StubFactory.java @@ -0,0 +1,51 @@ +/* + * 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.stubfactory; + +import org.springframework.beans.BeanInstantiationException; + +import io.grpc.Channel; +import io.grpc.stub.AbstractStub; + +/** + * A factory for gRPC stubs. This is an extension mechanism for supporting different types of gRPC compiled stubs in + * addition to the standard Java compiled gRPC. + * + * Spring beans implementing this type will be picked up automatically and added to the list of supported types. + */ +public interface StubFactory { + + /** + * Creates a stub of the given type. + * + * @param stubType The type of the stub to create. + * @param channel The channel used to create the stub. + * @return The newly created stub. + * + * @throws BeanInstantiationException If the stub couldn't be created. + */ + AbstractStub createStub(Class> stubType, Channel channel); + + /** + * Used to resolve a factory that matches the particular stub type. + * + * @param stubType The type of the stub that needs to be created. + * @return True if this particular factory is capable of creating instances of this stub type. False otherwise. + */ + boolean isApplicable(Class> stubType); +} diff --git a/grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 000000000..c58150d25 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,161 @@ +{ + "groups": [ + { + "name": "grpc", + "type": "net.devh.boot.grpc.client.config.GrpcChannelsProperties", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelsProperties", + "description": "The container for grpc related configuration options." + }, + { + "name": "grpc.client", + "type": "java.util.Map", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelsProperties", + "sourceMethod": "getClient()", + "description": "A container for named channel properties.\nEach channel has its own configuration. If you try to get a channel that does not have a configuration yet, it will be created. If something is not configured in the channel properties, it will be copied from the global config during the first retrieval. If some property is configured in neither the channel properties nor the global properties then a default value will be used." + }, + { + "name": "grpc.client.GLOBAL", + "type": "net.devh.boot.grpc.client.config.GrpcChannelProperties", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties", + "description": "The channel properties for a single named gRPC channel or service reference. In this case the GLOBAL channel." + }, + { + "name": "grpc.client.GLOBAL.security", + "type": "net.devh.boot.grpc.client.config.GrpcChannelProperties$Security", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties", + "sourceMethod": "getSecurity()", + "description": "A container with options for the channel's transport security. In this case the GLOBAL channel's ones." + } + ], + "properties": [ + { + "name": "grpc.client", + "type": "java.util.Map", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelsProperties" + }, + { + "name": "grpc.client.GLOBAL.address", + "type": "java.net.URI", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties", + "description": "The target address uri for the channel.\nThe target uri must be in the format: schema:[\/\/[authority]][\/path].\nIf nothing is configured then the io.grpc.NameResolver.Factory will decide on the default.\nExamples:\n\t\"static:\/\/10.0.0.10:9090,10.11.12.11:9091\",\n\t\"dns:\/example.com:9090\",\n\t\"discovery:\/foo-service\"" + }, + { + "name": "grpc.client.GLOBAL.default-load-balancing-policy", + "type": "java.lang.String", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties", + "description": "The default load balancing policy this channel should use.", + "defaultValue": "round_robin" + }, + { + "name": "grpc.client.GLOBAL.enable-keep-alive", + "type": "java.lang.Boolean", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties", + "description": "Whether keepAlive should be enabled.", + "defaultValue": false + }, + { + "name": "grpc.client.GLOBAL.full-stream-decompression", + "type": "java.lang.Boolean", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties", + "description": "Whether full-stream decompression of inbound streams should be enabled.", + "defaultValue": false + }, + { + "name": "grpc.client.GLOBAL.keep-alive-time", + "type": "java.time.Duration", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties", + "description": "The default delay before we send a keepAlive.\nDefault unit is seconds.", + "defaultValue": "60s" + }, + { + "name": "grpc.client.GLOBAL.keep-alive-timeout", + "type": "java.time.Duration", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties", + "description": "The default timeout for a keepAlives ping request.\nDefault unit is seconds.", + "defaultValue": "20s" + }, + { + "name": "grpc.client.GLOBAL.keep-alive-without-calls", + "type": "java.lang.Boolean", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties", + "description": "Whether keepAlive will be performed when there are no outstanding RPC on a connection.", + "defaultValue": false + }, + { + "name": "grpc.client.GLOBAL.shutdown-grace-period", + "type": "java.time.Duration", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties", + "description": "The time to wait for the channel to gracefully shutdown (completing all requests).\nIf set to a negative value, the channel waits forever.\nIf set to 0 the channel will force shutdown immediately.\nDefaults to 30s.", + "defaultValue": "30s" + }, + { + "name": "grpc.client.GLOBAL.max-inbound-message-size", + "type": "org.springframework.util.unit.DataSize", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties", + "description": "The maximum message size allowed to be received by the channel.\nIf not set (null) then it will default to gRPC's default.\nIf set to -1 then it will use the highest possible limit (not recommended)." + }, + { + "name": "grpc.client.GLOBAL.negotiation-type", + "type": "net.devh.boot.grpc.client.config.NegotiationType", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties", + "description": "The negotiation type to use on the connection.", + "defaultValue": "TLS" + }, + { + "name": "grpc.client.GLOBAL.immediate-connect-timeout", + "type": "java.time.Duration", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties", + "description": "Connection timeout at application startup. If set to a positive duration instructs a client to connect to GRPC-endpoint when GRPC stub is created.", + "defaultValue": 0 + }, + { + "name": "grpc.client.GLOBAL.security.authority-override", + "type": "java.lang.String", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties$Security", + "description": "The authority to check for during server certificate verification. By default the clients will use the name of the client to check the server certificate's common + alternative names." + }, + { + "name": "grpc.client.GLOBAL.security.certificate-chain", + "type": "org.springframework.core.io.Resource", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties$Security", + "description": "The path to SSL certificate chain. Required if \"client-auth-enabled\" is enabled.\nThe linked certificate will be used to authenticate the client." + }, + { + "name": "grpc.client.GLOBAL.security.ciphers", + "type": "java.util.List", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties$Security", + "description": "The cipher suite accepted for secure connections (in the order of preference). If not specified (null), then the default suites will be used." + }, + { + "name": "grpc.client.GLOBAL.security.client-auth-enabled", + "type": "java.lang.Boolean", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties$Security", + "description": "Whether client can authenticate using certificates.", + "defaultValue": false + }, + { + "name": "grpc.client.GLOBAL.security.private-key", + "type": "org.springframework.core.io.Resource", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties$Security", + "description": "The path to the private key. Required if \"client-auth-enabled\" is enabled." + }, + { + "name": "grpc.client.GLOBAL.security.private-key-password", + "type": "java.lang.String", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties$Security", + "description": "The password for the private key." + }, + { + "name": "grpc.client.GLOBAL.security.protocols", + "type": "java.lang.String", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties$Security", + "description": "The TLS protocols accepted for secure connections. If not specified (null), then the default ones will be used." + }, + { + "name": "grpc.client.GLOBAL.security.trust-cert-collection", + "type": "org.springframework.core.io.Resource", + "sourceType": "net.devh.boot.grpc.client.config.GrpcChannelProperties$Security", + "description": "The path to the trusted certificate collection.\nIf not set (null) it will use the system's default collection (Default).\nThis collection will be used to verify server certificates." + } + ] +} \ No newline at end of file diff --git a/grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/services/io.grpc.NameResolverProvider b/grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/services/io.grpc.NameResolverProvider new file mode 100644 index 000000000..218836246 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/services/io.grpc.NameResolverProvider @@ -0,0 +1 @@ +net.devh.boot.grpc.client.nameresolver.StaticNameResolverProvider \ No newline at end of file diff --git a/grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..1eb576df7 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -0,0 +1,8 @@ +# AutoConfiguration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +net.devh.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration,\ +net.devh.boot.grpc.client.autoconfigure.GrpcClientMetricAutoConfiguration,\ +net.devh.boot.grpc.client.autoconfigure.GrpcClientHealthAutoConfiguration,\ +net.devh.boot.grpc.client.autoconfigure.GrpcClientSecurityAutoConfiguration,\ +net.devh.boot.grpc.client.autoconfigure.GrpcClientTraceAutoConfiguration,\ +net.devh.boot.grpc.client.autoconfigure.GrpcDiscoveryClientAutoConfiguration diff --git a/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesConfig.java b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesConfig.java new file mode 100644 index 000000000..869196245 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesConfig.java @@ -0,0 +1,29 @@ +/* + * 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.config; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * Dummy config - because Spring needs one. + */ +@SpringBootConfiguration +@EnableConfigurationProperties(GrpcChannelsProperties.class) +public class GrpcChannelPropertiesConfig { +} diff --git a/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGivenUnitTest.java b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGivenUnitTest.java new file mode 100644 index 000000000..cc1d1fe02 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGivenUnitTest.java @@ -0,0 +1,51 @@ +/* + * 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.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.util.unit.DataSize; + +/** + * Tests whether the property resolution works when using suffixes. + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = { + "grpc.client.test.keepAliveTime=42m", + "grpc.client.test.maxInboundMessageSize=5MB" +}) +class GrpcChannelPropertiesGivenUnitTest { + + @Autowired + private GrpcChannelsProperties grpcChannelsProperties; + + @Test + void test() { + final GrpcChannelProperties properties = this.grpcChannelsProperties.getChannel("test"); + assertEquals(Duration.ofMinutes(42), properties.getKeepAliveTime()); + assertEquals(DataSize.ofMegabytes(5), properties.getMaxInboundMessageSize()); + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGlobalTest.java b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGlobalTest.java new file mode 100644 index 000000000..e19bc7e02 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesGlobalTest.java @@ -0,0 +1,56 @@ +/* + * 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.config; + +import static org.junit.Assert.assertSame; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * Tests whether the global property fallback works. + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = { + "grpc.client.GLOBAL.keepAliveTime=23m", + "grpc.client.GLOBAL.keepAliveTimeout=31s", + "grpc.client.test.keepAliveTime=42m"}) +class GrpcChannelPropertiesGlobalTest { + + @Autowired + private GrpcChannelsProperties grpcChannelsProperties; + + @Test + void test() { + assertSame(this.grpcChannelsProperties.getGlobalChannel(), + this.grpcChannelsProperties.getChannel(GrpcChannelsProperties.GLOBAL_PROPERTIES_KEY)); + + assertEquals(Duration.ofMinutes(23), this.grpcChannelsProperties.getGlobalChannel().getKeepAliveTime()); + assertEquals(Duration.ofSeconds(31), this.grpcChannelsProperties.getGlobalChannel().getKeepAliveTimeout()); + + assertEquals(Duration.ofMinutes(42), this.grpcChannelsProperties.getChannel("test").getKeepAliveTime()); + assertEquals(Duration.ofSeconds(31), this.grpcChannelsProperties.getChannel("test").getKeepAliveTimeout()); + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesNegativeGivenUnitTest.java b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesNegativeGivenUnitTest.java new file mode 100644 index 000000000..ef821a488 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesNegativeGivenUnitTest.java @@ -0,0 +1,48 @@ +/* + * 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.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * Tests whether the property resolution works with negative values and when using suffixes. + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = { + "grpc.client.test.shutdownGracePeriod=-1ms" +}) +class GrpcChannelPropertiesNegativeGivenUnitTest { + + @Autowired + private GrpcChannelsProperties grpcChannelsProperties; + + @Test + void test() { + final GrpcChannelProperties properties = this.grpcChannelsProperties.getChannel("test"); + assertEquals(Duration.ofMillis(-1), properties.getShutdownGracePeriod()); + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesNegativeNoUnitTest.java b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesNegativeNoUnitTest.java new file mode 100644 index 000000000..c158e3b87 --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesNegativeNoUnitTest.java @@ -0,0 +1,48 @@ +/* + * 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.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * Tests whether the property resolution works with negative values and without suffixes. + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = { + "grpc.client.test.shutdownGracePeriod=-1" +}) +class GrpcChannelPropertiesNegativeNoUnitTest { + + @Autowired + private GrpcChannelsProperties grpcChannelsProperties; + + @Test + void test() { + final GrpcChannelProperties properties = this.grpcChannelsProperties.getChannel("test"); + assertEquals(Duration.ofSeconds(-1), properties.getShutdownGracePeriod()); + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesNoUnitTest.java b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesNoUnitTest.java new file mode 100644 index 000000000..8181057ea --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/config/GrpcChannelPropertiesNoUnitTest.java @@ -0,0 +1,51 @@ +/* + * 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.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.util.unit.DataSize; + +/** + * Tests whether the property resolution works without suffixes (backwards compatibility). + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = { + "grpc.client.test.keepAliveTime=42", + "grpc.client.test.maxInboundMessageSize=5242880" +}) +class GrpcChannelPropertiesNoUnitTest { + + @Autowired + private GrpcChannelsProperties grpcChannelsProperties; + + @Test + void test() { + final GrpcChannelProperties properties = this.grpcChannelsProperties.getChannel("test"); + assertEquals(Duration.ofSeconds(42), properties.getKeepAliveTime()); + assertEquals(DataSize.ofMegabytes(5), properties.getMaxInboundMessageSize()); + } + +} diff --git a/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/inject/GrpcClientBeanPostProcessorTest.java b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/inject/GrpcClientBeanPostProcessorTest.java new file mode 100644 index 000000000..5600b9e3c --- /dev/null +++ b/grpc-client-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/client/inject/GrpcClientBeanPostProcessorTest.java @@ -0,0 +1,160 @@ +/* + * 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 static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import java.lang.annotation.Annotation; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.test.annotation.DirtiesContext; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; +import net.devh.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration; + +/** + * Tests for {@link GrpcClientBeanPostProcessor}. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@SpringBootTest(classes = GrpcClientBeanPostProcessorTest.TestConfig.class) +@DirtiesContext +@ImportAutoConfiguration(GrpcClientAutoConfiguration.class) +class GrpcClientBeanPostProcessorTest { + + @Autowired + GrpcClientBeanPostProcessor postProcessor; + + @Autowired + Interceptor1 interceptor1; + + @Test + void testInterceptorsFromAnnotation1() { + final List beans = + assertDoesNotThrow(() -> this.postProcessor.interceptorsFromAnnotation(new GrpcClient() { + + @Override + public Class annotationType() { + return GrpcClient.class; + } + + @Override + public String value() { + return "test"; + } + + @Override + public boolean sortInterceptors() { + return false; + } + + @Override + @SuppressWarnings("unchecked") + public Class[] interceptors() { + return new Class[] {Interceptor1.class}; + } + + @Override + public String[] interceptorNames() { + return new String[0]; + } + + })); + + assertThat(beans).containsExactly(this.interceptor1); + } + + @Test + void testInterceptorsFromAnnotation2() { + final List beans = + assertDoesNotThrow(() -> this.postProcessor.interceptorsFromAnnotation(new GrpcClient() { + + @Override + public Class annotationType() { + return GrpcClient.class; + } + + @Override + public String value() { + return "test"; + } + + @Override + public boolean sortInterceptors() { + return false; + } + + @Override + @SuppressWarnings("unchecked") + public Class[] interceptors() { + return new Class[] {Interceptor2.class}; + } + + @Override + public String[] interceptorNames() { + return new String[0]; + } + + })); + + assertThat(beans).hasSize(1).doesNotContain(this.interceptor1); + } + + static class TestConfig { + + @Bean + Interceptor1 interceptor1() { + return new Interceptor1(); + } + + + } + + public static class Interceptor1 implements ClientInterceptor { + + @Override + public ClientCall interceptCall( + final MethodDescriptor method, + final CallOptions callOptions, final Channel next) { + return next.newCall(method, callOptions); + } + + } + + public static class Interceptor2 implements ClientInterceptor { + + @Override + public ClientCall interceptCall( + final MethodDescriptor method, + final CallOptions callOptions, final Channel next) { + return next.newCall(method, callOptions); + } + + } + +} diff --git a/grpc-client-spring-boot-starter/build.gradle b/grpc-client-spring-boot-starter/build.gradle new file mode 100644 index 000000000..2098c4b39 --- /dev/null +++ b/grpc-client-spring-boot-starter/build.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java-library' +} + +apply from: '../deploy.gradle' + +group = 'net.devh' +version = projectVersion + +dependencies { + api project(':grpc-client-spring-boot-autoconfigure') +} diff --git a/grpc-common-spring-boot/build.gradle b/grpc-common-spring-boot/build.gradle new file mode 100644 index 000000000..a4aa19292 --- /dev/null +++ b/grpc-common-spring-boot/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'java-library' +} + +apply from: '../deploy.gradle' + +group = 'net.devh' +version = projectVersion + +compileJava.dependsOn(processResources) + +dependencies { + annotationProcessor('org.springframework.boot:spring-boot-autoconfigure-processor') + + api('org.springframework.boot:spring-boot-starter') + optionalSupportImplementation('org.springframework.boot:spring-boot-starter-actuator') + api('io.grpc:grpc-core') + optionalSupportImplementation('com.google.guava:guava') + + optionalSupportImplementation('org.springframework.cloud:spring-cloud-starter-sleuth') + optionalSupportImplementation('io.zipkin.brave:brave-instrumentation-grpc') +} diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/autoconfigure/GrpcCommonCodecAutoConfiguration.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/autoconfigure/GrpcCommonCodecAutoConfiguration.java new file mode 100644 index 000000000..a106f30b3 --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/autoconfigure/GrpcCommonCodecAutoConfiguration.java @@ -0,0 +1,84 @@ +/* + * 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.common.autoconfigure; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.grpc.Codec; +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.common.codec.AnnotationGrpcCodecDiscoverer; +import net.devh.boot.grpc.common.codec.GrpcCodecDefinition; +import net.devh.boot.grpc.common.codec.GrpcCodecDiscoverer; + +/** + * The auto configuration used by Spring-Boot that contains all codec related beans for clients/servers. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(Codec.class) +public class GrpcCommonCodecAutoConfiguration { + + @ConditionalOnMissingBean + @Bean + public GrpcCodecDiscoverer defaultGrpcCodecDiscoverer() { + return new AnnotationGrpcCodecDiscoverer(); + } + + @ConditionalOnBean(GrpcCodecDiscoverer.class) + @ConditionalOnMissingBean + @Bean + public CompressorRegistry defaultCompressorRegistry(final GrpcCodecDiscoverer codecDiscoverer) { + log.debug("Found GrpcCodecDiscoverer -> Creating custom CompressorRegistry"); + final CompressorRegistry registry = CompressorRegistry.getDefaultInstance(); + for (final GrpcCodecDefinition definition : codecDiscoverer.findGrpcCodecs()) { + if (definition.getCodecType().isForCompression()) { + final Codec codec = definition.getCodec(); + log.debug("Registering compressor: '{}' ({})", codec.getMessageEncoding(), codec.getClass().getName()); + registry.register(codec); + } + } + return registry; + } + + @ConditionalOnBean(GrpcCodecDiscoverer.class) + @ConditionalOnMissingBean + @Bean + public DecompressorRegistry defaultDecompressorRegistry(final GrpcCodecDiscoverer codecDiscoverer) { + log.debug("Found GrpcCodecDiscoverer -> Creating custom DecompressorRegistry"); + DecompressorRegistry registry = DecompressorRegistry.getDefaultInstance(); + for (final GrpcCodecDefinition definition : codecDiscoverer.findGrpcCodecs()) { + if (definition.getCodecType().isForDecompression()) { + final Codec codec = definition.getCodec(); + final boolean isAdvertised = definition.isAdvertised(); + log.debug("Registering {} decompressor: '{}' ({})", + isAdvertised ? "advertised" : "", codec.getMessageEncoding(), codec.getClass().getName()); + registry = registry.with(codec, isAdvertised); + } + } + return registry; + } + +} diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/autoconfigure/GrpcCommonTraceAutoConfiguration.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/autoconfigure/GrpcCommonTraceAutoConfiguration.java new file mode 100644 index 000000000..ce64eafa3 --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/autoconfigure/GrpcCommonTraceAutoConfiguration.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.common.autoconfigure; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.sleuth.autoconfig.brave.BraveAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import brave.Tracing; +import brave.grpc.GrpcTracing; + +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(value = "spring.sleuth.grpc.enabled", matchIfMissing = true) +@AutoConfigureAfter(BraveAutoConfiguration.class) +@ConditionalOnClass(value = {Tracing.class, GrpcTracing.class}) +public class GrpcCommonTraceAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public GrpcTracing grpcTracing(final Tracing tracing) { + return GrpcTracing.create(tracing); + } + +} diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/autoconfigure/package-info.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/autoconfigure/package-info.java new file mode 100644 index 000000000..a04f67150 --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/autoconfigure/package-info.java @@ -0,0 +1,6 @@ +/** + * The Spring-Boot auto configuration classes that setup the gRPC environment with features that can be used by both the + * client and the server. + */ + +package net.devh.boot.grpc.common.autoconfigure; diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/AnnotationGrpcCodecDiscoverer.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/AnnotationGrpcCodecDiscoverer.java new file mode 100644 index 000000000..70b455528 --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/AnnotationGrpcCodecDiscoverer.java @@ -0,0 +1,65 @@ +/* + * 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.common.codec; + +import java.util.Collection; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +import com.google.common.collect.ImmutableList; + +import io.grpc.Codec; +import lombok.extern.slf4j.Slf4j; + +/** + * A {@link GrpcCodecDiscoverer} that searches for beans with the {@link GrpcCodec} annotations. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +public class AnnotationGrpcCodecDiscoverer implements ApplicationContextAware, GrpcCodecDiscoverer { + + private ApplicationContext applicationContext; + private Collection definitions; + + @Override + public void setApplicationContext(final ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public Collection findGrpcCodecs() { + if (this.definitions == null) { + log.debug("Searching for codecs..."); + final String[] beanNames = this.applicationContext.getBeanNamesForAnnotation(GrpcCodec.class); + final ImmutableList.Builder builder = ImmutableList.builder(); + for (final String beanName : beanNames) { + final Codec codec = this.applicationContext.getBean(beanName, Codec.class); + final GrpcCodec annotation = this.applicationContext.findAnnotationOnBean(beanName, GrpcCodec.class); + builder.add(new GrpcCodecDefinition(codec, annotation.advertised(), annotation.codecType())); + log.debug("Found gRPC codec: {}, bean: {}, class: {}", + codec.getMessageEncoding(), beanName, codec.getClass().getName()); + } + this.definitions = builder.build(); + log.debug("Done"); + } + return this.definitions; + } + +} diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/CodecType.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/CodecType.java new file mode 100644 index 000000000..d994ceb19 --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/CodecType.java @@ -0,0 +1,69 @@ +/* + * 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.common.codec; + +/** + * The type of the codec. + * + * @author Michael (yidongnan@gmail.com) + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public enum CodecType { + + /** + * The codec should be used for compression only. + */ + COMPRESS(true, false), + + /** + * The codec should be used for decompression only. + */ + DECOMPRESS(false, true), + + /** + * The codec should be used for both compression and decompression. + */ + ALL(true, true); + + private final boolean forCompression; + private final boolean forDecompression; + + private CodecType(final boolean forCompression, final boolean forDecompression) { + this.forCompression = forCompression; + this.forDecompression = forDecompression; + } + + /** + * Whether the associated codec should be used for compression. + * + * @return True, if the codec can be used for compression. False otherwise. + */ + public boolean isForCompression() { + return this.forCompression; + } + + /** + * Whether the associated codec should be used for decompression. + * + * @return True, if the codec can be used for decompression. False otherwise. + */ + public boolean isForDecompression() { + return this.forDecompression; + } + +} diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/GrpcCodec.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/GrpcCodec.java new file mode 100644 index 000000000..0f38be3db --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/GrpcCodec.java @@ -0,0 +1,57 @@ +/* + * 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.common.codec; + +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 org.springframework.stereotype.Component; + +import io.grpc.Codec; + +/** + * Annotation that marks gRPC codecs that should be registered with a gRPC server. This annotation should only be added + * to beans that implement {@link Codec}. + * + * @author Michael (yidongnan@gmail.com) + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface GrpcCodec { + + /** + * Advertised codecs will be listed in the {@code Accept-Encoding} header. Defaults to false. + * + * @return True, of the codec should be advertised. False otherwise. + */ + boolean advertised() default false; + + /** + * Gets the type of codec the annotated bean should be used for. + * + * @return The type of codec. + */ + CodecType codecType() default CodecType.ALL; + +} diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/GrpcCodecDefinition.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/GrpcCodecDefinition.java new file mode 100644 index 000000000..eca2ba202 --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/GrpcCodecDefinition.java @@ -0,0 +1,74 @@ +/* + * 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.common.codec; + +import java.util.Collection; + +import com.google.common.collect.ImmutableList; + +import io.grpc.Codec; +import lombok.Getter; + +/** + * Container class that contains all relevant information about a grpc codec. + * + * @see GrpcCodec + * + * @author Michael (yidongnan@gmail.com) + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Getter +public class GrpcCodecDefinition { + + /** + * The codec definition for gzip. + */ + public static final GrpcCodecDefinition GZIP_DEFINITION = + new GrpcCodecDefinition(new Codec.Gzip(), true, CodecType.ALL); + /** + * The codec definition for identity (no-op). + */ + public static final GrpcCodecDefinition IDENTITY_DEFINITION = + new GrpcCodecDefinition(Codec.Identity.NONE, false, CodecType.ALL); + /** + * The default encodings used by gRPC. + */ + public static final Collection DEFAULT_DEFINITIONS = + ImmutableList.builder() + .add(GZIP_DEFINITION) + .add(IDENTITY_DEFINITION) + .build(); + + private final Codec codec; + private final boolean advertised; + private final CodecType codecType; + + /** + * Creates a new GrpcCodecDefinition. + * + * @param codec The codec bean. + * @param advertised Whether the codec should be advertised in the headers. + * @param codecType The type of the codec. + */ + public GrpcCodecDefinition(final Codec codec, final boolean advertised, final CodecType codecType) { + this.codec = codec; + this.advertised = advertised; + this.codecType = codecType; + } + +} diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/GrpcCodecDiscoverer.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/GrpcCodecDiscoverer.java new file mode 100644 index 000000000..ec334561c --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/GrpcCodecDiscoverer.java @@ -0,0 +1,37 @@ +/* + * 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.common.codec; + +import java.util.Collection; + +/** + * An interface for a bean that will be used to find grpc codecs. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@FunctionalInterface +public interface GrpcCodecDiscoverer { + + /** + * Find the grpc codecs that should uses by the client/server. + * + * @return The grpc codecs that should be provided. Never null. + */ + Collection findGrpcCodecs(); + +} diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/package-info.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/package-info.java new file mode 100644 index 000000000..eebf3ec12 --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/codec/package-info.java @@ -0,0 +1,5 @@ +/** + * Classes related to the gRPC codec usage for both the server and the client. + */ + +package net.devh.boot.grpc.common.codec; diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/metric/AbstractMetricCollectingInterceptor.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/metric/AbstractMetricCollectingInterceptor.java new file mode 100644 index 000000000..77e9ddb0d --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/metric/AbstractMetricCollectingInterceptor.java @@ -0,0 +1,216 @@ +/* + * 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.common.metric; + +import static net.devh.boot.grpc.common.metric.MetricConstants.TAG_STATUS_CODE; + +import java.util.EnumMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.function.UnaryOperator; + +import io.grpc.MethodDescriptor; +import io.grpc.ServiceDescriptor; +import io.grpc.Status; +import io.grpc.Status.Code; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.Timer.Sample; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +/** + * An abstract gRPC interceptor that will collect metrics for micrometer. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +public abstract class AbstractMetricCollectingInterceptor { + + private final Map, MetricSet> metricsForMethods = new ConcurrentHashMap<>(); + + protected final MeterRegistry registry; + + protected final UnaryOperator counterCustomizer; + protected final UnaryOperator timerCustomizer; + protected final Code[] eagerInitializedCodes; + + /** + * Creates a new gRPC interceptor that will collect metrics into the given {@link MeterRegistry}. This method won't + * use any customizers and will only initialize the {@link Code#OK OK} status. + * + * @param registry The registry to use. + */ + public AbstractMetricCollectingInterceptor(final MeterRegistry registry) { + this(registry, UnaryOperator.identity(), UnaryOperator.identity(), Code.OK); + } + + /** + * Creates a new gRPC interceptor that will collect metrics into the given {@link MeterRegistry} and uses the given + * customizer to configure the {@link Counter}s and {@link Timer}s. + * + * @param registry The registry to use. + * @param counterCustomizer The unary function that can be used to customize the created counters. + * @param timerCustomizer The unary function that can be used to customize the created timers. + * @param eagerInitializedCodes The status codes that should be eager initialized. + */ + public AbstractMetricCollectingInterceptor(final MeterRegistry registry, + final UnaryOperator counterCustomizer, + final UnaryOperator timerCustomizer, final Code... eagerInitializedCodes) { + this.registry = registry; + this.counterCustomizer = counterCustomizer; + this.timerCustomizer = timerCustomizer; + this.eagerInitializedCodes = eagerInitializedCodes; + } + + /** + * Pre-registers the all methods provided by the given service. This will initialize all default counters and timers + * for those methods. + * + * @param service The service to initialize the meters for. + * @see #preregisterMethod(MethodDescriptor) + */ + public void preregisterService(final ServiceDescriptor service) { + for (final MethodDescriptor method : service.getMethods()) { + preregisterMethod(method); + } + } + + /** + * Pre-registers the given method. This will initialize all default counters and timers for that method. + * + * @param method The method to initialize the meters for. + */ + public void preregisterMethod(final MethodDescriptor method) { + metricsFor(method); + } + + /** + * Gets or creates a {@link MetricSet} for the given gRPC method. This will initialize all default counters and + * timers for that method. + * + * @param method The method to get the metric set for. + * @return The metric set for the given method. + * @see #newMetricsFor(MethodDescriptor) + */ + protected final MetricSet metricsFor(final MethodDescriptor method) { + return this.metricsForMethods.computeIfAbsent(method, this::newMetricsFor); + } + + /** + * Creates a {@link MetricSet} for the given gRPC method. This will initialize all default counters and timers for + * that method. + * + * @param method The method to get the metric set for. + * @return The newly created metric set for the given method. + */ + protected MetricSet newMetricsFor(final MethodDescriptor method) { + log.debug("Creating new metrics for {}", method.getFullMethodName()); + return new MetricSet(newRequestCounterFor(method), newResponseCounterFor(method), newTimerFunction(method)); + } + + /** + * Creates a new request counter for the given method. + * + * @param method The method to create the counter for. + * @return The newly created request counter. + */ + protected abstract Counter newRequestCounterFor(final MethodDescriptor method); + + /** + * Creates a new response counter for the given method. + * + * @param method The method to create the counter for. + * @return The newly created response counter. + */ + protected abstract Counter newResponseCounterFor(final MethodDescriptor method); + + /** + * Creates a new timer function using the given template. This method initializes the default timers. + * + * @param timerTemplate The template to create the instances from. + * @return The newly created function that returns a timer for a given code. + */ + protected Function asTimerFunction(final Supplier timerTemplate) { + final Map cache = new EnumMap<>(Code.class); + final Function creator = code -> timerTemplate.get() + .tag(TAG_STATUS_CODE, code.name()) + .register(this.registry); + final Function cacheResolver = code -> cache.computeIfAbsent(code, creator); + // Eager initialize + for (final Code code : this.eagerInitializedCodes) { + cacheResolver.apply(code); + } + return cacheResolver; + } + + /** + * Creates a new timer for a given code for the given method. + * + * @param method The method to create the timer for. + * @return The newly created function that returns a timer for a given code. + */ + protected abstract Function newTimerFunction(final MethodDescriptor method); + + /** + * Container for all metrics of a certain call. Used instead of 3 maps to improve performance. + */ + @Getter + protected static class MetricSet { + + private final Counter requestCounter; + private final Counter responseCounter; + private final Function timerFunction; + + /** + * Creates a new metric set with the given meter instances. + * + * @param requestCounter The request counter to use. + * @param responseCounter The response counter to use. + * @param timerFunction The timer function to use. + */ + public MetricSet( + final Counter requestCounter, + final Counter responseCounter, + final Function timerFunction) { + + this.requestCounter = requestCounter; + this.responseCounter = responseCounter; + this.timerFunction = timerFunction; + } + + /** + * Uses the given registry to create a {@link Sample Timer.Sample} that will be reported if the returned + * consumer is invoked. + * + * @param registry The registry used to create the sample. + * @return The newly created consumer that will report the processing duration since calling this method and + * invoking the returned consumer along with the status code. + */ + public Consumer newProcessingDurationTiming(final MeterRegistry registry) { + final Timer.Sample timerSample = Timer.start(registry); + return code -> timerSample.stop(this.timerFunction.apply(code)); + } + + } + +} diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/metric/MetricConstants.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/metric/MetricConstants.java new file mode 100644 index 000000000..397acf7cf --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/metric/MetricConstants.java @@ -0,0 +1,72 @@ +/* + * 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.common.metric; + +/** + * Utility class that contains constants that are used multiple times by different classes. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public final class MetricConstants { + + /** + * The total number of requests received + */ + public static final String METRIC_NAME_SERVER_REQUESTS_RECEIVED = "grpc.server.requests.received"; + /** + * The total number of responses sent + */ + public static final String METRIC_NAME_SERVER_RESPONSES_SENT = "grpc.server.responses.sent"; + /** + * The total time taken for the server to complete the call. + */ + public static final String METRIC_NAME_SERVER_PROCESSING_DURATION = "grpc.server.processing.duration"; + + /** + * The total number of requests sent + */ + public static final String METRIC_NAME_CLIENT_REQUESTS_SENT = "grpc.client.requests.sent"; + /** + * The total number of responses received + */ + public static final String METRIC_NAME_CLIENT_RESPONSES_RECEIVED = "grpc.client.responses.received"; + /** + * The total time taken for the client to complete the call, including network delay + */ + public static final String METRIC_NAME_CLIENT_PROCESSING_DURATION = "grpc.client.processing.duration"; + + /** + * The metrics tag key that belongs to the called service name. + */ + public static final String TAG_SERVICE_NAME = "service"; + /** + * The metrics tag key that belongs to the called method name. + */ + public static final String TAG_METHOD_NAME = "method"; + /** + * The metrics tag key that belongs to the type of the called method. + */ + public static final String TAG_METHOD_TYPE = "methodType"; + /** + * The metrics tag key that belongs to the result status code. + */ + public static final String TAG_STATUS_CODE = "statusCode"; + + private MetricConstants() {} + +} diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/metric/MetricUtils.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/metric/MetricUtils.java new file mode 100644 index 000000000..572ccdf6e --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/metric/MetricUtils.java @@ -0,0 +1,75 @@ +/* + * 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.common.metric; + +import static net.devh.boot.grpc.common.metric.MetricConstants.TAG_METHOD_NAME; +import static net.devh.boot.grpc.common.metric.MetricConstants.TAG_METHOD_TYPE; +import static net.devh.boot.grpc.common.metric.MetricConstants.TAG_SERVICE_NAME; +import static net.devh.boot.grpc.common.util.GrpcUtils.extractMethodName; +import static net.devh.boot.grpc.common.util.GrpcUtils.extractServiceName; + +import io.grpc.MethodDescriptor; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Timer; + +/** + * Utility class that contains methods to create {@link Meter} instances for {@link MethodDescriptor}s. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public final class MetricUtils { + + /** + * Creates a new counter builder for the given method. By default the base unit will be messages. + * + * @param method The method the counter will be created for. + * @param name The name of the counter to use. + * @param description The description of the counter to use. + * @return The newly created counter builder. + */ + public static Counter.Builder prepareCounterFor(final MethodDescriptor method, + final String name, final String description) { + return Counter.builder(name) + .description(description) + .baseUnit("messages") + .tag(TAG_SERVICE_NAME, extractServiceName(method)) + .tag(TAG_METHOD_NAME, extractMethodName(method)) + .tag(TAG_METHOD_TYPE, method.getType().name()); + } + + /** + * Creates a new timer builder for the given method. + * + * @param method The method the timer will be created for. + * @param name The name of the timer to use. + * @param description The description of the timer to use. + * @return The newly created timer builder. + */ + public static Timer.Builder prepareTimerFor(final MethodDescriptor method, + final String name, final String description) { + return Timer.builder(name) + .description(description) + .tag(TAG_SERVICE_NAME, extractServiceName(method)) + .tag(TAG_METHOD_NAME, extractMethodName(method)) + .tag(TAG_METHOD_TYPE, method.getType().name()); + } + + private MetricUtils() {} + +} diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/metric/package-info.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/metric/package-info.java new file mode 100644 index 000000000..a34bd9d08 --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/metric/package-info.java @@ -0,0 +1,5 @@ +/** + * Shared code for grpc metric collection related classes. + */ + +package net.devh.boot.grpc.common.metric; diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/security/SecurityConstants.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/security/SecurityConstants.java new file mode 100644 index 000000000..be26ecbc7 --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/security/SecurityConstants.java @@ -0,0 +1,50 @@ +/* + * 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.common.security; + +import java.nio.charset.StandardCharsets; + +import io.grpc.Metadata; +import io.grpc.Metadata.Key; + +/** + * A helper class with constants related to grpc security. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public final class SecurityConstants { + + /** + * A convenience constant that contains the key for the HTTP Authorization Header. + */ + public static final Key AUTHORIZATION_HEADER = Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER); + + /** + * The prefix for basic auth as used in the {@link #AUTHORIZATION_HEADER}. This library assumes that the both the + * username and password are {@link StandardCharsets#UTF_8 UTF_8} encoded before being turned into a base64 string. + */ + public static final String BASIC_AUTH_PREFIX = "Basic "; + + /** + * The prefix for bearer auth as used in the {@link #AUTHORIZATION_HEADER}. + */ + public static final String BEARER_AUTH_PREFIX = "Bearer "; + + private SecurityConstants() {} + +} diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/security/package-info.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/security/package-info.java new file mode 100644 index 000000000..03572857d --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/security/package-info.java @@ -0,0 +1,5 @@ +/** + * Contains classes and utilities that related to security that are used by both the client and the server. + */ + +package net.devh.boot.grpc.common.security; diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/util/GrpcUtils.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/util/GrpcUtils.java new file mode 100644 index 000000000..18ca3a39e --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/util/GrpcUtils.java @@ -0,0 +1,104 @@ +/* + * 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.common.util; + +import io.grpc.MethodDescriptor; + +/** + * Utility class that contains methods to extract some information from grpc classes. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public final class GrpcUtils { + + /** + * A constant that defines, the scheme of a Unix domain socket address. + */ + public static final String DOMAIN_SOCKET_ADDRESS_SCHEME = "unix"; + + /** + * A constant that defines, the scheme prefix of a Unix domain socket address. + */ + public static final String DOMAIN_SOCKET_ADDRESS_PREFIX = DOMAIN_SOCKET_ADDRESS_SCHEME + ":"; + + /** + * The cloud discovery metadata key used to identify the grpc port. + */ + public static final String CLOUD_DISCOVERY_METADATA_PORT = "gRPC_port"; + + /** + * The constant for the grpc server port, -1 represents don't start an inter process server. + */ + public static final int INTER_PROCESS_DISABLE = -1; + + /** + * Extracts the domain socket address specific path from the given full address. The address must fulfill the + * requirements as specified by grpc. + * + * @param address The address to extract it from. + * @return The extracted domain socket address specific path. + * @throws IllegalArgumentException If the given address is not a valid address. + */ + public static String extractDomainSocketAddressPath(final String address) { + if (!address.startsWith(DOMAIN_SOCKET_ADDRESS_PREFIX)) { + throw new IllegalArgumentException(address + " is not a valid domain socket address."); + } + String path = address.substring(DOMAIN_SOCKET_ADDRESS_PREFIX.length()); + if (path.startsWith("//")) { + path = path.substring(2); + // We don't check this as there is no reliable way to check that it's an absolute path, + // especially when Windows adds support for these in the future + // if (!path.startsWith("/")) { + // throw new IllegalArgumentException("If the path is prefixed with '//', then the path must be absolute"); + // } + } + return path; + } + + /** + * Extracts the service name from the given method. + * + * @param method The method to get the service name from. + * @return The extracted service name. + * @see MethodDescriptor#extractFullServiceName(String) + * @see #extractMethodName(MethodDescriptor) + */ + public static String extractServiceName(final MethodDescriptor method) { + return MethodDescriptor.extractFullServiceName(method.getFullMethodName()); + } + + /** + * Extracts the method name from the given method. + * + * @param method The method to get the method name from. + * @return The extracted method name. + * @see #extractServiceName(MethodDescriptor) + */ + public static String extractMethodName(final MethodDescriptor method) { + // This method is the equivalent of MethodDescriptor.extractFullServiceName + final String fullMethodName = method.getFullMethodName(); + final int index = fullMethodName.lastIndexOf('/'); + if (index == -1) { + return fullMethodName; + } + return fullMethodName.substring(index + 1); + } + + private GrpcUtils() {} + +} diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/util/InterceptorOrder.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/util/InterceptorOrder.java new file mode 100644 index 000000000..205b2a492 --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/util/InterceptorOrder.java @@ -0,0 +1,65 @@ +/* + * 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.common.util; + +import org.springframework.core.Ordered; + +/** + * A util class with constants that can be used to configure the order of interceptors. + * + *

+ * Note: The order constants provided by this class are just a suggestion to simplify the interoperability of + * multiple libraries and may be overridden. This library will use them for their own interceptors though. + *

+ */ +public final class InterceptorOrder { + + /** + * The order value for interceptors that should be executed first. This is equivalent to + * {@link Ordered#HIGHEST_PRECEDENCE}. + */ + public static final int ORDER_FIRST = Ordered.HIGHEST_PRECEDENCE; + /** + * The order value for global exception handling interceptors. + */ + public static final int ORDER_GLOBAL_EXCEPTION_HANDLING = 0; + /** + * The order value for tracing and metrics collecting interceptors. + */ + public static final int ORDER_TRACING_METRICS = 2500; + /** + * The order value for interceptors related security exception handling. + */ + public static final int ORDER_SECURITY_EXCEPTION_HANDLING = 5000; + /** + * The order value for security interceptors related to authentication. + */ + public static final int ORDER_SECURITY_AUTHENTICATION = 5100; + /** + * The order value for security interceptors related to authorization checks. + */ + public static final int ORDER_SECURITY_AUTHORISATION = 5200; + /** + * The order value for interceptors that should be executed last. This is equivalent to + * {@link Ordered#LOWEST_PRECEDENCE}. This is the default for interceptors without specified priority. + */ + public static final int ORDER_LAST = Ordered.LOWEST_PRECEDENCE; + + private InterceptorOrder() {} + +} diff --git a/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/util/package-info.java b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/util/package-info.java new file mode 100644 index 000000000..9b9971ac7 --- /dev/null +++ b/grpc-common-spring-boot/src/main/java/net/devh/boot/grpc/common/util/package-info.java @@ -0,0 +1,5 @@ +/** + * Utilities for both the server and the client. + */ + +package net.devh.boot.grpc.common.util; diff --git a/grpc-common-spring-boot/src/main/resources/META-INF/spring.factories b/grpc-common-spring-boot/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..0e9af3edd --- /dev/null +++ b/grpc-common-spring-boot/src/main/resources/META-INF/spring.factories @@ -0,0 +1,4 @@ +# AutoConfiguration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +net.devh.boot.grpc.common.autoconfigure.GrpcCommonCodecAutoConfiguration,\ +net.devh.boot.grpc.common.autoconfigure.GrpcCommonTraceAutoConfiguration diff --git a/grpc-server-spring-boot-autoconfigure/build.gradle b/grpc-server-spring-boot-autoconfigure/build.gradle new file mode 100644 index 000000000..dbed78073 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/build.gradle @@ -0,0 +1,42 @@ +plugins { + id 'java-library' +} + +apply from: '../deploy.gradle' + +group = 'net.devh' +version = projectVersion + +compileJava.dependsOn(processResources) + +dependencies { + annotationProcessor 'org.springframework.boot:spring-boot-autoconfigure-processor' + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' + + api project(':grpc-common-spring-boot') + api 'org.springframework.boot:spring-boot-starter' + optionalSupportImplementation 'org.springframework.boot:spring-boot-starter-actuator' + optionalSupportImplementation 'org.springframework.security:spring-security-core' + optionalSupportImplementation 'org.springframework.cloud:spring-cloud-starter-sleuth' + optionalSupportImplementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery' + optionalSupportImplementation 'org.springframework.cloud:spring-cloud-starter-zookeeper-discovery' + optionalSupportImplementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + optionalSupportImplementation "com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery" + optionalSupportImplementation 'com.google.inject:guice:4.2.3' // Only needed to avoid some warnings during compilation (for eureka) + optionalSupportImplementation 'io.zipkin.brave:brave-instrumentation-grpc' + optionalSupportApi 'io.grpc:grpc-netty' + api 'io.grpc:grpc-netty-shaded' + api 'io.grpc:grpc-protobuf' + api 'io.grpc:grpc-stub' + api 'io.grpc:grpc-services' + api 'io.grpc:grpc-api' + + testImplementation 'io.grpc:grpc-testing' + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude module: 'junit-vintage-engine' + exclude module: 'junit' + } + + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdvice.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdvice.java new file mode 100644 index 000000000..8d25d83f2 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdvice.java @@ -0,0 +1,44 @@ +/* + * 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.server.advice; + +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 org.springframework.stereotype.Component; + +/** + * Special {@link Component @Component} to declare global gRPC exception handling. + * + * Every class annotated with {@link GrpcAdvice @GrpcAdvice} is marked to be scanned for + * {@link GrpcExceptionHandler @GrpcExceptionHandler} annotations. + *

+ * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + * @see GrpcExceptionHandler + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface GrpcAdvice { + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceDiscoverer.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceDiscoverer.java new file mode 100644 index 000000000..5c9db512c --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceDiscoverer.java @@ -0,0 +1,93 @@ +/* + * 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.server.advice; + +import java.lang.reflect.Method; +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.MethodIntrospector; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils.MethodFilter; + +import lombok.extern.slf4j.Slf4j; + +/** + * A discovery class to find all Beans annotated with {@link GrpcAdvice @GrpcAdvice} and for all found beans a second + * search is performed looking for methods with {@link GrpcExceptionHandler @GrpcExceptionHandler}. + * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + * @see GrpcAdvice + * @see GrpcExceptionHandler + */ +@Slf4j +public class GrpcAdviceDiscoverer implements InitializingBean, ApplicationContextAware { + + /** + * A filter for selecting {@code @GrpcExceptionHandler} methods. + */ + public static final MethodFilter EXCEPTION_HANDLER_METHODS = + method -> AnnotatedElementUtils.hasAnnotation(method, GrpcExceptionHandler.class); + + private ApplicationContext applicationContext; + private Map annotatedBeans; + private Set annotatedMethods; + + @Override + public void setApplicationContext(final ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public void afterPropertiesSet() { + annotatedBeans = applicationContext.getBeansWithAnnotation(GrpcAdvice.class); + annotatedBeans.forEach( + (key, value) -> log.debug("Found gRPC advice: " + key + ", class: " + value.getClass().getName())); + + annotatedMethods = findAnnotatedMethods(); + } + + private Set findAnnotatedMethods() { + return this.annotatedBeans.values().stream() + .map(Object::getClass) + .map(this::findAnnotatedMethods) + .flatMap(Collection::stream) + .collect(Collectors.toSet()); + } + + private Set findAnnotatedMethods(final Class clazz) { + return MethodIntrospector.selectMethods(clazz, EXCEPTION_HANDLER_METHODS); + } + + public Map getAnnotatedBeans() { + Assert.state(annotatedBeans != null, "@GrpcAdvice annotation scanning failed."); + return annotatedBeans; + } + + public Set getAnnotatedMethods() { + Assert.state(annotatedMethods != null, "@GrpcExceptionHandler annotation scanning failed."); + return annotatedMethods; + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceExceptionHandler.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceExceptionHandler.java new file mode 100644 index 000000000..d9c398acf --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceExceptionHandler.java @@ -0,0 +1,117 @@ +/* + * 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.server.advice; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.Type; +import java.util.Map.Entry; + +import org.springframework.lang.Nullable; + +import lombok.extern.slf4j.Slf4j; + +/** + * As part of {@link GrpcAdvice @GrpcAdvice}, when a thrown exception is caught during gRPC calls (via global + * interceptor {@link GrpcAdviceExceptionInterceptor}, then this thrown exception is being handled. By + * {@link GrpcExceptionHandlerMethodResolver} is a mapping between exception and the in case to be executed method + * provided.
+ * Returned object is declared in {@link GrpcAdvice @GrpcAdvice} classes with annotated methods + * {@link GrpcExceptionHandler @GrpcExceptionHandler}. + *

+ * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + * @see GrpcExceptionHandlerMethodResolver + * @see GrpcAdviceExceptionInterceptor + */ +@Slf4j +public class GrpcAdviceExceptionHandler { + + private final GrpcExceptionHandlerMethodResolver grpcExceptionHandlerMethodResolver; + + public GrpcAdviceExceptionHandler( + final GrpcExceptionHandlerMethodResolver grpcExceptionHandlerMethodResolver) { + this.grpcExceptionHandlerMethodResolver = grpcExceptionHandlerMethodResolver; + } + + /** + * Given an exception, a lookup is performed to retrieve mapped method.
+ * In case of successful returned method, and matching exception parameter type for given exception, the exception + * is handed over to retrieved method. Retrieved method is then being invoked. + * + * @param exception exception to search for + * @param type of exception + * @return result of invoked mapped method to given exception + * @throws Throwable rethrows exception if no mapping existent or exceptions raised by implementation + */ + @Nullable + public Object handleThrownException(E exception) throws Throwable { + log.debug("Exception caught during gRPC execution: ", exception); + + final Class exceptionClass = exception.getClass(); + boolean exceptionIsMapped = + grpcExceptionHandlerMethodResolver.isMethodMappedForException(exceptionClass); + if (!exceptionIsMapped) { + throw exception; + } + + Entry methodWithInstance = + grpcExceptionHandlerMethodResolver.resolveMethodWithInstance(exceptionClass); + Method mappedMethod = methodWithInstance.getValue(); + Object instanceOfMappedMethod = methodWithInstance.getKey(); + Object[] instancedParams = determineInstancedParameters(mappedMethod, exception); + + return invokeMappedMethodSafely(mappedMethod, instanceOfMappedMethod, instancedParams); + } + + private Object[] determineInstancedParameters(Method mappedMethod, E exception) { + + Parameter[] parameters = mappedMethod.getParameters(); + Object[] instancedParams = new Object[parameters.length]; + + for (int i = 0; i < parameters.length; i++) { + Class parameterClass = convertToClass(parameters[i]); + if (parameterClass.isAssignableFrom(exception.getClass())) { + instancedParams[i] = exception; + break; + } + } + return instancedParams; + } + + private Class convertToClass(Parameter parameter) { + Type paramType = parameter.getParameterizedType(); + if (paramType instanceof Class) { + return (Class) paramType; + } + throw new IllegalStateException("Parameter type of method has to be from Class, it was: " + paramType); + } + + private Object invokeMappedMethodSafely( + Method mappedMethod, + Object instanceOfMappedMethod, + Object[] instancedParams) throws Throwable { + try { + return mappedMethod.invoke(instanceOfMappedMethod, instancedParams); + } catch (InvocationTargetException | IllegalAccessException e) { + throw e.getCause(); // throw the exception thrown by implementation + } + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceExceptionInterceptor.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceExceptionInterceptor.java new file mode 100644 index 000000000..fb587393f --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceExceptionInterceptor.java @@ -0,0 +1,67 @@ +/* + * 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.server.advice; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; + +/** + * Interceptor to use for global exception handling. Every raised {@link Throwable} is caught and being processed. + * Actual processing of exception is in {@link GrpcAdviceExceptionListener}. + *

+ * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + * @see GrpcAdviceExceptionHandler + * @see GrpcAdviceExceptionListener + */ +public class GrpcAdviceExceptionInterceptor implements ServerInterceptor { + + private final GrpcAdviceExceptionHandler grpcAdviceExceptionHandler; + + public GrpcAdviceExceptionInterceptor(final GrpcAdviceExceptionHandler grpcAdviceExceptionHandler) { + this.grpcAdviceExceptionHandler = grpcAdviceExceptionHandler; + } + + @Override + public Listener interceptCall( + ServerCall call, + Metadata headers, + ServerCallHandler next) { + try { + Listener delegate = next.startCall(call, headers); + return new GrpcAdviceExceptionListener<>(delegate, call, grpcAdviceExceptionHandler); + } catch (Throwable throwable) { + return noOpCallListener(); + } + } + + /** + * Creates a new no-op call listener because you can neither return null nor throw an exception in + * {@link #interceptCall(ServerCall, Metadata, ServerCallHandler)}. + * + * @param The type of the request. + * @return The newly created dummy listener. + */ + protected Listener noOpCallListener() { + return new Listener() {}; + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceExceptionListener.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceExceptionListener.java new file mode 100644 index 000000000..4568ac1ce --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceExceptionListener.java @@ -0,0 +1,108 @@ +/* + * 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.server.advice; + +import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; +import lombok.extern.slf4j.Slf4j; + +/** + * In case an exception is thrown inside {@link #onHalfClose()}, it is being handled by invoking annotated methods with + * {@link GrpcExceptionHandler @GrpcExceptionHandler}. On successful invocation proper exception handling is done. + *

+ * Note: In case of raised exceptions by implementation a {@link Status#INTERNAL} is returned in + * {@link #handleThrownExceptionByImplementation(Throwable)}. + * + * @param gRPC request type + * @param gRPC response type + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + * @see GrpcAdviceExceptionHandler + */ +@Slf4j +public class GrpcAdviceExceptionListener extends SimpleForwardingServerCallListener { + + private final GrpcAdviceExceptionHandler exceptionHandler; + private final ServerCall serverCall; + + protected GrpcAdviceExceptionListener( + Listener delegate, + ServerCall serverCall, + GrpcAdviceExceptionHandler grpcAdviceExceptionHandler) { + super(delegate); + this.serverCall = serverCall; + this.exceptionHandler = grpcAdviceExceptionHandler; + } + + @Override + public void onHalfClose() { + try { + super.onHalfClose(); + + } catch (Throwable throwable) { + handleCaughtException(throwable); + } + } + + private void handleCaughtException(Throwable throwable) { + try { + Object mappedReturnType = exceptionHandler.handleThrownException(throwable); + Status status = resolveStatus(mappedReturnType).withCause(throwable); + Metadata metadata = resolveMetadata(mappedReturnType); + + serverCall.close(status, metadata); + } catch (Throwable throwableWhileResolving) { + handleThrownExceptionByImplementation(throwableWhileResolving); + } + } + + private Status resolveStatus(Object mappedReturnType) { + if (mappedReturnType instanceof Status) { + return (Status) mappedReturnType; + } else if (mappedReturnType instanceof Throwable) { + return Status.fromThrowable((Throwable) mappedReturnType); + } + throw new IllegalStateException(String.format( + "Error for mapped return type [%s] inside @GrpcAdvice, it has to be of type: " + + "[Status, StatusException, StatusRuntimeException, Throwable] ", + mappedReturnType)); + } + + private Metadata resolveMetadata(Object mappedReturnType) { + Metadata result = null; + if (mappedReturnType instanceof StatusException) { + StatusException statusException = (StatusException) mappedReturnType; + result = statusException.getTrailers(); + } else if (mappedReturnType instanceof StatusRuntimeException) { + StatusRuntimeException statusException = (StatusRuntimeException) mappedReturnType; + result = statusException.getTrailers(); + } + return (result == null) ? new Metadata() : result; + } + + private void handleThrownExceptionByImplementation(Throwable throwable) { + log.error("Exception thrown during invocation of annotated @GrpcExceptionHandler method: ", throwable); + serverCall.close(Status.INTERNAL.withCause(throwable) + .withDescription("There was a server error trying to handle an exception"), new Metadata()); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceIsPresentCondition.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceIsPresentCondition.java new file mode 100644 index 000000000..359b22147 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcAdviceIsPresentCondition.java @@ -0,0 +1,48 @@ +/* + * 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.server.advice; + +import static java.util.Objects.requireNonNull; + +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.ConfigurationCondition; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * Condition to check if {@link GrpcAdvice @GrpcAdvice} is present. Mainly checking if {@link GrpcAdviceDiscoverer} + * should be a instantiated. + * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + * @see GrpcAdviceDiscoverer + */ +public class GrpcAdviceIsPresentCondition implements ConfigurationCondition { + + @Override + public ConfigurationPhase getConfigurationPhase() { + return ConfigurationPhase.REGISTER_BEAN; + } + + @Override + public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) { + final ConfigurableListableBeanFactory safeBeanFactory = + requireNonNull(context.getBeanFactory(), "ConfigurableListableBeanFactory is null"); + return safeBeanFactory.getBeanNamesForAnnotation(GrpcAdvice.class).length != 0; + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcExceptionHandler.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcExceptionHandler.java new file mode 100644 index 000000000..32a7f55d2 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcExceptionHandler.java @@ -0,0 +1,82 @@ +/* + * 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.server.advice; + +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; + +/** + * Methods annotated with {@link GrpcExceptionHandler @GrpcExceptionHandler} are being mapped to a corresponding + * Exception, by declaring either in {@link GrpcExceptionHandler#value() @GrpcExceptionHandler(value = ...)} as value or + * as annotated methods parameter (both is working too). + *

+ * Return type of annotated methods has to be of type {@link io.grpc.Status}, {@link io.grpc.StatusException}, + * {@link io.grpc.StatusRuntimeException} or {@link Throwable}. + *

+ * + * An example without {@link io.grpc.Metadata}: + * + *

+ * {@code @GrpcExceptionHandler
+ *    public Status handleIllegalArgumentException(IllegalArgumentException e){
+ *      return Status.INVALID_ARGUMENT
+ *                   .withDescription(e.getMessage())
+ *                   .withCause(e);
+ *    }
+ *  }
+ * 
+ * + * With {@link io.grpc.Metadata}: + * + *
+ * {@code @GrpcExceptionHandler
+ *    public StatusRuntimeException handleIllegalArgumentException(IllegalArgumentException e){
+ *      Status status = Status.INVALID_ARGUMENT
+ *                            .withDescription(e.getMessage())
+ *                            .withCause(e);
+ *      Metadata myMetadata = new Metadata();
+ *      myMetadata = ...
+ *      return status.asRuntimeException(myMetadata);
+ *    }
+ *  }
+ * 
+ * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + * @see GrpcAdvice + * @see GrpcExceptionHandlerMethodResolver + * @see GrpcAdviceExceptionHandler + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface GrpcExceptionHandler { + + /** + * Exceptions handled by the annotated method. + *

+ * If empty, will default to any exceptions listed in the method argument list. + *

+ * Note: When exception types are set within value, they are prioritized in mapping the exceptions over + * listed method arguments. And in case method arguments are provided, they must match the types declared + * with this value. + */ + Class[] value() default {}; +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcExceptionHandlerMethodResolver.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcExceptionHandlerMethodResolver.java new file mode 100644 index 000000000..bdd629161 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/advice/GrpcExceptionHandlerMethodResolver.java @@ -0,0 +1,191 @@ +/* + * 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.server.advice; + +import static java.util.Objects.requireNonNull; + +import java.lang.reflect.Method; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.ExceptionDepthComparator; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Given an annotated {@link GrpcAdvice @GrpcAdvice} class and annotated methods with + * {@link GrpcExceptionHandler @GrpcExceptionHandler}, {@link GrpcExceptionHandlerMethodResolver} resolves given + * exception type and maps it to the corresponding method to be executed, when this exception is being raised. + * + *

+ * For an example how to make use of it, please have a look at {@link GrpcExceptionHandler @GrpcExceptionHandler}. + *

+ * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + * @see GrpcAdvice + * @see GrpcExceptionHandler + * @see GrpcAdviceExceptionHandler + */ +public class GrpcExceptionHandlerMethodResolver implements InitializingBean { + + private final Map, Method> mappedMethods = new HashMap<>(16); + + private final GrpcAdviceDiscoverer grpcAdviceDiscoverer; + + private Class[] annotatedExceptions; + + /** + * Creates a new GrpcExceptionHandlerMethodResolver. + * + * @param grpcAdviceDiscoverer The advice discoverer to use. + */ + public GrpcExceptionHandlerMethodResolver(final GrpcAdviceDiscoverer grpcAdviceDiscoverer) { + this.grpcAdviceDiscoverer = requireNonNull(grpcAdviceDiscoverer, "grpcAdviceDiscoverer"); + } + + @Override + public void afterPropertiesSet() throws Exception { + grpcAdviceDiscoverer.getAnnotatedMethods() + .forEach(this::extractAndMapExceptionToMethod); + } + + private void extractAndMapExceptionToMethod(Method method) { + + GrpcExceptionHandler annotation = method.getDeclaredAnnotation(GrpcExceptionHandler.class); + Assert.notNull(annotation, "@GrpcExceptionHandler annotation not found."); + annotatedExceptions = annotation.value(); + + checkForPresentExceptionToMap(method); + Set> exceptionsToMap = extractExceptions(method.getParameterTypes()); + exceptionsToMap.forEach(exceptionType -> addExceptionMapping(exceptionType, method)); + } + + private void checkForPresentExceptionToMap(Method method) { + if (method.getParameterTypes().length == 0 && annotatedExceptions.length == 0) { + throw new IllegalStateException( + String.format("@GrpcExceptionHandler annotated method [%s] has no mapped exception!", + method.getName())); + } + } + + private Set> extractExceptions(Class[] methodParamTypes) { + + Set> exceptionsToBeMapped = new HashSet<>(); + for (Class annoClass : annotatedExceptions) { + if (methodParamTypes.length > 0) + validateAppropriateParentException(annoClass, methodParamTypes); + exceptionsToBeMapped.add(annoClass); + } + + addMappingInCaseAnnotationIsEmpty(methodParamTypes, exceptionsToBeMapped); + return exceptionsToBeMapped; + } + + private void validateAppropriateParentException(Class annoClass, Class[] methodParamTypes) { + + boolean paramTypeIsNotSuperclass = + Arrays.stream(methodParamTypes).noneMatch(param -> param.isAssignableFrom(annoClass)); + if (paramTypeIsNotSuperclass) { + throw new IllegalStateException( + String.format( + "no listed parameter argument [%s] is equal or superclass " + + "of annotated @GrpcExceptionHandler method declared exception [%s].", + Arrays.toString(methodParamTypes), annoClass)); + } + } + + private void addMappingInCaseAnnotationIsEmpty( + Class[] methodParamTypes, + Set> exceptionsToBeMapped) { + + @SuppressWarnings("unchecked") + Function, Class> convertSafely = clazz -> (Class) clazz; + + Arrays.stream(methodParamTypes) + .filter(param -> exceptionsToBeMapped.isEmpty()) + .filter(Throwable.class::isAssignableFrom) + .map(convertSafely) // safe to call, since check for Throwable superclass + .forEach(exceptionsToBeMapped::add); + } + + private void addExceptionMapping(Class exceptionType, Method method) { + + Method oldMethod = mappedMethods.put(exceptionType, method); + if (oldMethod != null && !oldMethod.equals(method)) { + throw new IllegalStateException("Ambiguous @GrpcExceptionHandler method mapped for [" + + exceptionType + "]: {" + oldMethod + ", " + method + "}"); + } + } + + + /** + * When given exception type is subtype of already provided mapped exception, this returns a valid mapped method to + * be later executed. + * + * @param exceptionType exception to check + * @param type of exception + * @return mapped method instance with its method + */ + @NonNull + public Map.Entry resolveMethodWithInstance(Class exceptionType) { + + Method value = extractExtendedThrowable(exceptionType); + if (value == null) { + return new SimpleImmutableEntry<>(null, null); + } + + Class methodClass = value.getDeclaringClass(); + Object key = grpcAdviceDiscoverer.getAnnotatedBeans() + .values() + .stream() + .filter(obj -> methodClass.isAssignableFrom(obj.getClass())) + .findFirst() + .orElse(null); + return new SimpleImmutableEntry<>(key, value); + } + + /** + * Lookup if a method is mapped to given exception. + * + * @param exception exception to check + * @param type of exception + * @return true if mapped to valid method + */ + public boolean isMethodMappedForException(Class exception) { + return extractExtendedThrowable(exception) != null; + } + + @Nullable + private Method extractExtendedThrowable(Class exceptionType) { + + return mappedMethods.keySet() + .stream() + .filter(ex -> ex.isAssignableFrom(exceptionType)) + .min(new ExceptionDepthComparator(exceptionType)) + .map(mappedMethods::get) + .orElse(null); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcAdviceAutoConfiguration.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcAdviceAutoConfiguration.java new file mode 100644 index 000000000..16630c014 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcAdviceAutoConfiguration.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2016-2020 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.server.autoconfigure; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; + +import net.devh.boot.grpc.common.util.InterceptorOrder; +import net.devh.boot.grpc.server.advice.GrpcAdvice; +import net.devh.boot.grpc.server.advice.GrpcAdviceDiscoverer; +import net.devh.boot.grpc.server.advice.GrpcAdviceExceptionHandler; +import net.devh.boot.grpc.server.advice.GrpcAdviceExceptionInterceptor; +import net.devh.boot.grpc.server.advice.GrpcAdviceIsPresentCondition; +import net.devh.boot.grpc.server.advice.GrpcExceptionHandler; +import net.devh.boot.grpc.server.advice.GrpcExceptionHandlerMethodResolver; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; + +/** + * The auto configuration that will create necessary beans to provide a proper exception handling via annotations + * {@link GrpcAdvice @GrpcAdvice} and {@link GrpcExceptionHandler @GrpcExceptionHandler}. + * + *

+ * Exception handling via global server interceptors {@link GrpcGlobalServerInterceptor @GrpcGlobalServerInterceptor}. + *

+ * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + * @see GrpcAdvice + * @see GrpcExceptionHandler + * @see GrpcAdviceExceptionInterceptor + */ +@Configuration(proxyBeanMethods = false) +@Conditional(GrpcAdviceIsPresentCondition.class) +@AutoConfigureBefore(GrpcServerFactoryAutoConfiguration.class) +public class GrpcAdviceAutoConfiguration { + + @Bean + public GrpcAdviceDiscoverer grpcAdviceDiscoverer() { + return new GrpcAdviceDiscoverer(); + } + + @Bean + public GrpcExceptionHandlerMethodResolver grpcExceptionHandlerMethodResolver( + final GrpcAdviceDiscoverer grpcAdviceDiscoverer) { + return new GrpcExceptionHandlerMethodResolver(grpcAdviceDiscoverer); + } + + @Bean + public GrpcAdviceExceptionHandler grpcAdviceExceptionHandler( + GrpcExceptionHandlerMethodResolver grpcExceptionHandlerMethodResolver) { + return new GrpcAdviceExceptionHandler(grpcExceptionHandlerMethodResolver); + } + + @GrpcGlobalServerInterceptor + @Order(InterceptorOrder.ORDER_GLOBAL_EXCEPTION_HANDLING) + public GrpcAdviceExceptionInterceptor grpcAdviceExceptionInterceptor( + GrpcAdviceExceptionHandler grpcAdviceExceptionHandler) { + return new GrpcAdviceExceptionInterceptor(grpcAdviceExceptionHandler); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcHealthServiceAutoConfiguration.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcHealthServiceAutoConfiguration.java new file mode 100644 index 000000000..aa15a6066 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcHealthServiceAutoConfiguration.java @@ -0,0 +1,59 @@ +/* + * 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.server.autoconfigure; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.grpc.BindableService; +import io.grpc.services.HealthStatusManager; +import net.devh.boot.grpc.server.service.GrpcService; + +/** + * Auto configuration that sets up the grpc health service. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Configuration +@ConditionalOnClass(HealthStatusManager.class) +@ConditionalOnProperty(prefix = "grpc.server", name = "health-service-enabled", matchIfMissing = true) +@AutoConfigureBefore(GrpcServerFactoryAutoConfiguration.class) +public class GrpcHealthServiceAutoConfiguration { + + /** + * Creates a new HealthStatusManager instance. + * + * @return The newly created bean. + */ + @Bean + @ConditionalOnMissingBean + HealthStatusManager healthStatusManager() { + return new HealthStatusManager(); + } + + @Bean + @GrpcService + BindableService grpcHealthService(final HealthStatusManager healthStatusManager) { + return healthStatusManager.getHealthService(); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcMetadataConsulConfiguration.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcMetadataConsulConfiguration.java new file mode 100644 index 000000000..dcaeac7fe --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcMetadataConsulConfiguration.java @@ -0,0 +1,65 @@ +/* + * 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.server.autoconfigure; + +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.consul.serviceregistry.ConsulRegistration; +import org.springframework.context.annotation.Configuration; + +import net.devh.boot.grpc.common.util.GrpcUtils; +import net.devh.boot.grpc.server.config.GrpcServerProperties; + +/** + * Configuration class that configures the required beans for gRPC discovery via Consul. + * + * @author Michael (yidongnan@gmail.com) + * @since 5/17/16 + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties +@ConditionalOnClass({ConsulRegistration.class}) +public class GrpcMetadataConsulConfiguration { + + @Autowired(required = false) + private ConsulRegistration consulRegistration; + + @Autowired + private GrpcServerProperties grpcProperties; + + @PostConstruct + public void init() { + if (consulRegistration != null) { + final int port = grpcProperties.getPort(); + Map meta = consulRegistration.getService().getMeta(); + if (meta == null) { + meta = new HashMap<>(); + } + if (GrpcUtils.INTER_PROCESS_DISABLE != port) { + meta.put(GrpcUtils.CLOUD_DISCOVERY_METADATA_PORT, Integer.toString(port)); + consulRegistration.getService().setMeta(meta); + } + } + } +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcMetadataEurekaConfiguration.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcMetadataEurekaConfiguration.java new file mode 100644 index 000000000..10026b93f --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcMetadataEurekaConfiguration.java @@ -0,0 +1,59 @@ +/* + * 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.server.autoconfigure; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.netflix.eureka.serviceregistry.EurekaRegistration; +import org.springframework.context.annotation.Configuration; + +import net.devh.boot.grpc.common.util.GrpcUtils; +import net.devh.boot.grpc.server.config.GrpcServerProperties; + +/** + * Configuration class that configures the required beans for grpc discovery via Eureka. + * + * @author Michael (yidongnan@gmail.com) + * @since 5/17/16 + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties +@ConditionalOnClass({EurekaRegistration.class}) +public class GrpcMetadataEurekaConfiguration { + + @Autowired(required = false) + private EurekaRegistration eurekaRegistration; + + @Autowired + private GrpcServerProperties grpcProperties; + + @PostConstruct + public void init() { + if (eurekaRegistration != null) { + final int port = grpcProperties.getPort(); + if (GrpcUtils.INTER_PROCESS_DISABLE != port) { + eurekaRegistration.getInstanceConfig().getMetadataMap() + .put(GrpcUtils.CLOUD_DISCOVERY_METADATA_PORT, Integer.toString(port)); + } + } + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcMetadataNacosConfiguration.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcMetadataNacosConfiguration.java new file mode 100644 index 000000000..269573c8f --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcMetadataNacosConfiguration.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.server.autoconfigure; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import com.alibaba.cloud.nacos.registry.NacosRegistration; + +import net.devh.boot.grpc.common.util.GrpcUtils; +import net.devh.boot.grpc.server.config.GrpcServerProperties; + +/** + * Configuration class that configures the required beans for grpc discovery via Nacos. + * + * @author Michael (yidongnan@gmail.com) + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties +@ConditionalOnClass({NacosRegistration.class}) +public class GrpcMetadataNacosConfiguration { + + @Autowired(required = false) + private NacosRegistration nacosRegistration; + + @Autowired + private GrpcServerProperties grpcProperties; + + @PostConstruct + public void init() { + if (nacosRegistration != null) { + final int port = grpcProperties.getPort(); + if (GrpcUtils.INTER_PROCESS_DISABLE != port) { + nacosRegistration.getMetadata() + .put(GrpcUtils.CLOUD_DISCOVERY_METADATA_PORT, Integer.toString(port)); + } + } + + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcMetadataZookeeperConfiguration.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcMetadataZookeeperConfiguration.java new file mode 100644 index 000000000..5131d2556 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcMetadataZookeeperConfiguration.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2016-2020 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.server.autoconfigure; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.zookeeper.serviceregistry.ZookeeperRegistration; +import org.springframework.context.annotation.Configuration; + +import net.devh.boot.grpc.common.util.GrpcUtils; +import net.devh.boot.grpc.server.config.GrpcServerProperties; + +/** + * Configuration class that configures the required beans for grpc discovery via Zookeeper. + * + * @author zhaochunlin (946599275@gmail.com) + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties +@ConditionalOnClass({ZookeeperRegistration.class}) +public class GrpcMetadataZookeeperConfiguration { + + @Autowired(required = false) + private ZookeeperRegistration zookeeperRegistration; + + @Autowired + private GrpcServerProperties grpcServerProperties; + + + @PostConstruct + public void init() { + if (zookeeperRegistration != null) { + final int port = grpcServerProperties.getPort(); + zookeeperRegistration.setPort(0); + if (GrpcUtils.INTER_PROCESS_DISABLE != port) { + zookeeperRegistration.getServiceInstance().getPayload().getMetadata() + .put(GrpcUtils.CLOUD_DISCOVERY_METADATA_PORT, Integer.toString(port)); + } + } + } +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcReflectionServiceAutoConfiguration.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcReflectionServiceAutoConfiguration.java new file mode 100644 index 000000000..eb08c9c58 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcReflectionServiceAutoConfiguration.java @@ -0,0 +1,52 @@ +/* + * 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.server.autoconfigure; + +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.grpc.BindableService; +import io.grpc.protobuf.services.ProtoReflectionService; +import net.devh.boot.grpc.server.service.GrpcService; + +/** + * Auto configuration that sets up the proto reflection service. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Configuration +@ConditionalOnClass(ProtoReflectionService.class) +@ConditionalOnProperty(prefix = "grpc.server", name = "reflection-service-enabled", matchIfMissing = true) +@AutoConfigureBefore(GrpcServerFactoryAutoConfiguration.class) +public class GrpcReflectionServiceAutoConfiguration { + + /** + * Creates a new ProtoReflectionService instance. + * + * @return The newly created bean. + */ + @Bean + @GrpcService + BindableService protoReflectionService() { + return ProtoReflectionService.newInstance(); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java new file mode 100644 index 000000000..009416f19 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerAutoConfiguration.java @@ -0,0 +1,137 @@ +/* + * 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.server.autoconfigure; + +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +import io.grpc.CompressorRegistry; +import io.grpc.DecompressorRegistry; +import io.grpc.Server; +import net.devh.boot.grpc.common.autoconfigure.GrpcCommonCodecAutoConfiguration; +import net.devh.boot.grpc.server.config.GrpcServerProperties; +import net.devh.boot.grpc.server.interceptor.AnnotationGlobalServerInterceptorConfigurer; +import net.devh.boot.grpc.server.interceptor.GlobalServerInterceptorRegistry; +import net.devh.boot.grpc.server.nameresolver.SelfNameResolverFactory; +import net.devh.boot.grpc.server.scope.GrpcRequestScope; +import net.devh.boot.grpc.server.serverfactory.GrpcServerConfigurer; +import net.devh.boot.grpc.server.serverfactory.GrpcServerFactory; +import net.devh.boot.grpc.server.serverfactory.GrpcServerLifecycle; +import net.devh.boot.grpc.server.service.AnnotationGrpcServiceDiscoverer; +import net.devh.boot.grpc.server.service.GrpcServiceDiscoverer; + +/** + * The auto configuration used by Spring-Boot that contains all beans to run a grpc server/service. + * + * @author Michael (yidongnan@gmail.com) + * @since 5/17/16 + */ +@Configuration(proxyBeanMethods = false) +@EnableConfigurationProperties +@ConditionalOnClass(Server.class) +@AutoConfigureAfter(GrpcCommonCodecAutoConfiguration.class) +public class GrpcServerAutoConfiguration { + + /** + * A scope that is valid for the duration of a grpc request. + * + * @return The grpc request scope bean. + */ + @Bean + public static GrpcRequestScope grpcRequestScope() { + return new GrpcRequestScope(); + } + + @ConditionalOnMissingBean + @Bean + public GrpcServerProperties defaultGrpcServerProperties() { + return new GrpcServerProperties(); + } + + /** + * Lazily creates a {@link SelfNameResolverFactory} bean, that can be used by the client to connect to the server + * itself. + * + * @param properties The properties to derive the address from. + * @return The newly created {@link SelfNameResolverFactory} bean. + */ + @ConditionalOnMissingBean + @Bean + @Lazy + public SelfNameResolverFactory selfNameResolverFactory(final GrpcServerProperties properties) { + return new SelfNameResolverFactory(properties); + } + + @ConditionalOnMissingBean + @Bean + GlobalServerInterceptorRegistry globalServerInterceptorRegistry( + final ApplicationContext applicationContext) { + return new GlobalServerInterceptorRegistry(applicationContext); + } + + @Bean + @Lazy + AnnotationGlobalServerInterceptorConfigurer annotationGlobalServerInterceptorConfigurer( + final ApplicationContext applicationContext) { + return new AnnotationGlobalServerInterceptorConfigurer(applicationContext); + } + + @ConditionalOnMissingBean + @Bean + public GrpcServiceDiscoverer defaultGrpcServiceDiscoverer() { + return new AnnotationGrpcServiceDiscoverer(); + } + + @ConditionalOnBean(CompressorRegistry.class) + @Bean + public GrpcServerConfigurer compressionServerConfigurer(final CompressorRegistry registry) { + return builder -> builder.compressorRegistry(registry); + } + + @ConditionalOnBean(DecompressorRegistry.class) + @Bean + public GrpcServerConfigurer decompressionServerConfigurer(final DecompressorRegistry registry) { + return builder -> builder.decompressorRegistry(registry); + } + + @ConditionalOnMissingBean(GrpcServerConfigurer.class) + @Bean + public List defaultServerConfigurers() { + return Collections.emptyList(); + } + + @ConditionalOnMissingBean + @ConditionalOnBean(GrpcServerFactory.class) + @Bean + public GrpcServerLifecycle grpcServerLifecycle( + final GrpcServerFactory factory, + final GrpcServerProperties properties) { + return new GrpcServerLifecycle(factory, properties.getShutdownGracePeriod()); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java new file mode 100644 index 000000000..f2812a6fe --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerFactoryAutoConfiguration.java @@ -0,0 +1,176 @@ +/* + * 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.server.autoconfigure; + +import java.util.List; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; + +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.condition.ConditionalOnInterprocessServer; +import net.devh.boot.grpc.server.config.GrpcServerProperties; +import net.devh.boot.grpc.server.serverfactory.GrpcServerConfigurer; +import net.devh.boot.grpc.server.serverfactory.GrpcServerFactory; +import net.devh.boot.grpc.server.serverfactory.GrpcServerLifecycle; +import net.devh.boot.grpc.server.serverfactory.InProcessGrpcServerFactory; +import net.devh.boot.grpc.server.serverfactory.NettyGrpcServerFactory; +import net.devh.boot.grpc.server.serverfactory.ShadedNettyGrpcServerFactory; +import net.devh.boot.grpc.server.service.GrpcServiceDefinition; +import net.devh.boot.grpc.server.service.GrpcServiceDiscoverer; + +/** + * The auto configuration that will create the {@link GrpcServerFactory}s and {@link GrpcServerLifecycle}s, if the + * developer hasn't specified their own variant. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean({GrpcServerFactory.class, GrpcServerLifecycle.class}) +@AutoConfigureAfter(GrpcServerAutoConfiguration.class) +public class GrpcServerFactoryAutoConfiguration { + + // First try the shaded netty server + /** + * Creates a GrpcServerFactory using the shaded netty. This is the recommended default for gRPC. + * + * @param properties The properties used to configure the server. + * @param serviceDiscoverer The discoverer used to identify the services that should be served. + * @param serverConfigurers The server configurers that contain additional configuration for the server. + * @return The shadedNettyGrpcServerFactory bean. + */ + @ConditionalOnClass(name = {"io.grpc.netty.shaded.io.netty.channel.Channel", + "io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder"}) + @Conditional(ConditionalOnInterprocessServer.class) + @Bean + public ShadedNettyGrpcServerFactory shadedNettyGrpcServerFactory( + final GrpcServerProperties properties, + final GrpcServiceDiscoverer serviceDiscoverer, + final List serverConfigurers) { + + log.info("Detected grpc-netty-shaded: Creating ShadedNettyGrpcServerFactory"); + final ShadedNettyGrpcServerFactory factory = new ShadedNettyGrpcServerFactory(properties, serverConfigurers); + for (final GrpcServiceDefinition service : serviceDiscoverer.findGrpcServices()) { + factory.addService(service); + } + return factory; + } + + /** + * The server lifecycle bean for a shaded netty based server. + * + * @param factory The factory used to create the lifecycle. + * @param properties The server properties to use. + * @return The inter-process server lifecycle bean. + */ + @ConditionalOnBean(ShadedNettyGrpcServerFactory.class) + @Bean + public GrpcServerLifecycle shadedNettyGrpcServerLifecycle( + final ShadedNettyGrpcServerFactory factory, + final GrpcServerProperties properties) { + + return new GrpcServerLifecycle(factory, properties.getShutdownGracePeriod()); + } + + // Then try the normal netty server + /** + * Creates a GrpcServerFactory using the non-shaded netty. This is the fallback, if the shaded one is not present. + * + * @param properties The properties used to configure the server. + * @param serviceDiscoverer The discoverer used to identify the services that should be served. + * @param serverConfigurers The server configurers that contain additional configuration for the server. + * @return The shadedNettyGrpcServerFactory bean. + */ + @ConditionalOnMissingBean(ShadedNettyGrpcServerFactory.class) + @Conditional(ConditionalOnInterprocessServer.class) + @ConditionalOnClass(name = {"io.netty.channel.Channel", "io.grpc.netty.NettyServerBuilder"}) + @Bean + public NettyGrpcServerFactory nettyGrpcServerFactory( + final GrpcServerProperties properties, + final GrpcServiceDiscoverer serviceDiscoverer, + final List serverConfigurers) { + + log.info("Detected grpc-netty: Creating NettyGrpcServerFactory"); + final NettyGrpcServerFactory factory = new NettyGrpcServerFactory(properties, serverConfigurers); + for (final GrpcServiceDefinition service : serviceDiscoverer.findGrpcServices()) { + factory.addService(service); + } + return factory; + } + + /** + * The server lifecycle bean for netty based server. + * + * @param factory The factory used to create the lifecycle. + * @param properties The server properties to use. + * @return The inter-process server lifecycle bean. + */ + @ConditionalOnBean(NettyGrpcServerFactory.class) + @Bean + public GrpcServerLifecycle nettyGrpcServerLifecycle( + final NettyGrpcServerFactory factory, + final GrpcServerProperties properties) { + + return new GrpcServerLifecycle(factory, properties.getShutdownGracePeriod()); + } + + /** + * Creates a GrpcServerFactory using the in-process-server, if a name is specified. + * + * @param properties The properties used to configure the server. + * @param serviceDiscoverer The discoverer used to identify the services that should be served. + * @return The shadedNettyGrpcServerFactory bean. + */ + @ConditionalOnProperty(prefix = "grpc.server", name = "in-process-name") + @Bean + public InProcessGrpcServerFactory inProcessGrpcServerFactory( + final GrpcServerProperties properties, + final GrpcServiceDiscoverer serviceDiscoverer) { + + log.info("'grpc.server.in-process-name' is set: Creating InProcessGrpcServerFactory"); + final InProcessGrpcServerFactory factory = new InProcessGrpcServerFactory(properties); + for (final GrpcServiceDefinition service : serviceDiscoverer.findGrpcServices()) { + factory.addService(service); + } + return factory; + } + + /** + * The server lifecycle bean for the in-process-server. + * + * @param factory The factory used to create the lifecycle. + * @param properties The server properties to use. + * @return The in-process server lifecycle bean. + */ + @ConditionalOnBean(InProcessGrpcServerFactory.class) + @Bean + public GrpcServerLifecycle inProcessGrpcServerLifecycle( + final InProcessGrpcServerFactory factory, + final GrpcServerProperties properties) { + + return new GrpcServerLifecycle(factory, properties.getShutdownGracePeriod()); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerMetricAutoConfiguration.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerMetricAutoConfiguration.java new file mode 100644 index 000000000..ccde26df3 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerMetricAutoConfiguration.java @@ -0,0 +1,110 @@ +/* + * 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.server.autoconfigure; + +import static net.devh.boot.grpc.common.util.GrpcUtils.extractMethodName; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; +import org.springframework.boot.actuate.info.InfoContributor; +import org.springframework.boot.actuate.info.SimpleInfoContributor; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +import io.grpc.BindableService; +import io.grpc.MethodDescriptor; +import io.grpc.ServiceDescriptor; +import io.micrometer.core.instrument.MeterRegistry; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.config.GrpcServerProperties; +import net.devh.boot.grpc.server.metric.MetricCollectingServerInterceptor; + +/** + * Auto configuration class for Spring-Boot. This allows zero config server metrics for gRPC services. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter(CompositeMeterRegistryAutoConfiguration.class) +@AutoConfigureBefore(GrpcServerAutoConfiguration.class) +@ConditionalOnBean(MeterRegistry.class) +public class GrpcServerMetricAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public MetricCollectingServerInterceptor metricCollectingServerInterceptor(final MeterRegistry registry, + final Collection services) { + final MetricCollectingServerInterceptor metricCollector = new MetricCollectingServerInterceptor(registry); + log.debug("Pre-Registering service metrics"); + for (final BindableService service : services) { + log.debug("- {}", service); + metricCollector.preregisterService(service); + } + return metricCollector; + } + + @Bean + @Lazy + InfoContributor grpcInfoContributor(final GrpcServerProperties properties, + final Collection grpcServices) { + final Map details = new LinkedHashMap<>(); + details.put("port", properties.getPort()); + + if (properties.isReflectionServiceEnabled()) { + // Only expose services via web-info if we do the same via grpc. + final Map> services = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + details.put("services", services); + for (final BindableService grpcService : grpcServices) { + final ServiceDescriptor serviceDescriptor = grpcService.bindService().getServiceDescriptor(); + + final List methods = collectMethodNamesForService(serviceDescriptor); + services.put(serviceDescriptor.getName(), methods); + } + } + + return new SimpleInfoContributor("grpc.server", details); + } + + /** + * Gets all method names from the given service descriptor. + * + * @param serviceDescriptor The service descriptor to get the names from. + * @return The newly created and sorted list of the method names. + */ + protected List collectMethodNamesForService(final ServiceDescriptor serviceDescriptor) { + final List methods = new ArrayList<>(); + for (final MethodDescriptor grpcMethod : serviceDescriptor.getMethods()) { + methods.add(extractMethodName(grpcMethod)); + } + methods.sort(String.CASE_INSENSITIVE_ORDER); + return methods; + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerSecurityAutoConfiguration.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerSecurityAutoConfiguration.java new file mode 100644 index 000000000..d9ea3dc4e --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerSecurityAutoConfiguration.java @@ -0,0 +1,110 @@ +/* + * 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.server.autoconfigure; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.AccessDecisionManager; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.core.AuthenticationException; + +import net.devh.boot.grpc.server.security.authentication.GrpcAuthenticationReader; +import net.devh.boot.grpc.server.security.check.GrpcSecurityMetadataSource; +import net.devh.boot.grpc.server.security.interceptors.AuthenticatingServerInterceptor; +import net.devh.boot.grpc.server.security.interceptors.AuthorizationCheckingServerInterceptor; +import net.devh.boot.grpc.server.security.interceptors.DefaultAuthenticatingServerInterceptor; +import net.devh.boot.grpc.server.security.interceptors.ExceptionTranslatingServerInterceptor; + +/** + * Auto configuration class with the required beans for the spring-security configuration of the grpc server. + * + *

+ * To enable security add both an {@link AuthenticationManager} and a {@link GrpcAuthenticationReader} to the + * application context. The authentication reader obtains the credentials from the requests which then will be validated + * by the authentication manager. After that, you can decide how you want to secure your application. Currently these + * options are available: + *

+ * + *
    + *
  • Use Spring Security's annotations. This requires + * {@code @EnableGlobalMethodSecurity(proxyTargetClass = true, ...)}.
  • + *
  • Having both an {@link AccessDecisionManager} and a {@link GrpcSecurityMetadataSource} in the application context. + *
+ * + *

+ * Note: The order of the beans is important! First the exception translating interceptor, then the + * authenticating interceptor and finally the authorization checking interceptor. That is necessary because they are + * executed in the same order as their order. + *

+ * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnBean(AuthenticationManager.class) +@AutoConfigureAfter(SecurityAutoConfiguration.class) +public class GrpcServerSecurityAutoConfiguration { + + /** + * The interceptor for handling security related exceptions such as {@link AuthenticationException} and + * {@link AccessDeniedException}. + * + * @return The exceptionTranslatingServerInterceptor bean. + */ + @Bean + @ConditionalOnMissingBean + public ExceptionTranslatingServerInterceptor exceptionTranslatingServerInterceptor() { + return new ExceptionTranslatingServerInterceptor(); + } + + /** + * The security interceptor that handles the authentication of requests. + * + * @param authenticationManager The authentication manager used to verify the credentials. + * @param authenticationReader The authentication reader used to extract the credentials from the call. + * @return The authenticatingServerInterceptor bean. + */ + @Bean + @ConditionalOnMissingBean(AuthenticatingServerInterceptor.class) + public DefaultAuthenticatingServerInterceptor authenticatingServerInterceptor( + final AuthenticationManager authenticationManager, + final GrpcAuthenticationReader authenticationReader) { + return new DefaultAuthenticatingServerInterceptor(authenticationManager, authenticationReader); + } + + /** + * The security interceptor that handles the authorization of requests. + * + * @param accessDecisionManager The access decision manager used to check the requesting user. + * @param securityMetadataSource The source for the security metadata (access constraints). + * @return The authorizationCheckingServerInterceptor bean. + */ + @Bean + @ConditionalOnMissingBean + @ConditionalOnBean({AccessDecisionManager.class, GrpcSecurityMetadataSource.class}) + public AuthorizationCheckingServerInterceptor authorizationCheckingServerInterceptor( + final AccessDecisionManager accessDecisionManager, + final GrpcSecurityMetadataSource securityMetadataSource) { + return new AuthorizationCheckingServerInterceptor(accessDecisionManager, securityMetadataSource); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerTraceAutoConfiguration.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerTraceAutoConfiguration.java new file mode 100644 index 000000000..cc5074728 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/GrpcServerTraceAutoConfiguration.java @@ -0,0 +1,56 @@ +/* + * 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.server.autoconfigure; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Configuration; + +import brave.grpc.GrpcTracing; +import io.grpc.ServerInterceptor; +import net.devh.boot.grpc.common.autoconfigure.GrpcCommonTraceAutoConfiguration; +import net.devh.boot.grpc.common.util.InterceptorOrder; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; +import net.devh.boot.grpc.server.interceptor.OrderedServerInterceptor; + +/** + * The configuration used to configure brave's tracing for grpc. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(value = "spring.sleuth.grpc.enabled", matchIfMissing = true) +@AutoConfigureAfter(GrpcCommonTraceAutoConfiguration.class) +@ConditionalOnBean(GrpcTracing.class) +public class GrpcServerTraceAutoConfiguration { + + /** + * Configures a global server interceptor that applies brave's tracing logic to the requests. + * + * @param grpcTracing The grpc tracing bean. + * @return The tracing server interceptor bean. + */ + @GrpcGlobalServerInterceptor + public ServerInterceptor globalTraceServerInterceptorConfigurer(final GrpcTracing grpcTracing) { + return new OrderedServerInterceptor( + grpcTracing.newServerInterceptor(), + InterceptorOrder.ORDER_TRACING_METRICS + 1); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/package-info.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/package-info.java new file mode 100644 index 000000000..8698ae8a2 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/autoconfigure/package-info.java @@ -0,0 +1,5 @@ +/** + * The Spring-Boot auto configuration classes that setup the gRPC server environment. + */ + +package net.devh.boot.grpc.server.autoconfigure; diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/cloud/package-info.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/cloud/package-info.java new file mode 100644 index 000000000..52048df0a --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/cloud/package-info.java @@ -0,0 +1,5 @@ +/** + * Helper classes for Spring-Cloud server features. + */ + +package net.devh.boot.grpc.server.cloud; diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/condition/ConditionalOnInterprocessServer.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/condition/ConditionalOnInterprocessServer.java new file mode 100644 index 000000000..5f148f413 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/condition/ConditionalOnInterprocessServer.java @@ -0,0 +1,38 @@ +/* + * 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.server.condition; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.NoneNestedConditions; + +/** + * A condition that matches if the {@code grpc.server.port} does not have the value {@code -1}. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public class ConditionalOnInterprocessServer extends NoneNestedConditions { + + ConditionalOnInterprocessServer() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(name = "grpc.server.port", havingValue = "-1") + static class NoServerPortCondition { + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/config/ClientAuth.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/config/ClientAuth.java new file mode 100644 index 000000000..695f7b721 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/config/ClientAuth.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.server.config; + +import javax.net.ssl.SSLEngine; + +/** + * Indicates the state of the {@link SSLEngine} with respect to client authentication. This configuration item really + * only applies when building the server-side SslContext. + */ +public enum ClientAuth { + + /** + * Indicates that the {@link SSLEngine} will not request client authentication. + */ + NONE, + + /** + * Indicates that the {@link SSLEngine} will request client authentication. + */ + OPTIONAL, + + /** + * Indicates that the {@link SSLEngine} will require client authentication. + */ + REQUIRE; + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/config/GrpcServerProperties.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/config/GrpcServerProperties.java new file mode 100644 index 000000000..50cbe170b --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/config/GrpcServerProperties.java @@ -0,0 +1,393 @@ +/* + * 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.server.config; + +import java.io.File; +import java.io.InputStream; +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.convert.DataSizeUnit; +import org.springframework.boot.convert.DurationUnit; +import org.springframework.core.io.Resource; +import org.springframework.util.SocketUtils; +import org.springframework.util.unit.DataSize; +import org.springframework.util.unit.DataUnit; + +import io.grpc.ServerBuilder; +import io.grpc.internal.GrpcUtil; +import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; +import lombok.Data; +import net.devh.boot.grpc.common.util.GrpcUtils; + +/** + * The properties for the gRPC server that will be started as part of the application. + * + * @author Michael (yidongnan@gmail.com) + * @since 5/17/16 + */ +@Data +@ConfigurationProperties("grpc.server") +@SuppressWarnings("javadoc") +public class GrpcServerProperties { + + /** + * A constant that defines, that the server should listen to any IPv4 and IPv6 address. + */ + public static final String ANY_IP_ADDRESS = "*"; + + /** + * A constant that defines, that the server should listen to any IPv4 address. + */ + public static final String ANY_IPv4_ADDRESS = "0.0.0.0"; + + /** + * A constant that defines, that the server should listen to any IPv6 address. + */ + public static final String ANY_IPv6_ADDRESS = "::"; + + /** + * Bind address for the server. Defaults to {@link #ANY_IP_ADDRESS "*"}. Alternatively you can restrict this to + * {@link #ANY_IPv4_ADDRESS "0.0.0.0"} or {@link #ANY_IPv6_ADDRESS "::"}. Or restrict it to exactly one IP address. + * On unix systems it is also possible to prefix it with {@link GrpcUtils#DOMAIN_SOCKET_ADDRESS_PREFIX unix:} to use + * domain socket addresses/paths (Additional dependencies may be required). + * + * @param address The address to bind to. + * @return The address the server should bind to. + */ + private String address = ANY_IP_ADDRESS; + + /** + * Server port to listen on. Defaults to {@code 9090}. If set to {@code 0} a random available port will be selected + * and used. Use {@code -1} to disable the inter-process server (for example if you only want to use the in-process + * server). + * + * @param port The port the server should listen on. + * @return The port the server will listen on. + */ + private int port = 9090; + + /** + * The name of the in-process server. If not set, then the in process server won't be started. + * + * @param inProcessName The name of the in-process server. + * @return The name of the in-process server or null if isn't configured. + */ + private String inProcessName; + + /** + * The time to wait for the server to gracefully shutdown (completing all requests after the server started to + * shutdown). If set to a negative value, the server waits forever. If set to {@code 0} the server will force + * shutdown immediately. Defaults to {@code 30s}. + * + * @param gracefullShutdownTimeout The time to wait for a graceful shutdown. + * @return The time to wait for a graceful shutdown. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration shutdownGracePeriod = Duration.of(30, ChronoUnit.SECONDS); + + /** + * Setting to enable keepAlive. Default to {@code false}. + * + * @param enableKeepAlive Whether keep alive should be enabled. + * @return True, if keep alive should be enabled. False otherwise. + */ + private boolean enableKeepAlive = false; + + /** + * The default delay before we send a keepAlives. Defaults to {@code 2h}. Default unit {@link ChronoUnit#SECONDS + * SECONDS}. + * + * @see #setEnableKeepAlive(boolean) + * @see NettyServerBuilder#keepAliveTime(long, TimeUnit) + * + * @param keepAliveTime The new default delay before sending keepAlives. + * @return The default delay before sending keepAlives. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration keepAliveTime = Duration.of(2, ChronoUnit.HOURS); + + /** + * The default timeout for a keepAlives ping request. Defaults to {@code 20s}. Default unit + * {@link ChronoUnit#SECONDS SECONDS}. + * + * @see #setEnableKeepAlive(boolean) + * @see NettyServerBuilder#keepAliveTimeout(long, TimeUnit) + * + * @param keepAliveTimeout Sets the default timeout for a keepAlives ping request. + * @return The default timeout for a keepAlives ping request. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration keepAliveTimeout = Duration.of(20, ChronoUnit.SECONDS); + + /** + * Specify the most aggressive keep-alive time clients are permitted to configure. Defaults to {@code 5min}. Default + * unit {@link ChronoUnit#SECONDS SECONDS}. + * + * @see NettyServerBuilder#permitKeepAliveTime(long, TimeUnit) + * + * @param permitKeepAliveTime The most aggressive keep-alive time clients are permitted to configure. + * @return The most aggressive keep-alive time clients are permitted to configure. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration permitKeepAliveTime = Duration.of(5, ChronoUnit.MINUTES); + + /** + * Whether clients are allowed to send keep-alive HTTP/2 PINGs even if there are no outstanding RPCs on the + * connection. Defaults to {@code false}. + * + * @see NettyServerBuilder#permitKeepAliveWithoutCalls(boolean) + * + * @param permitKeepAliveWithoutCalls Whether to allow clients to send keep-alive requests without calls. + * @return True, if clients are allowed to send keep-alive requests without calls. False otherwise. + */ + @DurationUnit(ChronoUnit.SECONDS) + private boolean permitKeepAliveWithoutCalls = false; + + /** + * Specify a max connection idle time. Defaults to disabled. Default unit {@link ChronoUnit#SECONDS SECONDS}. + * + * @see NettyServerBuilder#maxConnectionIdle(long, TimeUnit) + * + * @param maxConnectionIdle The max connection idle time. + * @return The max connection idle time. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration maxConnectionIdle = null; + + /** + * Specify a max connection age. Defaults to disabled. Default unit {@link ChronoUnit#SECONDS SECONDS}. + * + * @see NettyServerBuilder#maxConnectionAge(long, TimeUnit) + * + * @param maxConnectionAge The max connection age. + * @return The max connection age. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration maxConnectionAge = null; + + /** + * Specify a grace time for the graceful max connection age termination. Defaults to disabled. Default unit + * {@link ChronoUnit#SECONDS SECONDS}. + * + * @see NettyServerBuilder#maxConnectionAgeGrace(long, TimeUnit) + * + * @param maxConnectionAgeGrace The max connection age grace time. + * @return The max connection age grace time. + */ + @DurationUnit(ChronoUnit.SECONDS) + private Duration maxConnectionAgeGrace = null; + + /** + * The maximum message size allowed to be received by the server. If not set ({@code null}) then + * {@link GrpcUtil#DEFAULT_MAX_MESSAGE_SIZE gRPC's default} should be used. + * + * @return The maximum message size allowed. + */ + @DataSizeUnit(DataUnit.BYTES) + private DataSize maxInboundMessageSize = null; + + /** + * The maximum size of metadata allowed to be received. If not set ({@code null}) then + * {@link GrpcUtil#DEFAULT_MAX_HEADER_LIST_SIZE gRPC's default} should be used. + * + * @return The maximum metadata size allowed. + */ + @DataSizeUnit(DataUnit.BYTES) + private DataSize maxInboundMetadataSize = null; + + /** + * Whether gRPC health service is enabled or not. Defaults to {@code true}. + * + * @param healthServiceEnabled Whether gRPC health service is enabled. + * @return True, if the health service is enabled. False otherwise. + */ + private boolean healthServiceEnabled = true; + + /** + * Whether proto reflection service is enabled or not. Defaults to {@code true}. + * + * @param reflectionServiceEnabled Whether gRPC reflection service is enabled. + * @return True, if the reflection service is enabled. False otherwise. + */ + private boolean reflectionServiceEnabled = true; + + /** + * Security options for transport security. Defaults to disabled. We strongly recommend to enable this though. + * + * @return The security options for transport security. + */ + private final Security security = new Security(); + + /** + * The security configuration for the gRPC server. + */ + @Data + public static class Security { + + /** + * Flag that controls whether transport security is used. Defaults to {@code false}. + * + * @param enabled Whether transport security should be enabled. + * @return True, if transport security should be enabled. False otherwise. + */ + private boolean enabled = false; + + /** + * The resource containing the SSL certificate chain. Required if {@link #isEnabled()} is true. + * + * @see GrpcSslContexts#forServer(InputStream, InputStream, String) + * + * @param certificateChain The certificate chain resource. + * @return The certificate chain resource or null, if security is not enabled. + */ + private Resource certificateChain = null; + + /** + * The resource containing the private key. Required if {@link #enabled} is true. + * + * @see GrpcSslContexts#forServer(InputStream, InputStream, String) + * + * @param privateKey The private key resource. + * @return The private key resource or null, if security is not enabled. + */ + private Resource privateKey = null; + + /** + * Password for the private key. + * + * @see GrpcSslContexts#forServer(File, File, String) + * + * @param privateKeyPassword The password for the private key. + * @return The password for the private key or null, if the private key is not set or not encrypted. + */ + private String privateKeyPassword = null; + + /** + * Whether the client has to authenticate himself via certificates. Can be either of {@link ClientAuth#NONE + * NONE}, {@link ClientAuth#OPTIONAL OPTIONAL} or {@link ClientAuth#REQUIRE REQUIRE}. Defaults to + * {@link ClientAuth#NONE}. + * + * @see SslContextBuilder#clientAuth(io.grpc.netty.shaded.io.netty.handler.ssl.ClientAuth) + * SslContextBuilder#clientAuth(ClientAuth) + * + * @param clientAuth Whether the client has to authenticate himself via certificates. + * @return Whether the client has to authenticate himself via certificates. + */ + private ClientAuth clientAuth = ClientAuth.NONE; + + /** + * The resource containing the trusted certificate collection. If {@code null} or empty it will use the system's + * default collection (Default). This collection will be used to verify client certificates. + * + * @see SslContextBuilder#trustManager(InputStream) + * + * @param trustCertCollection The trusted certificate collection resource. + * @return The trusted certificate collection resource or null. + */ + private Resource trustCertCollection = null; + + /** + * Specifies the cipher suite. If {@code null} or empty it will use the system's default cipher suite. + * + * @param ciphers List of allowed ciphers + * @return The cipher suite accepted for secure connections or null. + */ + private List ciphers = null; + + public void setCiphers(final String ciphers) { + this.ciphers = Arrays.asList(ciphers.split("[ :,]")); + } + + /** + * Specifies the protocols accepted for secure connections. If {@code null} or empty it will use the system's + * default (all supported) protocols. + * + * @param protocols Protocol list consisting of one or more protocols separated by colons, commas or spaces. + * @return The protocols accepted for secure connections or null. + */ + private String[] protocols = null; + + public void setProtocols(final String protocols) { + this.protocols = protocols.split("[ :,]"); + } + + } + + /** + * Gets the port the server should listen on. Defaults to {@code 9090}. If set to {@code 0} a random available port + * will be selected and used. + * + * @return The server port to listen to. + * + * @see #setPort(int) + */ + public int getPort() { + if (this.port == 0) { + this.port = SocketUtils.findAvailableTcpPort(); + } + return this.port; + } + + /** + * Sets the maximum message size allowed to be received by the server. If not set ({@code null}) then it will + * default to {@link GrpcUtil#DEFAULT_MAX_MESSAGE_SIZE gRPC's default}. If set to {@code -1} then it will use the + * highest possible limit (not recommended). + * + * @param maxInboundMessageSize The new maximum size allowed for incoming messages. {@code -1} for max possible. + * Null to use the gRPC's default. + * + * @see ServerBuilder#maxInboundMessageSize(int) + */ + public void setMaxInboundMessageSize(final DataSize maxInboundMessageSize) { + if (maxInboundMessageSize == null || maxInboundMessageSize.toBytes() >= 0) { + this.maxInboundMessageSize = maxInboundMessageSize; + } else if (maxInboundMessageSize.toBytes() == -1) { + this.maxInboundMessageSize = DataSize.ofBytes(Integer.MAX_VALUE); + } else { + throw new IllegalArgumentException("Unsupported maxInboundMessageSize: " + maxInboundMessageSize); + } + } + + /** + * Sets the maximum metadata size allowed to be received by the server. If not set ({@code null}) then it will + * default to {@link GrpcUtil#DEFAULT_MAX_HEADER_LIST_SIZE gRPC's default}. If set to {@code -1} then it will use + * the highest possible limit (not recommended). + * + * @param maxInboundMetadataSize The new maximum size allowed for incoming metadata. {@code -1} for max possible. + * Null to use the gRPC's default. + * + * @see ServerBuilder#maxInboundMetadataSize(int) + */ + public void setMaxInboundMetadataSize(final DataSize maxInboundMetadataSize) { + if (maxInboundMetadataSize == null || maxInboundMetadataSize.toBytes() >= 0) { + this.maxInboundMetadataSize = maxInboundMetadataSize; + } else if (maxInboundMetadataSize.toBytes() == -1) { + this.maxInboundMetadataSize = DataSize.ofBytes(Integer.MAX_VALUE); + } else { + throw new IllegalArgumentException("Unsupported maxInboundMetadataSize: " + maxInboundMetadataSize); + } + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/config/package-info.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/config/package-info.java new file mode 100644 index 000000000..a6ccdd3dd --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/config/package-info.java @@ -0,0 +1,5 @@ +/** + * Classes related to the gRPC server configuration. + */ + +package net.devh.boot.grpc.server.config; diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/AnnotationGlobalServerInterceptorConfigurer.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/AnnotationGlobalServerInterceptorConfigurer.java new file mode 100644 index 000000000..0a4f951f0 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/AnnotationGlobalServerInterceptorConfigurer.java @@ -0,0 +1,72 @@ +/* + * 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.server.interceptor; + +import static com.google.common.collect.Maps.transformValues; +import static java.util.Objects.requireNonNull; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.context.ApplicationContext; + +import io.grpc.ServerInterceptor; +import lombok.extern.slf4j.Slf4j; + +/** + * Automatically find and configure {@link GrpcGlobalServerInterceptor annotated} global {@link ServerInterceptor}s. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +public class AnnotationGlobalServerInterceptorConfigurer implements GlobalServerInterceptorConfigurer { + + private final ApplicationContext applicationContext; + + /** + * Creates a new AnnotationGlobalServerInterceptorConfigurer. + * + * @param applicationContext The application context to fetch the {@link GrpcGlobalServerInterceptor} annotated + * {@link ServerInterceptor} beans from. + */ + public AnnotationGlobalServerInterceptorConfigurer(final ApplicationContext applicationContext) { + this.applicationContext = requireNonNull(applicationContext, "applicationContext"); + } + + /** + * Helper method used to get the {@link GrpcGlobalServerInterceptor} annotated {@link ServerInterceptor}s from the + * application context. + * + * @return A map containing the global interceptor beans. + */ + protected Map getServerInterceptorBeans() { + return transformValues(this.applicationContext.getBeansWithAnnotation(GrpcGlobalServerInterceptor.class), + ServerInterceptor.class::cast); + } + + @Override + public void configureServerInterceptors(final List interceptors) { + for (final Entry entry : getServerInterceptorBeans().entrySet()) { + final ServerInterceptor interceptor = entry.getValue(); + log.debug("Registering GlobalServerInterceptor: {} ({})", entry.getKey(), interceptor); + interceptors.add(interceptor); + } + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/GlobalServerInterceptorConfigurer.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/GlobalServerInterceptorConfigurer.java new file mode 100644 index 000000000..7b18927b9 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/GlobalServerInterceptorConfigurer.java @@ -0,0 +1,40 @@ +/* + * 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.server.interceptor; + +import java.util.List; + +import io.grpc.ServerInterceptor; + +/** + * A bean that can be used to configure global {@link ServerInterceptor}s. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@FunctionalInterface +public interface GlobalServerInterceptorConfigurer { + + /** + * Configures the given list of server interceptors, possibly adding new elements, removing unwanted elements, or + * reordering the existing ones. + * + * @param interceptors A mutable list of server interceptors to configure. + */ + void configureServerInterceptors(List interceptors); + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/GlobalServerInterceptorRegistry.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/GlobalServerInterceptorRegistry.java new file mode 100644 index 000000000..1ba195629 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/GlobalServerInterceptorRegistry.java @@ -0,0 +1,99 @@ +/* + * 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.server.interceptor; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; + +import com.google.common.collect.ImmutableList; + +import io.grpc.BindableService; +import io.grpc.ServerInterceptor; +import io.grpc.ServerInterceptors; + +/** + * The global server interceptor registry keeps references to all {@link ServerInterceptor}s that should be registered + * to all server channels. The interceptors will be applied in the same order they as specified by the + * {@link #sortInterceptors(List)} method. + * + *

+ * Note: Custom interceptors will be appended to the global interceptors and applied using + * {@link ServerInterceptors#interceptForward(BindableService, ServerInterceptor...)}. + *

+ * + * @author Michael (yidongnan@gmail.com) + */ +public class GlobalServerInterceptorRegistry { + + private final ApplicationContext applicationContext; + + private ImmutableList sortedServerInterceptors; + + /** + * Creates a new GlobalServerInterceptorRegistry. + * + * @param applicationContext The application context to fetch the {@link GlobalServerInterceptorConfigurer} beans + * from. + */ + public GlobalServerInterceptorRegistry(final ApplicationContext applicationContext) { + this.applicationContext = requireNonNull(applicationContext, "applicationContext"); + } + + /** + * Gets the immutable list of global server interceptors. + * + * @return The list of globally registered server interceptors. + */ + public ImmutableList getServerInterceptors() { + if (this.sortedServerInterceptors == null) { + this.sortedServerInterceptors = ImmutableList.copyOf(initServerInterceptors()); + } + return this.sortedServerInterceptors; + } + + /** + * Initializes the list of server interceptors. + * + * @return The list of global server interceptors. + */ + protected List initServerInterceptors() { + final List interceptors = new ArrayList<>(); + for (final GlobalServerInterceptorConfigurer configurer : this.applicationContext + .getBeansOfType(GlobalServerInterceptorConfigurer.class).values()) { + configurer.configureServerInterceptors(interceptors); + } + sortInterceptors(interceptors); + return interceptors; + } + + /** + * Sorts the given list of interceptors. Use this method if you want to sort custom interceptors. The default + * implementation will sort them by using then {@link AnnotationAwareOrderComparator}. + * + * @param interceptors The interceptors to sort. + */ + public void sortInterceptors(final List interceptors) { + interceptors.sort(AnnotationAwareOrderComparator.INSTANCE); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/GrpcGlobalServerInterceptor.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/GrpcGlobalServerInterceptor.java new file mode 100644 index 000000000..03c6fe7ca --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/GrpcGlobalServerInterceptor.java @@ -0,0 +1,42 @@ +/* + * 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.server.interceptor; + +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 org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Component; + +import io.grpc.ServerInterceptor; + +/** + * Annotation for gRPC {@link ServerInterceptor}s to apply them globally. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +@Bean +public @interface GrpcGlobalServerInterceptor { +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/OrderedServerInterceptor.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/OrderedServerInterceptor.java new file mode 100644 index 000000000..e4a4f6fc1 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/OrderedServerInterceptor.java @@ -0,0 +1,67 @@ +/* + * 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.server.interceptor; + +import static java.util.Objects.requireNonNull; + +import org.springframework.core.Ordered; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; + +/** + * A server interceptor wrapper that assigns an order to the underlying server interceptor. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public class OrderedServerInterceptor implements ServerInterceptor, Ordered { + + private final ServerInterceptor serverInterceptor; + private final int order; + + /** + * Creates a new OrderedServerInterceptor with the given server interceptor and order. + * + * @param serverInterceptor The server interceptor to delegate to. + * @param order The order of this interceptor. + */ + public OrderedServerInterceptor(ServerInterceptor serverInterceptor, int order) { + this.serverInterceptor = requireNonNull(serverInterceptor, "serverInterceptor"); + this.order = order; + } + + @Override + public Listener interceptCall(ServerCall call, Metadata headers, + ServerCallHandler next) { + return this.serverInterceptor.interceptCall(call, headers, next); + } + + @Override + public int getOrder() { + return this.order; + } + + @Override + public String toString() { + return "OrderedServerInterceptor [interceptor=" + this.serverInterceptor + ", order=" + this.order + "]"; + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/package-info.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/package-info.java new file mode 100644 index 000000000..6de741b49 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/interceptor/package-info.java @@ -0,0 +1,5 @@ +/** + * Classes related to the gRPC (global) server interceptors and their discovery. + */ + +package net.devh.boot.grpc.server.interceptor; diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/metric/MetricCollectingServerCall.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/metric/MetricCollectingServerCall.java new file mode 100644 index 000000000..12cb654f4 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/metric/MetricCollectingServerCall.java @@ -0,0 +1,69 @@ +/* + * 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.server.metric; + +import io.grpc.ForwardingServerCall.SimpleForwardingServerCall; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.Status; +import io.grpc.Status.Code; +import io.micrometer.core.instrument.Counter; + +/** + * A simple forwarding server call that collects metrics for micrometer. + * + * @param The type of message received one or more times from the client. + * @param The type of message sent one or more times to the client. + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +class MetricCollectingServerCall extends SimpleForwardingServerCall { + + private final Counter responseCounter; + private Code responseCode = Code.UNKNOWN; + + /** + * Creates a new delegating ServerCall that will wrap the given server call to collect metrics. + * + * @param delegate The original call to wrap. + * @param responseCounter The counter for incoming responses. + */ + public MetricCollectingServerCall( + final ServerCall delegate, + final Counter responseCounter) { + + super(delegate); + this.responseCounter = responseCounter; + } + + public Code getResponseCode() { + return this.responseCode; + } + + @Override + public void close(final Status status, final Metadata responseHeaders) { + this.responseCode = status.getCode(); + super.close(status, responseHeaders); + } + + @Override + public void sendMessage(final A responseMessage) { + this.responseCounter.increment(); + super.sendMessage(responseMessage); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/metric/MetricCollectingServerCallListener.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/metric/MetricCollectingServerCallListener.java new file mode 100644 index 000000000..20b75dc29 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/metric/MetricCollectingServerCallListener.java @@ -0,0 +1,83 @@ +/* + * 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.server.metric; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener; +import io.grpc.ServerCall.Listener; +import io.grpc.Status; +import io.micrometer.core.instrument.Counter; + +/** + * A simple forwarding server call listener that collects metrics for micrometer. + * + * @param The type of message received one or more times from the client. + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +class MetricCollectingServerCallListener extends SimpleForwardingServerCallListener { + + private final Counter requestCounter; + private final Supplier responseCodeSupplier; + private final Consumer responseStatusTiming; + + /** + * Creates a new delegating ServerCallListener that will wrap the given server call listener to collect metrics. + * + * @param delegate The original listener to wrap. + * @param requestCounter The counter for incoming requests. + * @param responseCodeSupplier The supplier of the response code. + * @param responseStatusTiming The consumer used to time the processing duration along with a response status. + */ + + public MetricCollectingServerCallListener( + final Listener delegate, + final Counter requestCounter, + final Supplier responseCodeSupplier, + final Consumer responseStatusTiming) { + + super(delegate); + this.requestCounter = requestCounter; + this.responseCodeSupplier = responseCodeSupplier; + this.responseStatusTiming = responseStatusTiming; + } + + @Override + public void onMessage(final Q requestMessage) { + this.requestCounter.increment(); + super.onMessage(requestMessage); + } + + @Override + public void onComplete() { + report(this.responseCodeSupplier.get()); + super.onComplete(); + } + + @Override + public void onCancel() { + report(Status.Code.CANCELLED); + super.onCancel(); + } + + private void report(final Status.Code code) { + this.responseStatusTiming.accept(code); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/metric/MetricCollectingServerInterceptor.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/metric/MetricCollectingServerInterceptor.java new file mode 100644 index 000000000..997ddda97 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/metric/MetricCollectingServerInterceptor.java @@ -0,0 +1,150 @@ +/* + * 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.server.metric; + +import static net.devh.boot.grpc.common.metric.MetricConstants.METRIC_NAME_SERVER_PROCESSING_DURATION; +import static net.devh.boot.grpc.common.metric.MetricConstants.METRIC_NAME_SERVER_REQUESTS_RECEIVED; +import static net.devh.boot.grpc.common.metric.MetricConstants.METRIC_NAME_SERVER_RESPONSES_SENT; +import static net.devh.boot.grpc.common.metric.MetricUtils.prepareCounterFor; +import static net.devh.boot.grpc.common.metric.MetricUtils.prepareTimerFor; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import org.springframework.core.annotation.Order; + +import io.grpc.BindableService; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.ServerServiceDefinition; +import io.grpc.ServiceDescriptor; +import io.grpc.Status; +import io.grpc.Status.Code; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import net.devh.boot.grpc.common.metric.AbstractMetricCollectingInterceptor; +import net.devh.boot.grpc.common.util.InterceptorOrder; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; + +/** + * A gRPC server interceptor that will collect metrics for micrometer. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@GrpcGlobalServerInterceptor +@Order(InterceptorOrder.ORDER_TRACING_METRICS) +public class MetricCollectingServerInterceptor extends AbstractMetricCollectingInterceptor + implements ServerInterceptor { + + /** + * Creates a new gRPC server interceptor that will collect metrics into the given {@link MeterRegistry}. + * + * @param registry The registry to use. + */ + public MetricCollectingServerInterceptor(final MeterRegistry registry) { + super(registry); + } + + /** + * Creates a new gRPC server interceptor that will collect metrics into the given {@link MeterRegistry} and uses the + * given customizer to configure the {@link Counter}s and {@link Timer}s. + * + * @param registry The registry to use. + * @param counterCustomizer The unary function that can be used to customize the created counters. + * @param timerCustomizer The unary function that can be used to customize the created timers. + * @param eagerInitializedCodes The status codes that should be eager initialized. + */ + public MetricCollectingServerInterceptor(final MeterRegistry registry, + final UnaryOperator counterCustomizer, + final UnaryOperator timerCustomizer, final Code... eagerInitializedCodes) { + super(registry, counterCustomizer, timerCustomizer, eagerInitializedCodes); + } + + /** + * Pre-registers the all methods provided by the given service. This will initialize all default counters and timers + * for those methods. + * + * @param service The service to initialize the meters for. + * @see #preregisterService(ServerServiceDefinition) + */ + public void preregisterService(final BindableService service) { + preregisterService(service.bindService()); + } + + /** + * Pre-registers the all methods provided by the given service. This will initialize all default counters and timers + * for those methods. + * + * @param serviceDefinition The service to initialize the meters for. + * @see #preregisterService(ServiceDescriptor) + */ + public void preregisterService(final ServerServiceDefinition serviceDefinition) { + preregisterService(serviceDefinition.getServiceDescriptor()); + } + + @Override + protected Counter newRequestCounterFor(final MethodDescriptor method) { + return this.counterCustomizer.apply( + prepareCounterFor(method, + METRIC_NAME_SERVER_REQUESTS_RECEIVED, + "The total number of requests received")) + .register(this.registry); + } + + @Override + protected Counter newResponseCounterFor(final MethodDescriptor method) { + return this.counterCustomizer.apply( + prepareCounterFor(method, + METRIC_NAME_SERVER_RESPONSES_SENT, + "The total number of responses sent")) + .register(this.registry); + } + + @Override + protected Function newTimerFunction(final MethodDescriptor method) { + return asTimerFunction(() -> this.timerCustomizer.apply( + prepareTimerFor(method, + METRIC_NAME_SERVER_PROCESSING_DURATION, + "The total time taken for the server to complete the call"))); + } + + @Override + public ServerCall.Listener interceptCall( + final ServerCall call, + final Metadata requestHeaders, + final ServerCallHandler next) { + + final MetricSet metrics = metricsFor(call.getMethodDescriptor()); + final Consumer responseStatusTiming = metrics.newProcessingDurationTiming(this.registry); + + final MetricCollectingServerCall monitoringCall = + new MetricCollectingServerCall<>(call, metrics.getResponseCounter()); + + return new MetricCollectingServerCallListener<>( + next.startCall(monitoringCall, requestHeaders), + metrics.getRequestCounter(), + monitoringCall::getResponseCode, + responseStatusTiming); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/metric/package-info.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/metric/package-info.java new file mode 100644 index 000000000..7ad6000a9 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/metric/package-info.java @@ -0,0 +1,5 @@ +/** + * A package containing the server side classes for grpc metric collection. + */ + +package net.devh.boot.grpc.server.metric; diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/nameresolver/SelfNameResolver.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/nameresolver/SelfNameResolver.java new file mode 100644 index 000000000..09bff8ed1 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/nameresolver/SelfNameResolver.java @@ -0,0 +1,186 @@ +/* + * 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.server.nameresolver; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.util.concurrent.Executor; + +import com.google.common.collect.ImmutableList; +import com.google.common.net.InetAddresses; + +import io.grpc.EquivalentAddressGroup; +import io.grpc.NameResolver; +import io.grpc.Status; +import io.grpc.SynchronizationContext; +import io.grpc.internal.GrpcUtil; +import io.grpc.internal.SharedResourceHolder; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.config.GrpcServerProperties; + +/** + * A {@link NameResolver} that will always respond with the server's own address. + */ +@Slf4j +public class SelfNameResolver extends NameResolver { + + private final GrpcServerProperties properties; + private final SynchronizationContext syncContext; + private final SharedResourceHolder.Resource executorResource; + private final boolean usingExecutorResource; + + // Following fields must be accessed from syncContext + private Executor executor = null; + private boolean resolving = false; + // The field must be accessed from syncContext, although the methods on an Listener2 can be called + // from any thread. + private Listener2 listener = null; + + /** + * Creates a self name resolver with the given properties. + * + * @param properties The properties to read the server address from. + * @param args The arguments for the resolver. + */ + public SelfNameResolver(final GrpcServerProperties properties, final Args args) { + this(properties, args, GrpcUtil.SHARED_CHANNEL_EXECUTOR); + } + + /** + * Creates a self name resolver with the given properties. + * + * @param properties The properties to read the server address from. + * @param args The arguments for the resolver. + * @param executorResource The shared executor resource for channels. + */ + public SelfNameResolver(final GrpcServerProperties properties, final Args args, + final SharedResourceHolder.Resource executorResource) { + this.properties = requireNonNull(properties, "properties"); + this.syncContext = requireNonNull(args.getSynchronizationContext(), "syncContext"); + this.executorResource = requireNonNull(executorResource, "executorResource"); + this.executor = args.getOffloadExecutor(); + this.usingExecutorResource = this.executor == null; + } + + @Override + public String getServiceAuthority() { + try { + return InetAddress.getLocalHost().getHostName(); + } catch (final UnknownHostException e) { + return getOwnAddressString("localhost"); + } + } + + @Override + public final void start(final Listener2 listener) { + checkState(this.listener == null, "already started"); + this.listener = checkNotNull(listener, "listener"); + if (this.usingExecutorResource) { + this.executor = SharedResourceHolder.get(this.executorResource); + } + resolve(); + } + + @Override + public final void refresh() { + checkState(this.listener != null, "not started"); + resolve(); + } + + private void resolve() { + log.debug("Scheduled self resolve"); + if (this.resolving || this.executor == null) { + return; + } + this.resolving = true; + this.executor.execute(new Resolve(this.listener)); + } + + @Override + public void shutdown() { + this.listener = null; + if (this.executor != null && this.usingExecutorResource) { + this.executor = SharedResourceHolder.release(this.executorResource, this.executor); + } + } + + private SocketAddress getOwnAddress() { + final String address = this.properties.getAddress(); + final int port = this.properties.getPort(); + final SocketAddress target; + if (GrpcServerProperties.ANY_IP_ADDRESS.equals(address)) { + target = new InetSocketAddress(port); + } else { + target = new InetSocketAddress(InetAddresses.forString(address), port); + } + return target; + } + + private String getOwnAddressString(final String fallback) { + try { + return getOwnAddress().toString().substring(1); + } catch (final IllegalArgumentException e) { + return fallback; + } + } + + @Override + public String toString() { + return "SelfNameResolver [" + getOwnAddressString("") + "]"; + } + + /** + * The logic for assigning the own address. + */ + private final class Resolve implements Runnable { + + private final Listener2 savedListener; + + /** + * Creates a new Resolve that stores a snapshot of the relevant states of the resolver. + * + * @param listener The listener to send the results to. + */ + Resolve(final Listener2 listener) { + this.savedListener = requireNonNull(listener, "listener"); + } + + @Override + public void run() { + try { + this.savedListener.onResult(ResolutionResult.newBuilder() + .setAddresses(ImmutableList.of( + new EquivalentAddressGroup(getOwnAddress()))) + .build()); + } catch (final Exception e) { + this.savedListener.onError(Status.UNAVAILABLE + .withDescription("Failed to resolve own address").withCause(e)); + } finally { + SelfNameResolver.this.syncContext.execute(() -> SelfNameResolver.this.resolving = false); + } + } + + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/nameresolver/SelfNameResolverFactory.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/nameresolver/SelfNameResolverFactory.java new file mode 100644 index 000000000..5e118711d --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/nameresolver/SelfNameResolverFactory.java @@ -0,0 +1,79 @@ +/* + * 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.server.nameresolver; + +import java.net.URI; + +import io.grpc.NameResolver; +import io.grpc.NameResolver.Args; +import io.grpc.NameResolverProvider; +import net.devh.boot.grpc.server.config.GrpcServerProperties; + +/** + * A name resolver factory that will create a {@link SelfNameResolverFactory} based on the target uri. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +// Do not add this to the NameResolverProvider service loader list +public class SelfNameResolverFactory extends NameResolverProvider { + + /** + * The constant containing the scheme that will be used by this factory. + */ + public static final String SELF_SCHEME = "self"; + + private final GrpcServerProperties properties; + + /** + * Creates a new SelfNameResolverFactory that uses the given properties. + * + * @param properties The properties used to resolve this server's address. + */ + public SelfNameResolverFactory(final GrpcServerProperties properties) { + this.properties = properties; + } + + @Override + public NameResolver newNameResolver(final URI targetUri, final Args args) { + if (SELF_SCHEME.equals(targetUri.getScheme()) || targetUri.toString().equals(SELF_SCHEME)) { + return new SelfNameResolver(this.properties, args); + } + return null; + } + + @Override + public String getDefaultScheme() { + return SELF_SCHEME; + } + + @Override + protected boolean isAvailable() { + return true; + } + + @Override + protected int priority() { + return 0; // Lowest priority + } + + @Override + public String toString() { + return "SelfNameResolverFactory [scheme=" + getDefaultScheme() + "]"; + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/nameresolver/package-info.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/nameresolver/package-info.java new file mode 100644 index 000000000..49eb48595 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/nameresolver/package-info.java @@ -0,0 +1,5 @@ +/** + * Classes used to resolve the client name into the actual service addresses. + */ + +package net.devh.boot.grpc.server.nameresolver; diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/scope/GrpcRequestScope.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/scope/GrpcRequestScope.java new file mode 100644 index 000000000..5b421bf14 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/scope/GrpcRequestScope.java @@ -0,0 +1,266 @@ +/* + * 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.server.scope; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.Scope; +import org.springframework.context.annotation.Bean; +import org.springframework.core.annotation.Order; + +import com.google.common.util.concurrent.MoreExecutors; + +import io.grpc.Context; +import io.grpc.Context.CancellationListener; +import io.grpc.Contexts; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import net.devh.boot.grpc.common.util.InterceptorOrder; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; + +/** + * The scope for beans that have their lifecycle bound to the grpc {@link Context}. + * + *

+ * Note: If you write the {@link Bean @Bean} definition of this class, you must use the {@code static} keyword. + *

+ * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@GrpcGlobalServerInterceptor +@Order(InterceptorOrder.ORDER_FIRST) +public class GrpcRequestScope implements Scope, BeanFactoryPostProcessor, ServerInterceptor, CancellationListener { + + public static final String GRPC_REQUEST_SCOPE_NAME = "grpcRequest"; + private static final String GRPC_REQUEST_SCOPE_ID = "grpc-request"; + private static final Context.Key GRPC_REQUEST_KEY = Context.key("grpcRequestScope"); + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException { + factory.registerScope(GRPC_REQUEST_SCOPE_NAME, this); + } + + @Override + public Listener interceptCall(ServerCall call, Metadata headers, + ServerCallHandler next) { + ScopedBeansContainer container = new ScopedBeansContainer(); + Context context = Context.current().withValue(GRPC_REQUEST_KEY, container); + context.addListener(this, MoreExecutors.directExecutor()); + return Contexts.interceptCall(context, call, headers, next); + } + + @Override + public Object get(String name, ObjectFactory objectFactory) { + return getCurrentScopeContainer().getOrCreate(name, objectFactory); + } + + @Override + public Object remove(String name) { + return getCurrentScopeContainer().remove(name); + } + + @Override + public void registerDestructionCallback(String name, Runnable callback) { + getCurrentScopeContainer().registerDestructionCallback(name, callback); + } + + @Override + public Object resolveContextualObject(String key) { + return null; + } + + @Override + public String getConversationId() { + return GRPC_REQUEST_SCOPE_ID; + } + + @Override + public void cancelled(Context context) { + final ScopedBeansContainer container = GRPC_REQUEST_KEY.get(context); + if (container != null) { + container.destroy(); + } + } + + /** + * Gets the current container for the grpc request scope. + * + * @return The currently active scope container. + * @throws IllegalStateException If the grpc request scope is currently not active. + */ + private ScopedBeansContainer getCurrentScopeContainer() { + ScopedBeansContainer scopedBeansContainer = GRPC_REQUEST_KEY.get(); + if (scopedBeansContainer == null) { + throw new IllegalStateException( + "Trying to access grpcRequest-Scope, but it was not started for this thread."); + } + return scopedBeansContainer; + } + + /** + * Container for all beans used in the active scope. + */ + private static class ScopedBeansContainer { + + private final Map references = new ConcurrentHashMap<>(); + + /** + * Gets or creates the bean with the given name using the given object factory. + * + * @param name The name of the bean. + * @param objectFactory The object factory used to create new instances. + * @return The bean associated with the given name. + */ + public Object getOrCreate(final String name, final ObjectFactory objectFactory) { + return this.references.computeIfAbsent(name, key -> new ScopedBeanReference(objectFactory)) + .getBean(); + } + + /** + * Removes the bean with the given name from this scope. + * + * @param name The name of the bean to remove. + * @return The bean instances that was removed from the scope or null, if it wasn't present. + */ + public Object remove(final String name) { + final ScopedBeanReference ref = this.references.remove(name); + if (ref == null) { + return null; + } else { + return ref.getBeanIfExists(); + } + } + + /** + * Attaches a destruction callback to the bean with the given name. + * + * @param name The name of the bean to attach the destruction callback to. + * @param callback The callback to register for the bean. + */ + public void registerDestructionCallback(final String name, final Runnable callback) { + final ScopedBeanReference ref = this.references.get(name); + if (ref != null) { + ref.setDestructionCallback(callback); + } + } + + /** + * Destroys all beans in the scope and executes their destruction callbacks. + */ + public void destroy() { + final List errors = new ArrayList<>(); + final Iterator it = this.references.values().iterator(); + while (it.hasNext()) { + ScopedBeanReference val = it.next(); + it.remove(); + try { + val.destroy(); + } catch (RuntimeException e) { + errors.add(e); + } + } + if (!errors.isEmpty()) { + RuntimeException rex = errors.remove(0); + for (RuntimeException error : errors) { + rex.addSuppressed(error); + } + throw rex; + } + } + + } + + /** + * Container for a single scoped bean. This class manages the bean creation + */ + private static class ScopedBeanReference { + + private final ObjectFactory objectFactory; + private Object bean; + private Runnable destructionCallback; + + /** + * Creates a new scoped bean reference using the given object factory. + * + * @param objectFactory The object factory used to create instances of that bean. + */ + public ScopedBeanReference(ObjectFactory objectFactory) { + this.objectFactory = objectFactory; + } + + /** + * Gets or creates the bean managed by this instance. + * + * @return The existing or newly created bean instance. + */ + public synchronized Object getBean() { + if (this.bean == null) { + this.bean = this.objectFactory.getObject(); + } + return this.bean; + } + + /** + * Gets the bean managed by this instance, if it exists. + * + * @return The existing bean or null. + */ + public Object getBeanIfExists() { + return this.bean; + } + + /** + * Sets the given callback used to destroy the managed bean. + * + * @param destructionCallback The destruction callback to use. + */ + public void setDestructionCallback(final Runnable destructionCallback) { + this.destructionCallback = destructionCallback; + } + + /** + * Executes the destruction callback if set and clears the internal bean references. + */ + public synchronized void destroy() { + Runnable callback = this.destructionCallback; + if (callback != null) { + callback.run(); + } + this.bean = null; + this.destructionCallback = null; + } + + @Override + public String toString() { + return "ScopedBeanReference [objectFactory=" + this.objectFactory + ", bean=" + this.bean + "]"; + } + + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/AnonymousAuthenticationReader.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/AnonymousAuthenticationReader.java new file mode 100644 index 000000000..b4a2672e0 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/AnonymousAuthenticationReader.java @@ -0,0 +1,75 @@ +/* + * 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.server.security.authentication; + +import static java.util.Objects.requireNonNull; + +import java.util.Collection; + +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import lombok.extern.slf4j.Slf4j; + +/** + * The AnonymousAuthenticationReader allows users without credentials to get an anonymous identity. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +public class AnonymousAuthenticationReader implements GrpcAuthenticationReader { + + private final String key; + private final Object principal; + private final Collection authorities; + + /** + * Creates a new AnonymousAuthenticationReader with the given key and {@code "anonymousUser"} as principal with the + * {@code ROLE_ANONYMOUS}. + * + * @param key The key to used to identify tokens that were created by this instance. + */ + public AnonymousAuthenticationReader(final String key) { + this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + } + + /** + * Creates a new AnonymousAuthenticationReader with the given key,principal and authorities. + * + * @param key The key to used to identify tokens that were created by this instance. + * @param principal The principal which will be used to represent anonymous users. + * @param authorities The authority list for anonymous users. + */ + public AnonymousAuthenticationReader(final String key, final Object principal, + final Collection authorities) { + this.key = requireNonNull(key, "key"); + this.principal = requireNonNull(principal, "principal"); + this.authorities = requireNonNull(authorities, "authorities"); + } + + @Override + public Authentication readAuthentication(final ServerCall call, final Metadata headers) { + log.debug("Continue with anonymous auth"); + return new AnonymousAuthenticationToken(this.key, this.principal, this.authorities); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/BasicGrpcAuthenticationReader.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/BasicGrpcAuthenticationReader.java new file mode 100644 index 000000000..654d6e536 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/BasicGrpcAuthenticationReader.java @@ -0,0 +1,87 @@ +/* + * 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.server.security.authentication; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static net.devh.boot.grpc.common.security.SecurityConstants.AUTHORIZATION_HEADER; +import static net.devh.boot.grpc.common.security.SecurityConstants.BASIC_AUTH_PREFIX; + +import java.util.Base64; + +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import lombok.extern.slf4j.Slf4j; + +/** + * Reads {@link UsernamePasswordAuthenticationToken basic auth credentials} from the request. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +public class BasicGrpcAuthenticationReader implements GrpcAuthenticationReader { + + private static final String PREFIX = BASIC_AUTH_PREFIX.toLowerCase(); + private static final int PREFIX_LENGTH = PREFIX.length(); + + @Override + public Authentication readAuthentication(final ServerCall call, final Metadata headers) + throws AuthenticationException { + final String header = headers.get(AUTHORIZATION_HEADER); + if (header == null || !header.toLowerCase().startsWith(PREFIX)) { + log.debug("No basic auth header found"); + return null; + } + final String[] decoded = extractAndDecodeHeader(header); + return new UsernamePasswordAuthenticationToken(decoded[0], decoded[1]); + } + + /** + * Decodes the header into a username and password. + * + * @param header The authorization header. + * @return The decoded username and password. + * @throws BadCredentialsException If the Basic header is not valid Base64 or is missing the {@code ':'} separator. + * @see
BasicAuthenticationFilter + */ + private String[] extractAndDecodeHeader(final String header) { + + final byte[] base64Token = header.substring(PREFIX_LENGTH).getBytes(UTF_8); + byte[] decoded; + try { + decoded = Base64.getDecoder().decode(base64Token); + } catch (final IllegalArgumentException e) { + throw new BadCredentialsException("Failed to decode basic authentication token", e); + } + + final String token = new String(decoded, UTF_8); + + final int delim = token.indexOf(':'); + + if (delim == -1) { + throw new BadCredentialsException("Invalid basic authentication token"); + } + return new String[] {token.substring(0, delim), token.substring(delim + 1)}; + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/BearerAuthenticationReader.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/BearerAuthenticationReader.java new file mode 100644 index 000000000..9ecba48aa --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/BearerAuthenticationReader.java @@ -0,0 +1,89 @@ +/* + * 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.server.security.authentication; + +import static net.devh.boot.grpc.common.security.SecurityConstants.AUTHORIZATION_HEADER; +import static net.devh.boot.grpc.common.security.SecurityConstants.BEARER_AUTH_PREFIX; + +import java.util.function.Function; + +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import lombok.extern.slf4j.Slf4j; + +/** + * Spring-Security has several token-based {@link AuthenticationProvider} implementations (e.g. in + * spring-security-web/oauth2 or spring-security-oauth2-resource-server), so you need to provide a {@link Function} that + * wraps the extracted token in a {@link Authentication} object supported by your AuthenticationProvider. + * + * @author Gregor Eeckels (gregor.eeckels@gmail.com) + */ +@Slf4j +public class BearerAuthenticationReader implements GrpcAuthenticationReader { + + private static final String PREFIX = BEARER_AUTH_PREFIX.toLowerCase(); + private static final int PREFIX_LENGTH = PREFIX.length(); + + private Function tokenWrapper; + + /** + * Creates a new BearerAuthenticationReader with the given wrapper function. + *

+ * Example-Usage: + *

+ * + * For spring-security-web: + * + *
+     * new BearerAuthenticationReader(token -> new PreAuthenticatedAuthenticationToken(token, null))
+     * 
+ * + * For spring-security-oauth2-resource-server: + * + *
+     * new BearerAuthenticationReader(token -> new BearerTokenAuthenticationToken(token))
+     * 
+ * + * @param tokenWrapper The function used to convert the token (without bearer prefix) into an {@link Authentication} + * object. + */ + public BearerAuthenticationReader(Function tokenWrapper) { + Assert.notNull(tokenWrapper, "tokenWrapper cannot be null"); + this.tokenWrapper = tokenWrapper; + } + + @Override + public Authentication readAuthentication(final ServerCall call, final Metadata headers) { + final String header = headers.get(AUTHORIZATION_HEADER); + + if (header == null || !header.toLowerCase().startsWith(PREFIX)) { + log.debug("No bearer auth header found"); + return null; + } + + // Cut away the "bearer " prefix + final String accessToken = header.substring(PREFIX_LENGTH); + + // Not authenticated yet, token needs to be processed + return tokenWrapper.apply(accessToken); + } +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/CompositeGrpcAuthenticationReader.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/CompositeGrpcAuthenticationReader.java new file mode 100644 index 000000000..d1d5d9432 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/CompositeGrpcAuthenticationReader.java @@ -0,0 +1,62 @@ +/* + * 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.server.security.authentication; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +import io.grpc.Metadata; +import io.grpc.ServerCall; + +/** + * Combines multiple {@link GrpcAuthenticationReader} into a single one. The readers will be executed in the same order + * the are passed to the constructor. The authentication is aborted if a grpc authentication reader throws an exception. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public class CompositeGrpcAuthenticationReader implements GrpcAuthenticationReader { + + private final List authenticationReaders; + + /** + * Creates a new CompositeGrpcAuthenticationReader with the given authentication readers. + * + * @param authenticationReaders The authentication readers to use. + */ + public CompositeGrpcAuthenticationReader(final List authenticationReaders) { + this.authenticationReaders = new ArrayList<>(requireNonNull(authenticationReaders, "authenticationReaders")); + } + + @Override + public Authentication readAuthentication(final ServerCall call, final Metadata headers) + throws AuthenticationException { + for (final GrpcAuthenticationReader authenticationReader : this.authenticationReaders) { + final Authentication authentication = authenticationReader.readAuthentication(call, headers); + if (authentication != null) { + return authentication; + } + } + return null; + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/GrpcAuthenticationReader.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/GrpcAuthenticationReader.java new file mode 100644 index 000000000..c58f2576e --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/GrpcAuthenticationReader.java @@ -0,0 +1,62 @@ +/* + * 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.server.security.authentication; + +import javax.annotation.Nullable; + +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; + +import io.grpc.Metadata; +import io.grpc.ServerCall; + +/** + * Reads the authentication data from the given {@link ServerCall} and {@link Metadata}. The returned + * {@link Authentication} is not yet validated and needs to be passed to an {@link AuthenticationManager}. + * + *

+ * Note: The authentication manager needs a corresponding {@link AuthenticationProvider} to actually verify the + * {@link Authentication}. + *

+ * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@FunctionalInterface +public interface GrpcAuthenticationReader { + + /** + * Tries to read the {@link Authentication} information from the given call and metadata. + * + *

+ * Note: Implementations are free to throw an {@link AuthenticationException} if no credentials could be + * found in the call. If an exception is thrown by an implementation then the authentication attempt should be + * considered as failed and no subsequent {@link GrpcAuthenticationReader}s should be called. + *

+ * + * @param call The call to get that send the request. + * @param headers The metadata/headers as sent by the client. + * @return The authentication object or null if no authentication is present. + * @throws AuthenticationException If the authentication details are malformed or incomplete and thus the + * authentication attempt should be aborted. + */ + @Nullable + Authentication readAuthentication(ServerCall call, Metadata headers) throws AuthenticationException; + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReader.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReader.java new file mode 100644 index 000000000..58bdc2d16 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/SSLContextGrpcAuthenticationReader.java @@ -0,0 +1,77 @@ +/* + * 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.server.security.authentication; + +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + +import javax.annotation.Nullable; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; + +import org.springframework.security.core.Authentication; + +import io.grpc.Grpc; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import lombok.extern.slf4j.Slf4j; + +/** + * An {@link GrpcAuthenticationReader} that will try to use the peer certificates to extract the client + * {@link Authentication}. Currently this class only supports {@link X509Certificate}s. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +public class SSLContextGrpcAuthenticationReader implements GrpcAuthenticationReader { + + @Override + public Authentication readAuthentication(final ServerCall call, final Metadata metadata) { + final SSLSession sslSession = call.getAttributes().get(Grpc.TRANSPORT_ATTR_SSL_SESSION); + if (sslSession == null) { + log.trace("Peer not verified via SSL"); + return null; + } + Certificate[] certs; + try { + certs = sslSession.getPeerCertificates(); + } catch (final SSLPeerUnverifiedException e) { + log.trace("Peer not verified via certificate", e); + return null; + } + return fromCertificate(certs[certs.length - 1]); + } + + /** + * Tries to prepare an {@link Authentication} using the given certificate. + * + * @param cert The certificate to use. + * @return The authentication instance created with the certificate or null if the certificate type is unsupported. + */ + @Nullable + protected Authentication fromCertificate(final Certificate cert) { + if (cert instanceof X509Certificate) { + log.debug("Found X509 certificate"); + return new X509CertificateAuthentication((X509Certificate) cert); + } else { + log.debug("Unsupported certificate type: {}", cert.getType()); + return null; + } + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/X509CertificateAuthentication.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/X509CertificateAuthentication.java new file mode 100644 index 000000000..4678b6042 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/X509CertificateAuthentication.java @@ -0,0 +1,101 @@ +/* + * 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.server.security.authentication; + +import static java.util.Objects.requireNonNull; + +import java.security.cert.X509Certificate; +import java.util.Collection; +import java.util.Collections; + +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +/** + * An authentication object that was created for a {@link X509Certificate}. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public class X509CertificateAuthentication extends AbstractAuthenticationToken { + + private static final long serialVersionUID = -5783300616514990238L; + + private final Object principal; + private X509Certificate certificate; + + /** + * Creates a new X509CertificateAuthentication that will use the given certificate. Any code can safely use this + * constructor to create an {@link Authentication}, because the {@link #isAuthenticated()} will return + * {@code false}. + * + * @param certificate The certificate to create the authentication from. + */ + public X509CertificateAuthentication(final X509Certificate certificate) { + super(Collections.emptyList()); + requireNonNull(certificate, "certificate"); + this.principal = certificate.getSubjectX500Principal(); + this.certificate = certificate; + setAuthenticated(false); + } + + /** + * Creates a new X509CertificateAuthentication that was authenticated using the given certificate. This constructor + * should only be used by {@link AuthenticationManager}s or {@link AuthenticationProvider}s. The resulting + * authentication is trusted ({@link #isAuthenticated()} returns true) and has the given authorities. + * + * @param principal The authenticated principal. + * @param certificate The certificate that was used to authenticate the principal. + * @param authorities The authorities of the principal. + */ + public X509CertificateAuthentication(final Object principal, final X509Certificate certificate, + final Collection authorities) { + super(authorities); + this.principal = requireNonNull(principal, "principal"); + this.certificate = requireNonNull(certificate, "certificate"); + super.setAuthenticated(true); + } + + @Override + public Object getPrincipal() { + return this.principal; + } + + @Override + public X509Certificate getCredentials() { + return this.certificate; + } + + @Override + public void eraseCredentials() { + this.certificate = null; + super.eraseCredentials(); + } + + @Override + public void setAuthenticated(final boolean authenticated) { + if (authenticated) { + throw new IllegalArgumentException( + "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); + } + super.setAuthenticated(false); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/X509CertificateAuthenticationProvider.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/X509CertificateAuthenticationProvider.java new file mode 100644 index 000000000..920a6365d --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/X509CertificateAuthenticationProvider.java @@ -0,0 +1,146 @@ +/* + * 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.server.security.authentication; + +import static java.util.Objects.requireNonNull; + +import java.security.cert.X509Certificate; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.security.auth.x500.X500Principal; + +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; + +import lombok.extern.slf4j.Slf4j; + +/** + * An {@link AuthenticationProvider} for {@link X509Certificate}s. This provider only supports + * {@link X509CertificateAuthentication}s. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +public class X509CertificateAuthenticationProvider implements AuthenticationProvider { + + /** + * The uses the name of the principal way to extract the username from an {@link Authentication}. + */ + public static final Function PRINCIPAL_USERNAME_EXTRACTOR = + authentication -> authentication.getName(); + + /** + * The default way to extract the username from an {@link Authentication} by using the CN. + */ + public static final Function CN_USERNAME_EXTRACTOR = + patternExtractor("CN", PRINCIPAL_USERNAME_EXTRACTOR); + + /** + * A fallback that will fail to extract the username and will return null. The null will later be converted to a + * {@link UsernameNotFoundException}. + */ + public static final Function FAIL_FALLBACK = authentication -> null; + + /** + * Creates a new case-insensitive pattern extractor with the given pattern. + * + * @param key The case insensitive key to use (Example: 'CN'). + * @param fallback The fallback function to use if the key was not present in the subject. + * @return The newly created extractor. + */ + public static Function patternExtractor(final String key, + final Function fallback) { + requireNonNull(key, "key"); + requireNonNull(fallback, "fallback"); + final Pattern pattern = Pattern.compile(key + "=(.+?)(?:,|$)", Pattern.CASE_INSENSITIVE); + return authentication -> { + final Object principal = authentication.getPrincipal(); + if (principal instanceof X500Principal) { + final X500Principal x500Principal = (X500Principal) principal; + final Matcher matcher = pattern.matcher(x500Principal.getName()); + if (matcher.find()) { + return matcher.group(1); + } + } + return fallback.apply(authentication); + }; + } + + private final Function usernameExtractor; + private final UserDetailsService userDetailsService; + + /** + * Creates a new X509CertificateAuthenticationProvider, which uses the {@link #CN_USERNAME_EXTRACTOR default way + * (via CN)} to extract the username and uses the given {@link UserDetailsService} to lookup the user. + * + * @param userDetailsService The user details service to use. + */ + public X509CertificateAuthenticationProvider(final UserDetailsService userDetailsService) { + this(CN_USERNAME_EXTRACTOR, userDetailsService); + } + + /** + * Creates a new X509CertificateAuthenticationProvider, which uses the given {@link Function} to extract the + * username and uses the given {@link UserDetailsService} to lookup the user. + * + * @param usernameExtractor The username extractor to use. The function should return null, if the username is + * missing. + * @param userDetailsService The user details service to use. + */ + public X509CertificateAuthenticationProvider( + final Function usernameExtractor, + final UserDetailsService userDetailsService) { + this.usernameExtractor = requireNonNull(usernameExtractor, "usernameExtractor"); + this.userDetailsService = requireNonNull(userDetailsService, "userDetailsService"); + } + + @Override + public Authentication authenticate(final Authentication authentication) throws AuthenticationException { + if (!(authentication instanceof X509CertificateAuthentication)) { + throw new IllegalArgumentException("Unsupported authentication type: " + authentication.getClass().getName() + + ". Only X509CertificateAuthentication is supported!"); + } + + final X509CertificateAuthentication auth = (X509CertificateAuthentication) authentication; + final String username = this.usernameExtractor.apply(auth); + if (username == null) { + log.debug("Could not find username"); + throw new UsernameNotFoundException("No username provided"); + } + + final UserDetails user = this.userDetailsService.loadUserByUsername(username); + if (user == null) { + log.debug("Could not find user '{}'", username); + throw new UsernameNotFoundException("Unknown username: " + username); + } + log.debug("Authenticated as '{}'", username); + return new X509CertificateAuthentication(user, auth.getCredentials(), user.getAuthorities()); + } + + @Override + public boolean supports(final Class authentication) { + return X509CertificateAuthentication.class.isAssignableFrom(authentication); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/package-info.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/package-info.java new file mode 100644 index 000000000..a12e85d9e --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/authentication/package-info.java @@ -0,0 +1,5 @@ +/** + * Contains the security classes related to authentication checks. + */ + +package net.devh.boot.grpc.server.security.authentication; diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AbstractGrpcSecurityMetadataSource.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AbstractGrpcSecurityMetadataSource.java new file mode 100644 index 000000000..7f45e50d4 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AbstractGrpcSecurityMetadataSource.java @@ -0,0 +1,47 @@ +/* + * 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.server.security.check; + +import java.util.Collection; + +import org.springframework.security.access.ConfigAttribute; + +import io.grpc.MethodDescriptor; + +/** + * Abstract implementation of {@link GrpcSecurityMetadataSource} which resolves the secured object type to a + * {@link MethodDescriptor}. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public abstract class AbstractGrpcSecurityMetadataSource implements GrpcSecurityMetadataSource { + + @Override + public final Collection getAttributes(final Object object) throws IllegalArgumentException { + if (object instanceof MethodDescriptor) { + return getAttributes((MethodDescriptor) object); + } + throw new IllegalArgumentException("Object must be a non-null MethodDescriptor"); + } + + @Override + public final boolean supports(final Class clazz) { + return MethodDescriptor.class.equals(clazz); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AccessPredicate.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AccessPredicate.java new file mode 100644 index 000000000..3da853a14 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AccessPredicate.java @@ -0,0 +1,278 @@ +/* + * 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.server.security.check; + +import static java.util.Objects.requireNonNull; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Set; +import java.util.function.Predicate; + +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +import com.google.common.collect.ImmutableSet; + +/** + * Predicate that can be used to check whether the given {@link Authentication} has access to the protected + * service/method. This interface assumes, that the user is authenticated before the method is called. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public interface AccessPredicate extends Predicate { + + @Override + default AccessPredicate negate() { + return t -> !test(t); + } + + @Override + default AccessPredicate and(final Predicate other) { + requireNonNull(other); + return t -> test(t) && other.test(t); + } + + @Override + default AccessPredicate or(final Predicate other) { + requireNonNull(other); + return t -> test(t) || other.test(t); + } + + /** + * Special constant that symbolizes that everybody (including unauthenticated users) can access the instance (no + * protection). + * + *

+ * Note: This is a special constant, that does not allow execution and mutation. It's sole purpose is to + * avoid ambiguity for {@code null} values. It should only be used in {@code ==} comparisons. + *

+ * + * @return A special constant that symbolizes public access. + */ + static AccessPredicate permitAll() { + return AccessPredicates.PERMIT_ALL; + } + + /** + * All authenticated users can access the protected instance including anonymous users. + * + *

+ * Note: The negation of this call is {@link #denyAll()} and NOT all unauthenticated. + *

+ * + * @return A newly created AccessPredicate that always returns true. + */ + static AccessPredicate authenticated() { + return authentication -> true; + } + + /** + * All authenticated users can access the protected instance excluding anonymous users. + * + * @return A newly created AccessPredicate that checks whether the user is explicitly authenticated. + */ + static AccessPredicate fullyAuthenticated() { + return authentication -> !(authentication instanceof AnonymousAuthenticationToken); + } + + /** + * Nobody can access the protected instance. + * + *

+ * Note: The negation of this call is {@link #authenticated()} and NOT {@link #permitAll()}. + *

+ * + * @return A newly created AccessPredicate that always returns false. + */ + static AccessPredicate denyAll() { + return authentication -> false; + } + + /** + * Only those who have the given role can access the protected instance. + * + * @param role The role to check for. + * @return A newly created AccessPredicate that only returns true, if the name of the {@link GrantedAuthority}s + * matches the given role name. + */ + static AccessPredicate hasRole(final String role) { + requireNonNull(role, "role"); + return authentication -> { + for (final GrantedAuthority authority : authentication.getAuthorities()) { + if (role.equals(authority.getAuthority())) { + return true; + } + } + return false; + }; + } + + /** + * Only those who have the given {@link GrantedAuthority} can access the protected instance. + * + * @param role The role to check for. + * @return A newly created AccessPredicate that only returns true, if the {@link GrantedAuthority}s matches the + * given role. + */ + static AccessPredicate hasAuthority(final GrantedAuthority role) { + requireNonNull(role, "role"); + return authentication -> { + for (final GrantedAuthority authority : authentication.getAuthorities()) { + if (role.equals(authority)) { + return true; + } + } + return false; + }; + } + + /** + * Only those who have any of the given roles can access the protected instance. + * + * @param roles The roles to check for. + * @return A newly created AccessPredicate that only returns true, if the name of the {@link GrantedAuthority}s + * matches any of the given role names. + */ + static AccessPredicate hasAnyRole(final String... roles) { + requireNonNull(roles, "roles"); + return hasAnyRole(Arrays.asList(roles)); + } + + /** + * Only those who have any of the given roles can access the protected instance. + * + * @param roles The roles to check for. + * @return A newly created AccessPredicate that only returns true, if the name of the {@link GrantedAuthority}s + * matches any of the given role names. + */ + static AccessPredicate hasAnyRole(final Collection roles) { + requireNonNull(roles, "roles"); + roles.forEach(role -> requireNonNull(role, "role")); + final Set immutableRoles = ImmutableSet.copyOf(roles); + return authentication -> { + for (final GrantedAuthority authority : authentication.getAuthorities()) { + if (immutableRoles.contains(authority.getAuthority())) { + return true; + } + } + return false; + }; + } + + /** + * Only those who have any of the given {@link GrantedAuthority} can access the protected instance. + * + * @param roles The roles to check for. + * @return A newly created AccessPredicate that only returns true, if the {@link GrantedAuthority}s matches any of + * the given roles. + */ + static AccessPredicate hasAnyAuthority(final GrantedAuthority... roles) { + requireNonNull(roles, "roles"); + return hasAnyAuthority(Arrays.asList(roles)); + } + + /** + * Only those who have any of the given {@link GrantedAuthority} can access the protected instance. + * + * @param roles The roles to check for. + * @return A newly created AccessPredicate that only returns true, if the {@link GrantedAuthority}s matches any of + * the given roles. + */ + static AccessPredicate hasAnyAuthority(final Collection roles) { + requireNonNull(roles, "roles"); + roles.forEach(role -> requireNonNull(role, "role")); + final Set immutableRoles = ImmutableSet.copyOf(roles); + return authentication -> { + for (final GrantedAuthority authority : authentication.getAuthorities()) { + if (immutableRoles.contains(authority)) { + return true; + } + } + return false; + }; + } + + /** + * Only those who have all of the given roles can access the protected instance. + * + * @param roles The roles to check for. + * @return A newly created AccessPredicate that only returns true, if the name of the {@link GrantedAuthority}s + * matches all of the given role names. + */ + static AccessPredicate hasAllRoles(final String... roles) { + requireNonNull(roles, "roles"); + return hasAnyRole(Arrays.asList(roles)); + } + + /** + * Only those who have all of the given roles can access the protected instance. + * + * @param roles The roles to check for. + * @return A newly created AccessPredicate that only returns true, if the name of the {@link GrantedAuthority}s + * matches all of the given role names. + */ + static AccessPredicate hasAllRoles(final Collection roles) { + requireNonNull(roles, "roles"); + roles.forEach(role -> requireNonNull(role, "role")); + final Set immutableRoles = ImmutableSet.copyOf(roles); + return authentication -> { + for (final GrantedAuthority authority : authentication.getAuthorities()) { + if (!immutableRoles.contains(authority.getAuthority())) { + return false; + } + } + return true; + }; + } + + /** + * Only those who have all of the given {@link GrantedAuthority} can access the protected instance. + * + * @param roles The roles to check for. + * @return A newly created AccessPredicate that only returns true, if the {@link GrantedAuthority}s matches all of + * the given roles. + */ + static AccessPredicate hasAllAuthorities(final GrantedAuthority... roles) { + requireNonNull(roles, "roles"); + return hasAllAuthorities(Arrays.asList(roles)); + } + + /** + * Only those who have any of the given {@link GrantedAuthority} can access the protected instance. + * + * @param roles The roles to check for. + * @return A newly created AccessPredicate that only returns true, if the {@link GrantedAuthority}s matches all of + * the given roles. + */ + static AccessPredicate hasAllAuthorities(final Collection roles) { + requireNonNull(roles, "roles"); + roles.forEach(role -> requireNonNull(role, "role")); + final Set immutableRoles = ImmutableSet.copyOf(roles); + return authentication -> { + for (final GrantedAuthority authority : authentication.getAuthorities()) { + if (!immutableRoles.contains(authority)) { + return false; + } + } + return true; + }; + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AccessPredicateConfigAttribute.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AccessPredicateConfigAttribute.java new file mode 100644 index 000000000..c4d7ffefe --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AccessPredicateConfigAttribute.java @@ -0,0 +1,85 @@ +/* + * 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.server.security.check; + +import static java.util.Objects.requireNonNull; + +import java.util.Objects; + +import org.springframework.security.access.ConfigAttribute; + +/** + * A {@link ConfigAttribute} which uses the embedded {@link AccessPredicate} for the decisions. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public final class AccessPredicateConfigAttribute implements ConfigAttribute { + + private static final long serialVersionUID = 2906954441251029428L; + + private final AccessPredicate accessPredicate; + + /** + * Creates a new AccessPredicateConfigAttribute with the given {@link AccessPredicate}. + * + * @param accessPredicate The access predicate to use. + */ + public AccessPredicateConfigAttribute(final AccessPredicate accessPredicate) { + this.accessPredicate = requireNonNull(accessPredicate, "accessPredicate"); + } + + /** + * Gets the access predicate that belongs to this instance. + * + * @return The associated access predicate. + */ + public AccessPredicate getAccessPredicate() { + return this.accessPredicate; + } + + @Override + public String getAttribute() { + return null; + } + + @Override + public int hashCode() { + return Objects.hash(this.accessPredicate); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final AccessPredicateConfigAttribute other = (AccessPredicateConfigAttribute) obj; + return Objects.equals(this.accessPredicate, other.accessPredicate); + } + + @Override + public String toString() { + return "AccessPredicateConfigAttribute [" + this.accessPredicate + "]"; + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AccessPredicateVoter.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AccessPredicateVoter.java new file mode 100644 index 000000000..62e0b4aaa --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AccessPredicateVoter.java @@ -0,0 +1,69 @@ +/* + * 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.server.security.check; + +import java.util.Collection; + +import org.springframework.security.access.AccessDecisionVoter; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.core.Authentication; + +/** + * An {@link AccessDecisionVoter} that checks for {@link AccessPredicateConfigAttribute}s. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public class AccessPredicateVoter implements AccessDecisionVoter { + + @Override + public boolean supports(final ConfigAttribute attribute) { + return attribute instanceof AccessPredicateConfigAttribute; + } + + @Override + public boolean supports(final Class clazz) { + return true; + } + + @Override + public int vote(final Authentication authentication, final Object object, + final Collection attributes) { + final AccessPredicateConfigAttribute attr = find(attributes); + if (attr == null) { + return ACCESS_ABSTAIN; + } + final boolean allowed = attr.getAccessPredicate().test(authentication); + return allowed ? ACCESS_GRANTED : ACCESS_DENIED; + } + + /** + * Finds the first AccessPredicateConfigAttribute in the given collection. + * + * @param attributes The attributes to search in. + * @return The first found AccessPredicateConfigAttribute or null, if no such elements were found. + */ + private AccessPredicateConfigAttribute find(final Collection attributes) { + for (final ConfigAttribute attribute : attributes) { + if (attribute instanceof AccessPredicateConfigAttribute) { + return (AccessPredicateConfigAttribute) attribute; + } + } + return null; + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AccessPredicates.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AccessPredicates.java new file mode 100644 index 000000000..697772937 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/AccessPredicates.java @@ -0,0 +1,78 @@ +/* + * 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.server.security.check; + +import java.util.function.Predicate; + +import org.springframework.security.authentication.InternalAuthenticationServiceException; +import org.springframework.security.core.Authentication; + +/** + * Helper class that contains some internal constants for {@link AccessPredicate}s. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +final class AccessPredicates { + + /** + * A marker constant that indicates that all restrictions should be disabled. This instance should never be + * executed, mutated or used in mutation. It should only be used in {@code ==} comparisons. + */ + static final AccessPredicate PERMIT_ALL = new AccessPredicate() { + + /** + * @deprecated Should never be called + */ + @Override + @Deprecated // Should never be called + public boolean test(final Authentication t) { + throw new InternalAuthenticationServiceException( + "Tried to execute the 'permit-all' access predicate. The server's security configuration is broken."); + } + + /** + * @deprecated Should never be called + */ + @Override + @Deprecated // Should never be called + public AccessPredicate and(final Predicate other) { + throw new UnsupportedOperationException("Not allowed for 'permit-all' access predicate"); + } + + /** + * @deprecated Should never be called + */ + @Override + @Deprecated // Should never be called + public AccessPredicate or(final Predicate other) { + throw new UnsupportedOperationException("Not allowed for 'permit-all' access predicate"); + } + + /** + * @deprecated Should never be called + */ + @Override + @Deprecated // Should never be called + public AccessPredicate negate() { + throw new UnsupportedOperationException("Not allowed for 'permit-all' access predicate"); + } + + }; + + private AccessPredicates() {} +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/GrpcSecurityMetadataSource.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/GrpcSecurityMetadataSource.java new file mode 100644 index 000000000..22db29fb6 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/GrpcSecurityMetadataSource.java @@ -0,0 +1,49 @@ +/* + * 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.server.security.check; + +import java.util.Collection; + +import org.springframework.security.access.AccessDecisionVoter; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.access.SecurityMetadataSource; + +import io.grpc.MethodDescriptor; + +/** + * A {@link SecurityMetadataSource} for grpc requests. + * + *

+ * Note: The authorization checking via this metadata source will only be enabled, if both an + * {@link AccessDecisionVoter} and a {@link GrpcSecurityMetadataSource} are present in the application context. + *

+ * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public interface GrpcSecurityMetadataSource extends SecurityMetadataSource { + + /** + * Accesses the {@code ConfigAttribute}s that apply to a given secure object. + * + * @param method The grpc method being secured. + * @return The attributes that apply to the passed in secured object. Should return an empty collection if there are + * no applicable attributes. + */ + Collection getAttributes(final MethodDescriptor method); + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/ManualGrpcSecurityMetadataSource.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/ManualGrpcSecurityMetadataSource.java new file mode 100644 index 000000000..df3ca6994 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/ManualGrpcSecurityMetadataSource.java @@ -0,0 +1,147 @@ +/* + * 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.server.security.check; + +import static com.google.common.collect.ImmutableList.of; +import static java.util.Objects.requireNonNull; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.security.access.AccessDecisionManager; +import org.springframework.security.access.ConfigAttribute; +import org.springframework.security.core.Authentication; + +import io.grpc.MethodDescriptor; +import io.grpc.ServiceDescriptor; + +/** + * A {@link GrpcSecurityMetadataSource} for manual configuration. For each {@link MethodDescriptor gRPC method} a + * {@link AccessPredicate} can be defined, that checks whether the user is authenticated and has access. This metadata + * source only works if an {@link AccessDecisionManager} is configured with an {@link AccessPredicateVoter}. + * + *

+ * Note: This instance is initialized with {@link AccessPredicate#denyAll() deny all} as default. + *

+ * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public final class ManualGrpcSecurityMetadataSource extends AbstractGrpcSecurityMetadataSource { + + private final Map, Collection> accessMap = new HashMap<>(); + private Collection defaultAttributes = wrap(AccessPredicate.denyAll()); + + @Override + public Collection getAttributes(final MethodDescriptor method) { + return this.accessMap.getOrDefault(method, this.defaultAttributes); + } + + @Override + public Collection getAllConfigAttributes() { + return this.accessMap.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()); + } + + /** + * Set the given access predicate for the all methods of the given service. This will replace previously set + * predicates. + * + * @param service The service to protect with a custom check. + * @param predicate The predicate used to check the {@link Authentication}. + * @return This instance for chaining. + * @see #setDefault(AccessPredicate) + */ + public ManualGrpcSecurityMetadataSource set(final ServiceDescriptor service, final AccessPredicate predicate) { + requireNonNull(service, "service"); + final Collection wrappedPredicate = wrap(predicate); + for (final MethodDescriptor method : service.getMethods()) { + this.accessMap.put(method, wrappedPredicate); + } + return this; + } + + /** + * Removes all access predicates for the all methods of the given service. After that, the default will be used for + * those methods. + * + * @param service The service to protect with only the default. + * @return This instance for chaining. + * @see #setDefault(AccessPredicate) + */ + public ManualGrpcSecurityMetadataSource remove(final ServiceDescriptor service) { + requireNonNull(service, "service"); + for (final MethodDescriptor method : service.getMethods()) { + this.accessMap.remove(method); + } + return this; + } + + /** + * Set the given access predicate for the given method. This will replace previously set predicates. + * + * @param method The method to protect with a custom check. + * @param predicate The predicate used to check the {@link Authentication}. + * @return This instance for chaining. + * @see #setDefault(AccessPredicate) + */ + public ManualGrpcSecurityMetadataSource set(final MethodDescriptor method, final AccessPredicate predicate) { + requireNonNull(method, "method"); + this.accessMap.put(method, wrap(predicate)); + return this; + } + + /** + * Removes all access predicates for the given method. After that, the default will be used for that method. + * + * @param method The method to protect with only the default. + * @return This instance for chaining. + * @see #setDefault(AccessPredicate) + */ + public ManualGrpcSecurityMetadataSource remove(final MethodDescriptor method) { + requireNonNull(method, "method"); + this.accessMap.remove(method); + return this; + } + + /** + * Sets the default that will be used if no specific configuration has been made. + * + * @param predicate The default predicate used to check the {@link Authentication}. + * @return This instance for chaining. + */ + public ManualGrpcSecurityMetadataSource setDefault(final AccessPredicate predicate) { + this.defaultAttributes = wrap(predicate); + return this; + } + + /** + * Wraps the given predicate in a configuration attribute and an immutable collection. + * + * @param predicate The predicate to wrap. + * @return The newly created list with the given predicate. + */ + private Collection wrap(final AccessPredicate predicate) { + requireNonNull(predicate, "predicate"); + if (predicate == AccessPredicates.PERMIT_ALL) { + return of(); // Empty collection => public invocation + } + return of(new AccessPredicateConfigAttribute(predicate)); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/package-info.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/package-info.java new file mode 100644 index 000000000..9844e3d0f --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/check/package-info.java @@ -0,0 +1,5 @@ +/** + * Contains the security classes related to authorization checks. + */ + +package net.devh.boot.grpc.server.security.check; diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/AbstractAuthenticatingServerCallListener.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/AbstractAuthenticatingServerCallListener.java new file mode 100644 index 000000000..12f4f840b --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/AbstractAuthenticatingServerCallListener.java @@ -0,0 +1,153 @@ +/* + * 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.server.security.interceptors; + +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import lombok.extern.slf4j.Slf4j; + +/** + * A call listener that will set the authentication context before each invocation and clear it afterwards. Use and + * extend this class if you want to setup non-grpc authentication contexts. + * + *

+ * Note: If you only want to setup the grpc-context and nothing else, then you can use + * {@link Contexts#interceptCall(Context, ServerCall, Metadata, ServerCallHandler)} instead. + * + * @param The type of the request. + */ +@Slf4j +public abstract class AbstractAuthenticatingServerCallListener extends SimpleForwardingServerCallListener { + + private final Context context; + + /** + * Creates a new AbstractAuthenticatingServerCallListener which will attach the given security context before + * delegating to the given listener. + * + * @param delegate The listener to delegate to. + * @param context The context to attach. + */ + protected AbstractAuthenticatingServerCallListener(final Listener delegate, final Context context) { + super(delegate); + this.context = context; + } + + /** + * Gets the {@link Context} associated with the call. + * + * @return The context of the current call. + */ + protected final Context context() { + return this.context; + } + + /** + * Attaches the authentication context before the actual call. + * + *

+ * This method is called after the grpc context is attached. + *

+ */ + protected abstract void attachAuthenticationContext(); + + /** + * Detaches the authentication context after the actual call. + * + *

+ * This method is called before the grpc context is detached. + *

+ */ + protected abstract void detachAuthenticationContext(); + + @Override + public void onMessage(final ReqT message) { + final Context previous = this.context.attach(); + try { + attachAuthenticationContext(); + log.debug("onMessage - Authentication set"); + super.onMessage(message); + } finally { + detachAuthenticationContext(); + this.context.detach(previous); + log.debug("onMessage - Authentication cleared"); + } + } + + @Override + public void onHalfClose() { + final Context previous = this.context.attach(); + try { + attachAuthenticationContext(); + log.debug("onHalfClose - Authentication set"); + super.onHalfClose(); + } finally { + detachAuthenticationContext(); + this.context.detach(previous); + log.debug("onHalfClose - Authentication cleared"); + } + } + + @Override + public void onCancel() { + final Context previous = this.context.attach(); + try { + attachAuthenticationContext(); + log.debug("onCancel - Authentication set"); + super.onCancel(); + } finally { + detachAuthenticationContext(); + log.debug("onCancel - Authentication cleared"); + this.context.detach(previous); + } + } + + @Override + public void onComplete() { + final Context previous = this.context.attach(); + try { + attachAuthenticationContext(); + log.debug("onComplete - Authentication set"); + super.onComplete(); + } finally { + detachAuthenticationContext(); + log.debug("onComplete - Authentication cleared"); + this.context.detach(previous); + } + } + + @Override + public void onReady() { + final Context previous = this.context.attach(); + try { + attachAuthenticationContext(); + log.debug("onReady - Authentication set"); + super.onReady(); + } finally { + detachAuthenticationContext(); + log.debug("onReady - Authentication cleared"); + this.context.detach(previous); + } + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/AuthenticatingServerInterceptor.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/AuthenticatingServerInterceptor.java new file mode 100644 index 000000000..be530a5e6 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/AuthenticatingServerInterceptor.java @@ -0,0 +1,51 @@ +/* + * 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.server.security.interceptors; + +import org.springframework.security.core.Authentication; + +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; + +/** + * Marker-Interface: A server interceptor that used to authenticate the client request. + * + *

+ * Note: Implementations must be thread safe and return a thread safe {@link Listener}. Do NOT store the + * authentication in a thread local context (permanently). The authentication context must be cleared before returning + * from {@link #interceptCall(ServerCall, Metadata, ServerCallHandler) interceptCall()} and all the {@link Listener} + * methods. + *

+ * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + * @see AbstractAuthenticatingServerCallListener + * @see Contexts#interceptCall(Context, ServerCall, Metadata, ServerCallHandler) + */ +public interface AuthenticatingServerInterceptor extends ServerInterceptor { + + /** + * The context key that can be used to retrieve the associated {@link Authentication}. + */ + public static final Context.Key AUTHENTICATION_CONTEXT_KEY = Context.key("authentication"); + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/AuthorizationCheckingServerInterceptor.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/AuthorizationCheckingServerInterceptor.java new file mode 100644 index 000000000..7dd49f9a6 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/AuthorizationCheckingServerInterceptor.java @@ -0,0 +1,108 @@ +/* + * 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.server.security.interceptors; + +import static java.util.Objects.requireNonNull; + +import org.springframework.core.annotation.Order; +import org.springframework.security.access.AccessDecisionManager; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.SecurityMetadataSource; +import org.springframework.security.access.intercept.AbstractSecurityInterceptor; +import org.springframework.security.access.intercept.InterceptorStatusToken; +import org.springframework.security.core.AuthenticationException; + +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.common.util.InterceptorOrder; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; +import net.devh.boot.grpc.server.security.check.GrpcSecurityMetadataSource; + +/** + * A server interceptor that will check the security context whether it has permission to access the grpc method. This + * interceptor uses a {@link GrpcSecurityMetadataSource} to obtain the information how the called method is protected + * and uses an {@link AccessDecisionManager} to evaluate that information. This interceptor isn't needed if you use + * spring's security annotations, but can be used additionally. An example use case of using both would be requiring all + * users to be authenticated, while using the annotations to require further permissions. + * + *

+ * Note: If you use spring's security annotations, the you have to use + * {@code @EnableGlobalMethodSecurity(proxyTargetClass = true, ...)} + *

+ * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@GrpcGlobalServerInterceptor +@Order(InterceptorOrder.ORDER_SECURITY_AUTHORISATION) +public class AuthorizationCheckingServerInterceptor extends AbstractSecurityInterceptor implements ServerInterceptor { + + private final GrpcSecurityMetadataSource securityMetadataSource; + + /** + * Creates a new AuthorizationCheckingServerInterceptor with the given {@link AccessDecisionManager} and + * {@link GrpcSecurityMetadataSource}. + * + * @param accessDecisionManager The access decision manager to use. + * @param securityMetadataSource The security metadata source to use. + */ + public AuthorizationCheckingServerInterceptor(final AccessDecisionManager accessDecisionManager, + final GrpcSecurityMetadataSource securityMetadataSource) { + setAccessDecisionManager(requireNonNull(accessDecisionManager, "accessDecisionManager")); + this.securityMetadataSource = requireNonNull(securityMetadataSource, "securityMetadataSource"); + } + + @SuppressWarnings("unchecked") + @Override + public Listener interceptCall(final ServerCall call, final Metadata headers, + final ServerCallHandler next) { + final MethodDescriptor methodDescriptor = call.getMethodDescriptor(); + final InterceptorStatusToken token; + try { + token = beforeInvocation(methodDescriptor); + } catch (final AuthenticationException | AccessDeniedException e) { + log.debug("Access denied"); + throw e; + } + log.debug("Access granted"); + final Listener result; + try { + result = next.startCall(call, headers); + } finally { + finallyInvocation(token); + } + // TODO: Call that here or in onHalfClose? + return (Listener) afterInvocation(token, result); + } + + @Override + public Class getSecureObjectClass() { + return MethodDescriptor.class; + } + + @Override + public SecurityMetadataSource obtainSecurityMetadataSource() { + return this.securityMetadataSource; + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/DefaultAuthenticatingServerInterceptor.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/DefaultAuthenticatingServerInterceptor.java new file mode 100644 index 000000000..cc25041d4 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/DefaultAuthenticatingServerInterceptor.java @@ -0,0 +1,178 @@ +/* + * 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.server.security.interceptors; + +import static java.util.Objects.requireNonNull; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.annotation.Order; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.authentication.AbstractAuthenticationToken; +import org.springframework.security.authentication.AnonymousAuthenticationToken; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.context.SecurityContextHolder; + +import io.grpc.Context; +import io.grpc.Contexts; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.common.util.InterceptorOrder; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; +import net.devh.boot.grpc.server.security.authentication.GrpcAuthenticationReader; + +/** + * A server interceptor that tries to {@link GrpcAuthenticationReader read} the credentials from the client and + * {@link AuthenticationManager#authenticate(Authentication) authenticate} them. This interceptor sets the + * authentication to both grpc's {@link Context} and {@link SecurityContextHolder}. + * + *

+ * Note: This interceptor works similar to + * {@link Contexts#interceptCall(Context, ServerCall, Metadata, ServerCallHandler)}. + *

+ * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@GrpcGlobalServerInterceptor +@Order(InterceptorOrder.ORDER_SECURITY_AUTHENTICATION) +public class DefaultAuthenticatingServerInterceptor implements AuthenticatingServerInterceptor { + + private final AuthenticationManager authenticationManager; + private final GrpcAuthenticationReader grpcAuthenticationReader; + + /** + * Creates a new DefaultAuthenticatingServerInterceptor with the given authentication manager and reader. + * + * @param authenticationManager The authentication manager used to verify the credentials. + * @param authenticationReader The authentication reader used to extract the credentials from the call. + */ + @Autowired + public DefaultAuthenticatingServerInterceptor(final AuthenticationManager authenticationManager, + final GrpcAuthenticationReader authenticationReader) { + this.authenticationManager = requireNonNull(authenticationManager, "authenticationManager"); + this.grpcAuthenticationReader = requireNonNull(authenticationReader, "authenticationReader"); + } + + @Override + public ServerCall.Listener interceptCall(final ServerCall call, + final Metadata headers, final ServerCallHandler next) { + Authentication authentication; + try { + authentication = this.grpcAuthenticationReader.readAuthentication(call, headers); + } catch (final AuthenticationException e) { + log.debug("Failed to read authentication: {}", e.getMessage()); + throw e; + } + if (authentication == null) { + log.debug("No credentials found: Continuing unauthenticated"); + try { + return next.startCall(call, headers); + } catch (final AccessDeniedException e) { + throw new BadCredentialsException("No credentials found in the request", e); + } + } + if (authentication.getDetails() == null && authentication instanceof AbstractAuthenticationToken) { + // Append call attributes to the authentication request. + // This gives the AuthenticationManager access to information like remote and local address. + // It can then decide whether it wants to use its own user details or the attributes. + ((AbstractAuthenticationToken) authentication).setDetails(call.getAttributes()); + } + log.debug("Credentials found: Authenticating '{}'", authentication.getName()); + try { + authentication = this.authenticationManager.authenticate(authentication); + } catch (final AuthenticationException e) { + log.debug("Authentication request failed: {}", e.getMessage()); + throw e; + } + + final Context context = Context.current().withValue(AUTHENTICATION_CONTEXT_KEY, authentication); + final Context previousContext = context.attach(); + SecurityContextHolder.getContext().setAuthentication(authentication); + log.debug("Authentication successful: Continuing as {} ({})", authentication.getName(), + authentication.getAuthorities()); + try { + return new AuthenticatingServerCallListener<>(next.startCall(call, headers), context, authentication); + } catch (final AccessDeniedException e) { + if (authentication instanceof AnonymousAuthenticationToken) { + throw new BadCredentialsException("No credentials found in the request", e); + } else { + throw e; + } + } finally { + SecurityContextHolder.clearContext(); + context.detach(previousContext); + log.debug("startCall - Authentication cleared"); + } + } + + /** + * A call listener that will set the authentication context using {@link SecurityContextHolder} before each + * invocation and clear it afterwards. + * + * @param The type of the request. + */ + private static class AuthenticatingServerCallListener extends AbstractAuthenticatingServerCallListener { + + private final Authentication authentication; + + /** + * Creates a new AuthenticatingServerCallListener which will attach the given security context before delegating + * to the given listener. + * + * @param delegate The listener to delegate to. + * @param context The context to attach. + * @param authentication The authentication instance to attach. + */ + public AuthenticatingServerCallListener(final Listener delegate, final Context context, + final Authentication authentication) { + super(delegate, context); + this.authentication = authentication; + } + + @Override + protected void attachAuthenticationContext() { + SecurityContextHolder.getContext().setAuthentication(this.authentication); + } + + @Override + protected void detachAuthenticationContext() { + SecurityContextHolder.clearContext(); + } + + @Override + public void onHalfClose() { + try { + super.onHalfClose(); + } catch (final AccessDeniedException e) { + if (this.authentication instanceof AnonymousAuthenticationToken) { + throw new BadCredentialsException("No credentials found in the request", e); + } else { + throw e; + } + } + } + + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/ExceptionTranslatingServerInterceptor.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/ExceptionTranslatingServerInterceptor.java new file mode 100644 index 000000000..735db5420 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/ExceptionTranslatingServerInterceptor.java @@ -0,0 +1,134 @@ +/* + * 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.server.security.interceptors; + +import org.springframework.core.annotation.Order; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.AuthenticationException; + +import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.common.util.InterceptorOrder; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; + +/** + * Server interceptor that translates any {@link AuthenticationException} and {@link AccessDeniedException} to + * appropriate grpc status responses. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@GrpcGlobalServerInterceptor +@Order(InterceptorOrder.ORDER_SECURITY_EXCEPTION_HANDLING) +public class ExceptionTranslatingServerInterceptor implements ServerInterceptor { + + /** + * A constant that contains the response message for unauthenticated calls. + */ + public static final String UNAUTHENTICATED_DESCRIPTION = "Authentication failed"; + /** + * A constant that contains the response message for calls with insufficient permissions. + */ + public static final String ACCESS_DENIED_DESCRIPTION = "Access denied"; + + @Override + public ServerCall.Listener interceptCall(final ServerCall call, + final Metadata headers, + final ServerCallHandler next) { + try { + // Streaming calls error out here + return new ExceptionTranslatorServerCallListener<>(next.startCall(call, headers), call); + } catch (final AuthenticationException aex) { + closeCallUnauthenticated(call, aex); + return noOpCallListener(); + } catch (final AccessDeniedException aex) { + closeCallAccessDenied(call, aex); + return noOpCallListener(); + } + } + + /** + * Creates a new no-op call listener because you can neither return null nor throw an exception in + * {@link #interceptCall(ServerCall, Metadata, ServerCallHandler)}. + * + * @param The type of the request. + * @return The newly created dummy listener. + */ + protected Listener noOpCallListener() { + return new Listener() {}; + } + + /** + * Close the call with {@link Status#UNAUTHENTICATED}. + * + * @param call The call to close. + * @param aex The exception that was the cause. + */ + protected void closeCallUnauthenticated(final ServerCall call, final AuthenticationException aex) { + log.debug(UNAUTHENTICATED_DESCRIPTION, aex); + call.close(Status.UNAUTHENTICATED.withCause(aex).withDescription(UNAUTHENTICATED_DESCRIPTION), new Metadata()); + } + + /** + * Close the call with {@link Status#PERMISSION_DENIED}. + * + * @param call The call to close. + * @param aex The exception that was the cause. + */ + protected void closeCallAccessDenied(final ServerCall call, final AccessDeniedException aex) { + log.debug(ACCESS_DENIED_DESCRIPTION, aex); + call.close(Status.PERMISSION_DENIED.withCause(aex).withDescription(ACCESS_DENIED_DESCRIPTION), new Metadata()); + } + + /** + * Server call listener that catches and handles exceptions in {@link #onHalfClose()}. + * + * @param The type of the request. + * @param The type of the response. + */ + private class ExceptionTranslatorServerCallListener extends SimpleForwardingServerCallListener { + + private final ServerCall call; + + protected ExceptionTranslatorServerCallListener(final Listener delegate, + final ServerCall call) { + super(delegate); + this.call = call; + } + + @Override + // Unary calls error out here + public void onHalfClose() { + try { + super.onHalfClose(); + } catch (final AuthenticationException aex) { + closeCallUnauthenticated(this.call, aex); + } catch (final AccessDeniedException aex) { + closeCallAccessDenied(this.call, aex); + } + } + + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/package-info.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/package-info.java new file mode 100644 index 000000000..3f7f746e8 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/interceptors/package-info.java @@ -0,0 +1,5 @@ +/** + * Contains the interceptors that handle the security for the actual gRPC requests. + */ + +package net.devh.boot.grpc.server.security.interceptors; diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/package-info.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/package-info.java new file mode 100644 index 000000000..84f565822 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/security/package-info.java @@ -0,0 +1,10 @@ +/** + * Contains classes that check the user's authentication and authorization. + * + *

+ * Requires Spring-Security. You might need additional + * libraries, if you want to use additional features such as OAuth or other authentication schemes. + *

+ */ + +package net.devh.boot.grpc.server.security; diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/AbstractGrpcServerFactory.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/AbstractGrpcServerFactory.java new file mode 100644 index 000000000..f8b42fea0 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/AbstractGrpcServerFactory.java @@ -0,0 +1,187 @@ +/* + * 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.server.serverfactory; + +import static java.util.Objects.requireNonNull; + +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.util.unit.DataSize; + +import com.google.common.collect.Lists; + +import io.grpc.Server; +import io.grpc.ServerBuilder; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.config.GrpcServerProperties; +import net.devh.boot.grpc.server.service.GrpcServiceDefinition; + +/** + * Abstract factory for grpc servers. + * + * @param The type of builder used by this factory. + * @author Michael (yidongnan@gmail.com) + * @author Daniel Theuke (daniel.theuke@heuboe.de) + * @since 5/17/16 + */ +@Slf4j +public abstract class AbstractGrpcServerFactory> implements GrpcServerFactory { + + private final List serviceList = Lists.newLinkedList(); + + protected final GrpcServerProperties properties; + protected final List serverConfigurers; + + /** + * Creates a new server factory with the given properties. + * + * @param properties The properties used to configure the server. + * @param serverConfigurers The server configurers to use. Can be empty. + */ + protected AbstractGrpcServerFactory(final GrpcServerProperties properties, + final List serverConfigurers) { + this.properties = requireNonNull(properties, "properties"); + this.serverConfigurers = requireNonNull(serverConfigurers, "serverConfigurers"); + } + + @Override + public Server createServer() { + final T builder = newServerBuilder(); + configure(builder); + return builder.build(); + } + + /** + * Creates a new server builder. + * + * @return The newly created server builder. + */ + protected abstract T newServerBuilder(); + + /** + * Configures the given server builder. This method can be overwritten to add features that are not yet supported by + * this library or use a {@link GrpcServerConfigurer} instead. + * + * @param builder The server builder to configure. + */ + protected void configure(final T builder) { + configureServices(builder); + configureKeepAlive(builder); + configureConnectionLimits(builder); + configureSecurity(builder); + configureLimits(builder); + for (final GrpcServerConfigurer serverConfigurer : this.serverConfigurers) { + serverConfigurer.accept(builder); + } + } + + /** + * Configures the services that should be served by the server. + * + * @param builder The server builder to configure. + */ + protected void configureServices(final T builder) { + final Set serviceNames = new LinkedHashSet<>(); + + for (final GrpcServiceDefinition service : this.serviceList) { + final String serviceName = service.getDefinition().getServiceDescriptor().getName(); + if (!serviceNames.add(serviceName)) { + throw new IllegalStateException("Found duplicate service implementation: " + serviceName); + } + log.info("Registered gRPC service: " + serviceName + ", bean: " + service.getBeanName() + ", class: " + + service.getBeanClazz().getName()); + builder.addService(service.getDefinition()); + } + } + + /** + * Configures the keep alive options that should be used by the server. + * + * @param builder The server builder to configure. + */ + protected void configureKeepAlive(final T builder) { + if (this.properties.isEnableKeepAlive()) { + throw new IllegalStateException("KeepAlive is enabled but this implementation does not support keepAlive!"); + } + } + + /** + * Configures the keep alive options that should be used by the server. + * + * @param builder The server builder to configure. + */ + protected void configureConnectionLimits(final T builder) { + if (this.properties.getMaxConnectionIdle() != null) { + throw new IllegalStateException( + "MaxConnectionIdle is set but this implementation does not support maxConnectionIdle!"); + } + if (this.properties.getMaxConnectionAge() != null) { + throw new IllegalStateException( + "MaxConnectionAge is set but this implementation does not support maxConnectionAge!"); + } + if (this.properties.getMaxConnectionAgeGrace() != null) { + throw new IllegalStateException( + "MaxConnectionAgeGrace is set but this implementation does not support maxConnectionAgeGrace!"); + } + } + + /** + * Configures the security options that should be used by the server. + * + * @param builder The server builder to configure. + */ + protected void configureSecurity(final T builder) { + if (this.properties.getSecurity().isEnabled()) { + throw new IllegalStateException("Security is enabled but this implementation does not support security!"); + } + } + + /** + * Configures limits such as max message sizes that should be used by the server. + * + * @param builder The server builder to configure. + */ + protected void configureLimits(final T builder) { + final DataSize maxInboundMessageSize = this.properties.getMaxInboundMessageSize(); + if (maxInboundMessageSize != null) { + builder.maxInboundMessageSize((int) maxInboundMessageSize.toBytes()); + } + final DataSize maxInboundMetadataSize = this.properties.getMaxInboundMetadataSize(); + if (maxInboundMetadataSize != null) { + builder.maxInboundMetadataSize((int) maxInboundMetadataSize.toBytes()); + } + } + + @Override + public String getAddress() { + return this.properties.getAddress(); + } + + @Override + public int getPort() { + return this.properties.getPort(); + } + + @Override + public void addService(final GrpcServiceDefinition service) { + this.serviceList.add(service); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/GrpcServerConfigurer.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/GrpcServerConfigurer.java new file mode 100644 index 000000000..032231354 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/GrpcServerConfigurer.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.server.serverfactory; + +import java.util.Objects; +import java.util.function.Consumer; + +import io.grpc.ServerBuilder; + +/** + * A configurer for {@link ServerBuilder}s which can be used by {@link GrpcServerFactory} to customize the created + * server. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@FunctionalInterface +public interface GrpcServerConfigurer extends Consumer> { + + @Override + default GrpcServerConfigurer andThen(final Consumer> after) { + Objects.requireNonNull(after); + return t -> { + accept(t); + after.accept(t); + }; + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/GrpcServerFactory.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/GrpcServerFactory.java new file mode 100644 index 000000000..7c3f99be1 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/GrpcServerFactory.java @@ -0,0 +1,65 @@ +/* + * 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.server.serverfactory; + +import io.grpc.Server; +import net.devh.boot.grpc.server.service.GrpcServiceDefinition; + +/** + * A factory that can be used to create grpc servers. + * + * @author Michael (yidongnan@gmail.com) + * @since 5/17/16 + */ +public interface GrpcServerFactory { + + /** + * Creates a new grpc server with the stored options. The entire lifecycle management of the server should be + * managed by the calling class. This includes starting and stopping the server. + * + * @return The newly created grpc server. + */ + Server createServer(); + + /** + * Gets the IP address the created server will be bound to. + * + * @return The IP address the server will be bound to. + */ + String getAddress(); + + /** + * Gets the local port the created server will use to listen to requests. + * + * @return Gets the local port the server will use. + */ + int getPort(); + + /** + * Adds the given grpc service definition to this factory. The created server will serve the services described by + * these definitions. + * + *

+ * Note: Adding a service does not effect servers that have already been created. + *

+ * + * @param service The service to add to the grpc server. + */ + void addService(GrpcServiceDefinition service); + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/GrpcServerLifecycle.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/GrpcServerLifecycle.java new file mode 100644 index 000000000..503e0eb11 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/GrpcServerLifecycle.java @@ -0,0 +1,150 @@ +/* + * 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.server.serverfactory; + +import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.MILLISECONDS; + +import java.io.IOException; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; + +import org.springframework.context.SmartLifecycle; + +import io.grpc.Server; +import lombok.extern.slf4j.Slf4j; + +/** + * Lifecycle bean that automatically starts and stops the grpc server. + * + * @author Michael (yidongnan@gmail.com) + */ +@Slf4j +public class GrpcServerLifecycle implements SmartLifecycle { + + private static AtomicInteger serverCounter = new AtomicInteger(-1); + + private final GrpcServerFactory factory; + private final Duration shutdownGracePeriod; + + private Server server; + + /** + * Creates a new GrpcServerLifecycle + * + * @param factory The server factory to use. + * @param shutdownGracePeriod The time to wait for the server to gracefully shut down. + */ + public GrpcServerLifecycle(final GrpcServerFactory factory, final Duration shutdownGracePeriod) { + this.factory = requireNonNull(factory, "factory"); + this.shutdownGracePeriod = requireNonNull(shutdownGracePeriod, "shutdownGracePeriod"); + } + + @Override + public void start() { + try { + createAndStartGrpcServer(); + } catch (final IOException e) { + throw new IllegalStateException("Failed to start the grpc server", e); + } + } + + @Override + public void stop() { + stopAndReleaseGrpcServer(); + } + + @Override + public void stop(final Runnable callback) { + stop(); + callback.run(); + } + + @Override + public boolean isRunning() { + return this.server != null && !this.server.isShutdown(); + } + + @Override + public int getPhase() { + return Integer.MAX_VALUE; + } + + @Override + public boolean isAutoStartup() { + return true; + } + + /** + * Creates and starts the grpc server. + * + * @throws IOException If the server is unable to bind the port. + */ + protected void createAndStartGrpcServer() throws IOException { + if (this.server == null) { + final Server localServer = this.factory.createServer(); + this.server = localServer; + localServer.start(); + log.info("gRPC Server started, listening on address: " + this.factory.getAddress() + ", port: " + + this.factory.getPort()); + + // Prevent the JVM from shutting down while the server is running + final Thread awaitThread = new Thread(() -> { + try { + localServer.awaitTermination(); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + }); + awaitThread.setName("grpc-server-container-" + (serverCounter.incrementAndGet())); + awaitThread.setDaemon(false); + awaitThread.start(); + } + } + + /** + * Initiates an orderly shutdown of the grpc server and releases the references to the server. This call waits for + * the server to be completely shut down. + */ + protected void stopAndReleaseGrpcServer() { + final Server localServer = this.server; + if (localServer != null) { + final long millis = this.shutdownGracePeriod.toMillis(); + log.debug("Initiating gRPC server shutdown"); + localServer.shutdown(); + // Wait for the server to shutdown completely before continuing with destroying the spring context + try { + if (millis > 0) { + localServer.awaitTermination(millis, MILLISECONDS); + } else if (millis == 0) { + // Do not wait + } else { + // Wait infinitely + localServer.awaitTermination(); + } + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + localServer.shutdownNow(); + this.server = null; + } + log.info("Completed gRPC server shutdown"); + } + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/InProcessGrpcServerFactory.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/InProcessGrpcServerFactory.java new file mode 100644 index 000000000..97d632b70 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/InProcessGrpcServerFactory.java @@ -0,0 +1,101 @@ +/* + * 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.server.serverfactory; + +import static java.util.Objects.requireNonNull; + +import java.util.Collections; +import java.util.List; + +import io.grpc.inprocess.InProcessServerBuilder; +import net.devh.boot.grpc.server.config.GrpcServerProperties; + +/** + * Factory for in process grpc servers. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +public class InProcessGrpcServerFactory extends AbstractGrpcServerFactory { + + private final String name; + + /** + * Creates a new in process server factory with the given properties. + * + * @param properties The properties used to configure the server. + */ + public InProcessGrpcServerFactory(final GrpcServerProperties properties) { + this(properties.getInProcessName(), properties); + } + + /** + * Creates a new in process server factory with the given properties. + * + * @param properties The properties used to configure the server. + * @param serverConfigurers The server configurers to use. Can be empty. + */ + public InProcessGrpcServerFactory(final GrpcServerProperties properties, + final List serverConfigurers) { + this(properties.getInProcessName(), properties, serverConfigurers); + } + + /** + * Creates a new in process server factory with the given properties. + * + * @param name The name of the in process server. + * @param properties The properties used to configure the server. + */ + public InProcessGrpcServerFactory(final String name, final GrpcServerProperties properties) { + this(name, properties, Collections.emptyList()); + } + + /** + * Creates a new in process server factory with the given properties. + * + * @param name The name of the in process server. + * @param properties The properties used to configure the server. + * @param serverConfigurers The server configurers to use. Can be empty. + */ + public InProcessGrpcServerFactory(final String name, final GrpcServerProperties properties, + final List serverConfigurers) { + super(properties, serverConfigurers); + this.name = requireNonNull(name, "name"); + } + + @Override + protected InProcessServerBuilder newServerBuilder() { + return InProcessServerBuilder.forName(this.name); + } + + @Override + protected void configureSecurity(final InProcessServerBuilder builder) { + // No need to configure security as we are in process only. + // There is also no need to throw exceptions if transport security is configured. + } + + @Override + public String getAddress() { + return "in-process:" + this.name; + } + + @Override + public int getPort() { + return -1; + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/NettyGrpcServerFactory.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/NettyGrpcServerFactory.java new file mode 100644 index 000000000..9e354e63a --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/NettyGrpcServerFactory.java @@ -0,0 +1,173 @@ +/* + * 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.server.serverfactory; + +import static java.util.Objects.requireNonNull; +import static net.devh.boot.grpc.common.util.GrpcUtils.DOMAIN_SOCKET_ADDRESS_PREFIX; +import static net.devh.boot.grpc.server.config.GrpcServerProperties.ANY_IP_ADDRESS; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLException; + +import org.springframework.core.io.Resource; + +import com.google.common.net.InetAddresses; + +import io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.NettyServerBuilder; +import io.netty.channel.epoll.EpollEventLoopGroup; +import io.netty.channel.epoll.EpollServerDomainSocketChannel; +import io.netty.channel.unix.DomainSocketAddress; +import io.netty.handler.ssl.SslContextBuilder; +import net.devh.boot.grpc.common.util.GrpcUtils; +import net.devh.boot.grpc.server.config.ClientAuth; +import net.devh.boot.grpc.server.config.GrpcServerProperties; +import net.devh.boot.grpc.server.config.GrpcServerProperties.Security; + +/** + * Factory for netty based grpc servers. + * + * @author Michael (yidongnan@gmail.com) + * @since 5/17/16 + */ +public class NettyGrpcServerFactory extends AbstractGrpcServerFactory { + + /** + * Creates a new netty server factory with the given properties. + * + * @param properties The properties used to configure the server. + * @param serverConfigurers The server configurers to use. Can be empty. + */ + public NettyGrpcServerFactory(final GrpcServerProperties properties, + final List serverConfigurers) { + super(properties, serverConfigurers); + } + + @Override + protected NettyServerBuilder newServerBuilder() { + final String address = getAddress(); + final int port = getPort(); + if (address.startsWith(DOMAIN_SOCKET_ADDRESS_PREFIX)) { + final String path = GrpcUtils.extractDomainSocketAddressPath(address); + return NettyServerBuilder.forAddress(new DomainSocketAddress(path)) + .channelType(EpollServerDomainSocketChannel.class) + .bossEventLoopGroup(new EpollEventLoopGroup(1)) + .workerEventLoopGroup(new EpollEventLoopGroup()); + } else if (ANY_IP_ADDRESS.equals(address)) { + return NettyServerBuilder.forPort(port); + } else { + return NettyServerBuilder.forAddress(new InetSocketAddress(InetAddresses.forString(address), port)); + } + } + + @Override + // Keep this in sync with ShadedNettyGrpcServerFactory#configureKeepAlive + protected void configureKeepAlive(final NettyServerBuilder builder) { + if (this.properties.isEnableKeepAlive()) { + builder.keepAliveTime(this.properties.getKeepAliveTime().toNanos(), TimeUnit.NANOSECONDS) + .keepAliveTimeout(this.properties.getKeepAliveTimeout().toNanos(), TimeUnit.NANOSECONDS); + } + builder.permitKeepAliveTime(this.properties.getPermitKeepAliveTime().toNanos(), TimeUnit.NANOSECONDS) + .permitKeepAliveWithoutCalls(this.properties.isPermitKeepAliveWithoutCalls()); + } + + @Override + // Keep this in sync with ShadedNettyGrpcServerFactory#configureConnectionLimits + protected void configureConnectionLimits(final NettyServerBuilder builder) { + if (this.properties.getMaxConnectionIdle() != null) { + builder.maxConnectionIdle(this.properties.getMaxConnectionIdle().toNanos(), TimeUnit.NANOSECONDS); + } + if (this.properties.getMaxConnectionAge() != null) { + builder.maxConnectionAge(this.properties.getMaxConnectionAge().toNanos(), TimeUnit.NANOSECONDS); + } + if (this.properties.getMaxConnectionAgeGrace() != null) { + builder.maxConnectionAgeGrace(this.properties.getMaxConnectionAgeGrace().toNanos(), TimeUnit.NANOSECONDS); + } + } + + @Override + // Keep this in sync with ShadedNettyGrpcServerFactory#configureSecurity + protected void configureSecurity(final NettyServerBuilder builder) { + final Security security = this.properties.getSecurity(); + if (security.isEnabled()) { + final Resource certificateChain = + requireNonNull(security.getCertificateChain(), "certificateChain not configured"); + final Resource privateKey = requireNonNull(security.getPrivateKey(), "privateKey not configured"); + SslContextBuilder sslContextBuilder; + try (InputStream certificateChainStream = certificateChain.getInputStream(); + InputStream privateKeyStream = privateKey.getInputStream()) { + sslContextBuilder = GrpcSslContexts.forServer(certificateChainStream, privateKeyStream, + security.getPrivateKeyPassword()); + } catch (IOException | RuntimeException e) { + throw new IllegalArgumentException("Failed to create SSLContext (PK/Cert)", e); + } + + if (security.getClientAuth() != ClientAuth.NONE) { + sslContextBuilder.clientAuth(of(security.getClientAuth())); + + final Resource trustCertCollection = security.getTrustCertCollection(); + if (trustCertCollection != null) { + try (InputStream trustCertCollectionStream = trustCertCollection.getInputStream()) { + sslContextBuilder.trustManager(trustCertCollectionStream); + } catch (IOException | RuntimeException e) { + throw new IllegalArgumentException("Failed to create SSLContext (TrustStore)", e); + } + } + } + + if (security.getCiphers() != null && !security.getCiphers().isEmpty()) { + sslContextBuilder.ciphers(security.getCiphers()); + } + + if (security.getProtocols() != null && security.getProtocols().length > 0) { + sslContextBuilder.protocols(security.getProtocols()); + } + + try { + builder.sslContext(sslContextBuilder.build()); + } catch (final SSLException e) { + throw new IllegalStateException("Failed to create ssl context for grpc server", e); + } + } + } + + /** + * Converts the given client auth option to netty's client auth. + * + * @param clientAuth The client auth option to convert. + * @return The converted client auth option. + */ + protected static io.netty.handler.ssl.ClientAuth of(final ClientAuth clientAuth) { + switch (clientAuth) { + case NONE: + return io.netty.handler.ssl.ClientAuth.NONE; + case OPTIONAL: + return io.netty.handler.ssl.ClientAuth.OPTIONAL; + case REQUIRE: + return io.netty.handler.ssl.ClientAuth.REQUIRE; + default: + throw new IllegalArgumentException("Unsupported ClientAuth: " + clientAuth); + } + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/ShadedNettyGrpcServerFactory.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/ShadedNettyGrpcServerFactory.java new file mode 100644 index 000000000..dee02746d --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/ShadedNettyGrpcServerFactory.java @@ -0,0 +1,174 @@ +/* + * 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.server.serverfactory; + +import static java.util.Objects.requireNonNull; +import static net.devh.boot.grpc.common.util.GrpcUtils.DOMAIN_SOCKET_ADDRESS_PREFIX; +import static net.devh.boot.grpc.server.config.GrpcServerProperties.ANY_IP_ADDRESS; + +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.net.ssl.SSLException; + +import org.springframework.core.io.Resource; + +import com.google.common.net.InetAddresses; + +import io.grpc.netty.shaded.io.grpc.netty.GrpcSslContexts; +import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; +import io.grpc.netty.shaded.io.netty.channel.epoll.EpollEventLoopGroup; +import io.grpc.netty.shaded.io.netty.channel.epoll.EpollServerDomainSocketChannel; +import io.grpc.netty.shaded.io.netty.channel.unix.DomainSocketAddress; +import io.grpc.netty.shaded.io.netty.handler.ssl.SslContextBuilder; +import net.devh.boot.grpc.common.util.GrpcUtils; +import net.devh.boot.grpc.server.config.ClientAuth; +import net.devh.boot.grpc.server.config.GrpcServerProperties; +import net.devh.boot.grpc.server.config.GrpcServerProperties.Security; + +/** + * Factory for shaded netty based grpc servers. + * + * @author Michael (yidongnan@gmail.com) + * @since 5/17/16 + */ +public class ShadedNettyGrpcServerFactory + extends AbstractGrpcServerFactory { + + /** + * Creates a new shaded netty server factory with the given properties. + * + * @param properties The properties used to configure the server. + * @param serverConfigurers The server configurers to use. Can be empty. + */ + public ShadedNettyGrpcServerFactory(final GrpcServerProperties properties, + final List serverConfigurers) { + super(properties, serverConfigurers); + } + + @Override + protected NettyServerBuilder newServerBuilder() { + final String address = getAddress(); + final int port = getPort(); + if (address.startsWith(DOMAIN_SOCKET_ADDRESS_PREFIX)) { + final String path = GrpcUtils.extractDomainSocketAddressPath(address); + return NettyServerBuilder.forAddress(new DomainSocketAddress(path)) + .channelType(EpollServerDomainSocketChannel.class) + .bossEventLoopGroup(new EpollEventLoopGroup(1)) + .workerEventLoopGroup(new EpollEventLoopGroup()); + } else if (ANY_IP_ADDRESS.equals(address)) { + return NettyServerBuilder.forPort(port); + } else { + return NettyServerBuilder.forAddress(new InetSocketAddress(InetAddresses.forString(address), port)); + } + } + + @Override + // Keep this in sync with NettyGrpcServerFactory#configureConnectionLimits + protected void configureConnectionLimits(final NettyServerBuilder builder) { + if (this.properties.getMaxConnectionIdle() != null) { + builder.maxConnectionIdle(this.properties.getMaxConnectionIdle().toNanos(), TimeUnit.NANOSECONDS); + } + if (this.properties.getMaxConnectionAge() != null) { + builder.maxConnectionAge(this.properties.getMaxConnectionAge().toNanos(), TimeUnit.NANOSECONDS); + } + if (this.properties.getMaxConnectionAgeGrace() != null) { + builder.maxConnectionAgeGrace(this.properties.getMaxConnectionAgeGrace().toNanos(), TimeUnit.NANOSECONDS); + } + } + + @Override + // Keep this in sync with NettyGrpcServerFactory#configureKeepAlive + protected void configureKeepAlive(final NettyServerBuilder builder) { + if (this.properties.isEnableKeepAlive()) { + builder.keepAliveTime(this.properties.getKeepAliveTime().toNanos(), TimeUnit.NANOSECONDS) + .keepAliveTimeout(this.properties.getKeepAliveTimeout().toNanos(), TimeUnit.NANOSECONDS); + } + builder.permitKeepAliveTime(this.properties.getPermitKeepAliveTime().toNanos(), TimeUnit.NANOSECONDS) + .permitKeepAliveWithoutCalls(this.properties.isPermitKeepAliveWithoutCalls()); + } + + @Override + // Keep this in sync with NettyGrpcServerFactory#configureSecurity + protected void configureSecurity(final NettyServerBuilder builder) { + final Security security = this.properties.getSecurity(); + if (security.isEnabled()) { + final Resource certificateChain = + requireNonNull(security.getCertificateChain(), "certificateChain not configured"); + final Resource privateKey = requireNonNull(security.getPrivateKey(), "privateKey not configured"); + SslContextBuilder sslContextBuilder; + try (InputStream certificateChainStream = certificateChain.getInputStream(); + InputStream privateKeyStream = privateKey.getInputStream()) { + sslContextBuilder = GrpcSslContexts.forServer(certificateChainStream, privateKeyStream, + security.getPrivateKeyPassword()); + } catch (IOException | RuntimeException e) { + throw new IllegalArgumentException("Failed to create SSLContext (PK/Cert)", e); + } + + if (security.getClientAuth() != ClientAuth.NONE) { + sslContextBuilder.clientAuth(of(security.getClientAuth())); + + final Resource trustCertCollection = security.getTrustCertCollection(); + if (trustCertCollection != null) { + try (InputStream trustCertCollectionStream = trustCertCollection.getInputStream()) { + sslContextBuilder.trustManager(trustCertCollectionStream); + } catch (IOException | RuntimeException e) { + throw new IllegalArgumentException("Failed to create SSLContext (TrustStore)", e); + } + } + } + + if (security.getCiphers() != null && !security.getCiphers().isEmpty()) { + sslContextBuilder.ciphers(security.getCiphers()); + } + + if (security.getProtocols() != null && security.getProtocols().length > 0) { + sslContextBuilder.protocols(security.getProtocols()); + } + + try { + builder.sslContext(sslContextBuilder.build()); + } catch (final SSLException e) { + throw new IllegalStateException("Failed to create ssl context for grpc server", e); + } + } + } + + /** + * Converts the given client auth option to netty's client auth. + * + * @param clientAuth The client auth option to convert. + * @return The converted client auth option. + */ + protected static io.grpc.netty.shaded.io.netty.handler.ssl.ClientAuth of(final ClientAuth clientAuth) { + switch (clientAuth) { + case NONE: + return io.grpc.netty.shaded.io.netty.handler.ssl.ClientAuth.NONE; + case OPTIONAL: + return io.grpc.netty.shaded.io.netty.handler.ssl.ClientAuth.OPTIONAL; + case REQUIRE: + return io.grpc.netty.shaded.io.netty.handler.ssl.ClientAuth.REQUIRE; + default: + throw new IllegalArgumentException("Unsupported ClientAuth: " + clientAuth); + } + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/package-info.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/package-info.java new file mode 100644 index 000000000..dbab04a4d --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/serverfactory/package-info.java @@ -0,0 +1,5 @@ +/** + * Contains factories and related classes to setup the server. + */ + +package net.devh.boot.grpc.server.serverfactory; diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/AnnotationGrpcServiceDiscoverer.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/AnnotationGrpcServiceDiscoverer.java new file mode 100644 index 000000000..6ae50c6f0 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/AnnotationGrpcServiceDiscoverer.java @@ -0,0 +1,100 @@ +/* + * 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.server.service; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +import com.google.common.collect.Lists; + +import io.grpc.BindableService; +import io.grpc.ServerInterceptor; +import io.grpc.ServerInterceptors; +import io.grpc.ServerServiceDefinition; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.interceptor.GlobalServerInterceptorRegistry; + +/** + * A {@link GrpcServiceDiscoverer} that searches for beans with the {@link GrpcService} annotations. + * + * @author Michael (yidongnan@gmail.com) + * @since 5/17/16 + */ +@Slf4j +public class AnnotationGrpcServiceDiscoverer implements ApplicationContextAware, GrpcServiceDiscoverer { + + private ApplicationContext applicationContext; + + @Override + public void setApplicationContext(final ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + public Collection findGrpcServices() { + Collection beanNames = + Arrays.asList(this.applicationContext.getBeanNamesForAnnotation(GrpcService.class)); + List definitions = Lists.newArrayListWithCapacity(beanNames.size()); + GlobalServerInterceptorRegistry globalServerInterceptorRegistry = + applicationContext.getBean(GlobalServerInterceptorRegistry.class); + for (String beanName : beanNames) { + BindableService bindableService = this.applicationContext.getBean(beanName, BindableService.class); + ServerServiceDefinition serviceDefinition = bindableService.bindService(); + GrpcService grpcServiceAnnotation = applicationContext.findAnnotationOnBean(beanName, GrpcService.class); + serviceDefinition = + bindInterceptors(serviceDefinition, grpcServiceAnnotation, globalServerInterceptorRegistry); + definitions.add(new GrpcServiceDefinition(beanName, bindableService.getClass(), serviceDefinition)); + log.debug("Found gRPC service: " + serviceDefinition.getServiceDescriptor().getName() + ", bean: " + + beanName + ", class: " + bindableService.getClass().getName()); + } + return definitions; + } + + private ServerServiceDefinition bindInterceptors(final ServerServiceDefinition serviceDefinition, + final GrpcService grpcServiceAnnotation, + final GlobalServerInterceptorRegistry globalServerInterceptorRegistry) { + final List interceptors = Lists.newArrayList(); + interceptors.addAll(globalServerInterceptorRegistry.getServerInterceptors()); + for (final Class interceptorClass : grpcServiceAnnotation.interceptors()) { + final ServerInterceptor serverInterceptor; + if (this.applicationContext.getBeanNamesForType(interceptorClass).length > 0) { + serverInterceptor = this.applicationContext.getBean(interceptorClass); + } else { + try { + serverInterceptor = interceptorClass.getConstructor().newInstance(); + } catch (final Exception e) { + throw new BeanCreationException("Failed to create interceptor instance", e); + } + } + interceptors.add(serverInterceptor); + } + for (final String interceptorName : grpcServiceAnnotation.interceptorNames()) { + interceptors.add(this.applicationContext.getBean(interceptorName, ServerInterceptor.class)); + } + if (grpcServiceAnnotation.sortInterceptors()) { + globalServerInterceptorRegistry.sortInterceptors(interceptors); + } + return ServerInterceptors.interceptForward(serviceDefinition, interceptors); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/GrpcService.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/GrpcService.java new file mode 100644 index 000000000..79a4b843e --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/GrpcService.java @@ -0,0 +1,84 @@ +/* + * 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.server.service; + +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 org.springframework.context.annotation.Bean; +import org.springframework.stereotype.Service; + +import io.grpc.BindableService; +import io.grpc.ServerInterceptor; +import io.grpc.ServerInterceptors; + +/** + * Annotation that marks gRPC services that should be registered with a gRPC server. If spring-boot's auto configuration + * is used, then the server will be created automatically. This annotation should only be added to implementations of + * {@link BindableService} (GrpcService-ImplBase). + * + *

+ * Note: These annotation allows the specification of custom interceptors. These will be appended to the global + * interceptors and applied using {@link ServerInterceptors#interceptForward(BindableService, ServerInterceptor...)}. + *

+ * + * @author Michael (yidongnan@gmail.com) + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Service +@Bean +public @interface GrpcService { + + /** + * A list of {@link ServerInterceptor} classes that should be applied to only this service. If a bean of the given + * type exists, it will be used; otherwise a new instance of that class will be created via no-args constructor. + * + *

+ * Note: Please read the javadocs regarding the ordering of interceptors. + *

+ * + * @return A list of ServerInterceptor classes that should be used. + */ + Class[] interceptors() default {}; + + /** + * A list of {@link ServerInterceptor} beans that should be applied to only this service. + * + *

+ * Note: Please read the javadocs regarding the ordering of interceptors. + *

+ * + * @return A list of ServerInterceptor beans that should be used. + */ + String[] interceptorNames() default {}; + + /** + * Whether the custom interceptors should be mixed with the global interceptors and sorted afterwards. Use this + * option if you want to add a custom interceptor between global interceptors. + * + * @return True, if the custom interceptors should be merged with the global ones and sorted afterwards. False + * otherwise. + */ + boolean sortInterceptors() default false; + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/GrpcServiceDefinition.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/GrpcServiceDefinition.java new file mode 100644 index 000000000..abc14d7b0 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/GrpcServiceDefinition.java @@ -0,0 +1,76 @@ +/* + * 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.server.service; + +import io.grpc.ServerServiceDefinition; + +/** + * Container class that contains all relevant information about a grpc service. + * + * @author Michael (yidongnan@gmail.com) + * @since 5/17/16 + * @see GrpcServiceDiscoverer + */ +public class GrpcServiceDefinition { + + private final String beanName; + private final Class beanClazz; + private final ServerServiceDefinition definition; + + /** + * Creates a new GrpcServiceDefinition. + * + * @param beanName The name of the grpc service bean in the spring context. + * @param beanClazz The class of the grpc service bean. + * @param definition The grpc service definition. + */ + public GrpcServiceDefinition(final String beanName, final Class beanClazz, + final ServerServiceDefinition definition) { + this.beanName = beanName; + this.beanClazz = beanClazz; + this.definition = definition; + } + + /** + * Gets the name of the grpc service bean. + * + * @return The name of the bean. + */ + public String getBeanName() { + return this.beanName; + } + + /** + * Gets the class of the grpc service bean. + * + * @return The class of the grpc service bean. + */ + public Class getBeanClazz() { + return this.beanClazz; + } + + /** + * Gets the grpc service definition. + * + * @return The grpc service definition. + */ + public ServerServiceDefinition getDefinition() { + return this.definition; + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/GrpcServiceDiscoverer.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/GrpcServiceDiscoverer.java new file mode 100644 index 000000000..4eb13f22b --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/GrpcServiceDiscoverer.java @@ -0,0 +1,41 @@ +/* + * 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.server.service; + +import java.util.Collection; + +import net.devh.boot.grpc.server.serverfactory.GrpcServerFactory; + +/** + * An interface for a bean that will be used to find grpc services and codecs. These will then be provided to the + * {@link GrpcServerFactory} which then uses them to configure the server. + * + * @author Michael (yidongnan@gmail.com) + * @since 5/17/16 + */ +@FunctionalInterface +public interface GrpcServiceDiscoverer { + + /** + * Find the grpc services that should provided by the server. + * + * @return The grpc services that should be provided. Never null. + */ + Collection findGrpcServices(); + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/package-info.java b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/package-info.java new file mode 100644 index 000000000..9694690e4 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/java/net/devh/boot/grpc/server/service/package-info.java @@ -0,0 +1,5 @@ +/** + * Classes related to the gRPC server services and their discovery. + */ + +package net.devh.boot.grpc.server.service; diff --git a/grpc-server-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/grpc-server-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..9d4fea620 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -0,0 +1,14 @@ +# AutoConfiguration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +net.devh.boot.grpc.server.autoconfigure.GrpcAdviceAutoConfiguration,\ +net.devh.boot.grpc.server.autoconfigure.GrpcHealthServiceAutoConfiguration,\ +net.devh.boot.grpc.server.autoconfigure.GrpcMetadataConsulConfiguration,\ +net.devh.boot.grpc.server.autoconfigure.GrpcMetadataEurekaConfiguration,\ +net.devh.boot.grpc.server.autoconfigure.GrpcMetadataNacosConfiguration,\ +net.devh.boot.grpc.server.autoconfigure.GrpcMetadataZookeeperConfiguration,\ +net.devh.boot.grpc.server.autoconfigure.GrpcReflectionServiceAutoConfiguration,\ +net.devh.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration,\ +net.devh.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration,\ +net.devh.boot.grpc.server.autoconfigure.GrpcServerMetricAutoConfiguration,\ +net.devh.boot.grpc.server.autoconfigure.GrpcServerSecurityAutoConfiguration,\ +net.devh.boot.grpc.server.autoconfigure.GrpcServerTraceAutoConfiguration diff --git a/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/advice/GrpcAdviceDiscovererTest.java b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/advice/GrpcAdviceDiscovererTest.java new file mode 100644 index 000000000..84e5c7b5e --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/advice/GrpcAdviceDiscovererTest.java @@ -0,0 +1,116 @@ +/* + * 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.server.advice; + +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.ApplicationContext; + +import io.grpc.Status; + +/** + * Tests for {@link GrpcAdviceDiscoverer}. + */ +class GrpcAdviceDiscovererTest { + + private final ApplicationContext context = mock(ApplicationContext.class); + + @BeforeEach + void beforeEach() { + reset(this.context); + } + + /** + * Tests that the {@link GrpcAdviceDiscoverer} discovers inherited methods. + */ + @Test + void testDiscoversInheritedMethods() { + when(this.context.getBeansWithAnnotation(GrpcAdvice.class)) + .thenReturn(singletonMap("bean", new Extended())); + + final GrpcAdviceDiscoverer disco = new GrpcAdviceDiscoverer(); + disco.setApplicationContext(this.context); + disco.afterPropertiesSet(); + + assertThat(disco.getAnnotatedMethods()) + .containsExactlyInAnyOrder( + findMethod(Base.class, "handleRuntimeException", RuntimeException.class), + findMethod(Extended.class, "handleIllegalArgument", IllegalArgumentException.class)); + } + + @Test + void testOverriddenMethods() { + when(this.context.getBeansWithAnnotation(GrpcAdvice.class)) + .thenReturn(singletonMap("bean", new Overriden())); + + final GrpcAdviceDiscoverer disco = new GrpcAdviceDiscoverer(); + disco.setApplicationContext(this.context); + disco.afterPropertiesSet(); + + assertThat(disco.getAnnotatedMethods()) + .containsExactly(findMethod(Overriden.class, "handleRuntimeException", RuntimeException.class)); + } + + private static Method findMethod(final Class clazz, final String method, final Class... parameters) { + try { + return clazz.getDeclaredMethod(method, parameters); + } catch (NoSuchMethodException | SecurityException e) { + throw new IllegalStateException("Failed to find method", e); + } + } + + @GrpcAdvice + private class Base { + + @GrpcExceptionHandler + Status handleRuntimeException(final RuntimeException e) { + return Status.INTERNAL; + } + + } + + @GrpcAdvice + private class Extended extends Base { + + @GrpcExceptionHandler + Status handleIllegalArgument(final IllegalArgumentException e) { + return Status.INVALID_ARGUMENT; + } + + } + + @GrpcAdvice + private class Overriden extends Base { + + @Override + @GrpcExceptionHandler + Status handleRuntimeException(final RuntimeException e) { + return Status.INVALID_ARGUMENT; + } + + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/AwaitableStreamObserver.java b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/AwaitableStreamObserver.java new file mode 100644 index 000000000..f9595a42b --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/AwaitableStreamObserver.java @@ -0,0 +1,90 @@ +/* + * 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.server.autoconfigure; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; + +import io.grpc.stub.StreamObserver; + +class AwaitableStreamObserver implements StreamObserver { + + private final CountDownLatch doneLatch = new CountDownLatch(1); + private final List results = new ArrayList<>(); + private volatile Throwable error; + + @Override + public void onNext(final V value) { + this.results.add(value); + } + + @Override + public void onError(final Throwable t) { + this.error = t; + this.doneLatch.countDown(); + } + + @Override + public void onCompleted() { + this.doneLatch.countDown(); + } + + public V getFirst() throws InterruptedException { + this.doneLatch.await(); + if (this.error != null) { + throw new IllegalStateException("Request failed with unexpected error", this.error); + } + if (this.results.isEmpty()) { + throw new IllegalStateException("Requested completed without response"); + } + return this.results.get(0); + } + + public V getSingle() throws InterruptedException { + this.doneLatch.await(); + if (this.error != null) { + throw new IllegalStateException("Request failed with unexpected error", this.error); + } + if (this.results.isEmpty()) { + throw new IllegalStateException("Requested completed without response"); + } + if (this.results.size() != 1) { + throw new IllegalStateException( + "Request completed with more than one response - Got " + this.results.size()); + } + return this.results.get(0); + } + + public List getAll() throws InterruptedException { + this.doneLatch.await(); + if (this.error != null) { + throw new IllegalStateException("Request failed with unexpected error", this.error); + } + return this.results; + } + + public Throwable getError() throws InterruptedException { + this.doneLatch.await(); + if (this.error == null) { + throw new IllegalStateException("Request completed without error"); + } + return this.error; + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcHealthServiceDefaultAutoConfigurationTest.java b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcHealthServiceDefaultAutoConfigurationTest.java new file mode 100644 index 000000000..f85adc3d1 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcHealthServiceDefaultAutoConfigurationTest.java @@ -0,0 +1,68 @@ +/* + * 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.server.autoconfigure; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.health.v1.HealthCheckRequest; +import io.grpc.health.v1.HealthCheckResponse; +import io.grpc.health.v1.HealthCheckResponse.ServingStatus; +import io.grpc.health.v1.HealthGrpc; +import io.grpc.health.v1.HealthGrpc.HealthStub; + +@SpringBootTest(classes = GrpcHealthServiceDefaultAutoConfigurationTest.TestConfig.class) +@ImportAutoConfiguration({ + GrpcServerAutoConfiguration.class, + GrpcServerFactoryAutoConfiguration.class, + GrpcHealthServiceAutoConfiguration.class}) +@DirtiesContext +class GrpcHealthServiceDefaultAutoConfigurationTest { + + private static final HealthCheckRequest HEALTH_CHECK_REQUEST = HealthCheckRequest.getDefaultInstance(); + + @Test + void testHealthService() { + final ManagedChannel channel = ManagedChannelBuilder.forTarget("localhost:9090").usePlaintext().build(); + try { + final HealthStub stub = HealthGrpc.newStub(channel); + + final AwaitableStreamObserver resultObserver = new AwaitableStreamObserver<>(); + stub.check(HEALTH_CHECK_REQUEST, resultObserver); + checkResult(resultObserver); + } finally { + channel.shutdown(); + } + } + + void checkResult(final AwaitableStreamObserver resultObserver) { + final HealthCheckResponse response = assertDoesNotThrow(resultObserver::getSingle); + assertEquals(ServingStatus.SERVING, response.getStatus()); + } + + static class TestConfig { + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcHealthServiceFalseAutoConfigurationTest.java b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcHealthServiceFalseAutoConfigurationTest.java new file mode 100644 index 000000000..266c2846b --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcHealthServiceFalseAutoConfigurationTest.java @@ -0,0 +1,51 @@ +/* + * 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.server.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.type; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +import io.grpc.Status; +import io.grpc.Status.Code; +import io.grpc.StatusRuntimeException; +import io.grpc.health.v1.HealthCheckResponse; + +@SpringBootTest(classes = GrpcHealthServiceDefaultAutoConfigurationTest.TestConfig.class, + properties = "grpc.server.health-service-enabled=false") +@ImportAutoConfiguration({ + GrpcServerAutoConfiguration.class, + GrpcServerFactoryAutoConfiguration.class, + GrpcHealthServiceAutoConfiguration.class}) +@DirtiesContext +class GrpcHealthServiceFalseAutoConfigurationTest extends GrpcHealthServiceDefaultAutoConfigurationTest { + + @Override + void checkResult(final AwaitableStreamObserver resultObserver) { + final Throwable error = assertDoesNotThrow(resultObserver::getError); + assertThat(error).asInstanceOf(type(StatusRuntimeException.class)) + .extracting(StatusRuntimeException::getStatus) + .extracting(Status::getCode) + .isEqualTo(Code.UNIMPLEMENTED); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcHealthServiceTrueAutoConfigurationTest.java b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcHealthServiceTrueAutoConfigurationTest.java new file mode 100644 index 000000000..0476c80df --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcHealthServiceTrueAutoConfigurationTest.java @@ -0,0 +1,32 @@ +/* + * 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.server.autoconfigure; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +@SpringBootTest(classes = GrpcHealthServiceDefaultAutoConfigurationTest.TestConfig.class, + properties = "grpc.server.health-service-enabled=true") +@ImportAutoConfiguration({ + GrpcServerAutoConfiguration.class, + GrpcServerFactoryAutoConfiguration.class, + GrpcHealthServiceAutoConfiguration.class}) +@DirtiesContext +class GrpcHealthServiceTrueAutoConfigurationTest extends GrpcHealthServiceDefaultAutoConfigurationTest { +} diff --git a/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcReflectionServiceDefaultAutoConfigurationTest.java b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcReflectionServiceDefaultAutoConfigurationTest.java new file mode 100644 index 000000000..e3f81fd6b --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcReflectionServiceDefaultAutoConfigurationTest.java @@ -0,0 +1,71 @@ +/* + * 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.server.autoconfigure; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import io.grpc.reflection.v1alpha.ServerReflectionGrpc; +import io.grpc.reflection.v1alpha.ServerReflectionGrpc.ServerReflectionStub; +import io.grpc.reflection.v1alpha.ServerReflectionRequest; +import io.grpc.reflection.v1alpha.ServerReflectionResponse; +import io.grpc.stub.StreamObserver; + +@SpringBootTest(classes = GrpcReflectionServiceDefaultAutoConfigurationTest.TestConfig.class) +@ImportAutoConfiguration({ + GrpcServerAutoConfiguration.class, + GrpcServerFactoryAutoConfiguration.class, + GrpcReflectionServiceAutoConfiguration.class}) +@DirtiesContext +class GrpcReflectionServiceDefaultAutoConfigurationTest { + + @Test + void testReflectionService() { + final ManagedChannel channel = ManagedChannelBuilder.forTarget("localhost:9090").usePlaintext().build(); + try { + final ServerReflectionStub stub = ServerReflectionGrpc.newStub(channel); + + final AwaitableStreamObserver resultObserver = new AwaitableStreamObserver<>(); + final StreamObserver requestObserver = stub.serverReflectionInfo(resultObserver); + requestObserver.onNext(ServerReflectionRequest.newBuilder() + .setListServices("") + .build()); + requestObserver.onCompleted(); + checkResult(resultObserver); + } finally { + channel.shutdown(); + } + } + + void checkResult(final AwaitableStreamObserver resultObserver) { + final ServerReflectionResponse response = assertDoesNotThrow(resultObserver::getSingle); + assertEquals("grpc.reflection.v1alpha.ServerReflection", + response.getListServicesResponse().getServiceList().get(0).getName()); + } + + static class TestConfig { + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcReflectionServiceFalseAutoConfigurationTest.java b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcReflectionServiceFalseAutoConfigurationTest.java new file mode 100644 index 000000000..023bf6853 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcReflectionServiceFalseAutoConfigurationTest.java @@ -0,0 +1,51 @@ +/* + * 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.server.autoconfigure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.InstanceOfAssertFactories.type; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +import io.grpc.Status; +import io.grpc.Status.Code; +import io.grpc.StatusRuntimeException; +import io.grpc.reflection.v1alpha.ServerReflectionResponse; + +@SpringBootTest(classes = GrpcReflectionServiceDefaultAutoConfigurationTest.TestConfig.class, + properties = "grpc.server.reflection-service-enabled=false") +@ImportAutoConfiguration({ + GrpcServerAutoConfiguration.class, + GrpcServerFactoryAutoConfiguration.class, + GrpcReflectionServiceAutoConfiguration.class}) +@DirtiesContext +class GrpcReflectionServiceFalseAutoConfigurationTest extends GrpcReflectionServiceDefaultAutoConfigurationTest { + + @Override + void checkResult(final AwaitableStreamObserver resultObserver) { + final Throwable error = assertDoesNotThrow(resultObserver::getError); + assertThat(error).asInstanceOf(type(StatusRuntimeException.class)) + .extracting(StatusRuntimeException::getStatus) + .extracting(Status::getCode) + .isEqualTo(Code.UNIMPLEMENTED); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcReflectionServiceTrueAutoConfigurationTest.java b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcReflectionServiceTrueAutoConfigurationTest.java new file mode 100644 index 000000000..9ecc907ba --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/autoconfigure/GrpcReflectionServiceTrueAutoConfigurationTest.java @@ -0,0 +1,32 @@ +/* + * 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.server.autoconfigure; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; + +@SpringBootTest(classes = GrpcReflectionServiceDefaultAutoConfigurationTest.TestConfig.class, + properties = "grpc.server.reflection-service-enabled=true") +@ImportAutoConfiguration({ + GrpcServerAutoConfiguration.class, + GrpcServerFactoryAutoConfiguration.class, + GrpcReflectionServiceAutoConfiguration.class}) +@DirtiesContext +class GrpcReflectionServiceTrueAutoConfigurationTest extends GrpcReflectionServiceDefaultAutoConfigurationTest { +} diff --git a/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesConfig.java b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesConfig.java new file mode 100644 index 000000000..ada13767c --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesConfig.java @@ -0,0 +1,29 @@ +/* + * 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.server.config; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * Dummy config - because Spring needs one. + */ +@SpringBootConfiguration +@EnableConfigurationProperties(GrpcServerProperties.class) +public class GrpcServerPropertiesConfig { +} diff --git a/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesGivenUnitTest.java b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesGivenUnitTest.java new file mode 100644 index 000000000..4646847d4 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesGivenUnitTest.java @@ -0,0 +1,52 @@ +/* + * 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.server.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.util.unit.DataSize; + +/** + * Tests whether the property resolution works when using suffixes. + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = { + "grpc.server.keepAliveTime=42m", + "grpc.server.maxInboundMessageSize=5MB", + "grpc.server.maxInboundMetadataSize=10KB" +}) +class GrpcServerPropertiesGivenUnitTest { + + @Autowired + private GrpcServerProperties grpcServerProperties; + + @Test + void test() { + assertEquals(Duration.ofMinutes(42), this.grpcServerProperties.getKeepAliveTime()); + assertEquals(DataSize.ofMegabytes(5), this.grpcServerProperties.getMaxInboundMessageSize()); + assertEquals(DataSize.ofKilobytes(10), this.grpcServerProperties.getMaxInboundMetadataSize()); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesNegativeGivenUnitTest.java b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesNegativeGivenUnitTest.java new file mode 100644 index 000000000..f840e7ab6 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesNegativeGivenUnitTest.java @@ -0,0 +1,47 @@ +/* + * 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.server.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * Tests whether the property resolution works with negative values and suffixes. + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = { + "grpc.server.shutdownGracePeriod=-1ms" +}) +class GrpcServerPropertiesNegativeGivenUnitTest { + + @Autowired + private GrpcServerProperties grpcServerProperties; + + @Test + void test() { + assertEquals(Duration.ofMillis(-1), this.grpcServerProperties.getShutdownGracePeriod()); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesNegativeNoUnitTest.java b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesNegativeNoUnitTest.java new file mode 100644 index 000000000..22f4d2c30 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesNegativeNoUnitTest.java @@ -0,0 +1,47 @@ +/* + * 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.server.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * Tests whether the property resolution works with negative values and without suffixes. + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = { + "grpc.server.shutdownGracePeriod=-1" +}) +class GrpcServerPropertiesNegativeNoUnitTest { + + @Autowired + private GrpcServerProperties grpcServerProperties; + + @Test + void test() { + assertEquals(Duration.ofSeconds(-1), this.grpcServerProperties.getShutdownGracePeriod()); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesNoUnitTest.java b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesNoUnitTest.java new file mode 100644 index 000000000..51b57afc6 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/config/GrpcServerPropertiesNoUnitTest.java @@ -0,0 +1,50 @@ +/* + * 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.server.config; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.util.unit.DataSize; + +/** + * Tests whether the property resolution works without suffixes (backwards compatibility). + */ +@ExtendWith(SpringExtension.class) +@SpringBootTest(properties = { + "grpc.server.keepAliveTime=42", + "grpc.server.maxInboundMessageSize=5242880" +}) +class GrpcServerPropertiesNoUnitTest { + + @Autowired + private GrpcServerProperties grpcServerProperties; + + @Test + void test() { + assertEquals(Duration.ofSeconds(42), this.grpcServerProperties.getKeepAliveTime()); + assertEquals(DataSize.ofMegabytes(5), this.grpcServerProperties.getMaxInboundMessageSize()); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/serverfactory/AbstractGrpcServerFactoryTest.java b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/serverfactory/AbstractGrpcServerFactoryTest.java new file mode 100644 index 000000000..94f1df9b2 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/serverfactory/AbstractGrpcServerFactoryTest.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.server.serverfactory; + +import static java.util.Collections.emptyList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +import io.grpc.ServerBuilder; +import io.grpc.netty.NettyServerBuilder; +import io.grpc.protobuf.services.ProtoReflectionService; +import io.grpc.reflection.v1alpha.ServerReflectionGrpc; +import net.devh.boot.grpc.server.config.GrpcServerProperties; +import net.devh.boot.grpc.server.service.GrpcServiceDefinition; + +/** + * Tests for {@link AbstractGrpcServerFactory}. + */ +class AbstractGrpcServerFactoryTest { + + /** + * Tests {@link AbstractGrpcServerFactory#configureServices(ServerBuilder)}. + */ + @Test + void testConfigureServices() { + final GrpcServerProperties properties = new GrpcServerProperties(); + properties.setReflectionServiceEnabled(false); + + final NettyGrpcServerFactory serverFactory = new NettyGrpcServerFactory(properties, emptyList()); + + serverFactory.addService(new GrpcServiceDefinition("test1", ProtoReflectionService.class, + ProtoReflectionService.newInstance().bindService())); + serverFactory.addService(new GrpcServiceDefinition("test2", ProtoReflectionService.class, + ProtoReflectionService.newInstance().bindService())); + + final NettyServerBuilder serverBuilder = serverFactory.newServerBuilder(); + + assertEquals("Found duplicate service implementation: " + ServerReflectionGrpc.SERVICE_NAME, + assertThrows(IllegalStateException.class, () -> serverFactory.configureServices(serverBuilder)) + .getMessage()); + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/serverfactory/GrpcServerLifecycleTest.java b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/serverfactory/GrpcServerLifecycleTest.java new file mode 100644 index 000000000..4793d5831 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/test/java/net/devh/boot/grpc/server/serverfactory/GrpcServerLifecycleTest.java @@ -0,0 +1,197 @@ +/* + * 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.server.serverfactory; + +import static java.time.Duration.ZERO; +import static java.time.Duration.ofMillis; +import static java.time.Duration.ofSeconds; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTimeout; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import io.grpc.Server; + +/** + * Tests for {@link GrpcServerLifecycle}. + */ +class GrpcServerLifecycleTest { + + private final GrpcServerFactory factory = mock(GrpcServerFactory.class); + + @BeforeEach + void beforeEach() { + reset(this.factory); + when(this.factory.getAddress()).thenReturn("test"); + when(this.factory.getPort()).thenReturn(-1); + + } + + @Test + void testNoGraceShutdown() { + // The server takes 5s seconds to shutdown + final TestServer server = new TestServer(5000); + when(this.factory.createServer()).thenReturn(server); + + // But we won't wait + final GrpcServerLifecycle lifecycle = new GrpcServerLifecycle(this.factory, ZERO); + + lifecycle.start(); + + assertFalse(server.isShutdown()); + assertFalse(server.isTerminated()); + + // So the shutdown should complete near instantly + assertTimeoutPreemptively(ofMillis(100), (Executable) lifecycle::stop); + + assertTrue(server.isShutdown()); + assertTrue(server.isTerminated()); + } + + @Test + void testGracefulShutdown() { + + // The server takes 2s seconds to shutdown + final TestServer server = new TestServer(2000); + when(this.factory.createServer()).thenReturn(server); + + // And we give it 5s to shutdown + final GrpcServerLifecycle lifecycle = new GrpcServerLifecycle(this.factory, ofMillis(5000)); + + lifecycle.start(); + + assertFalse(server.isShutdown()); + assertFalse(server.isTerminated()); + + // So it should finish within 5.1 seconds + assertTimeout(ofMillis(5100), (Executable) lifecycle::stop); + + assertTrue(server.isShutdown()); + assertTrue(server.isTerminated()); + + } + + @Test + void testAwaitShutdown() { + + // The server takes 2s seconds to shutdown + final TestServer server = new TestServer(5000); + when(this.factory.createServer()).thenReturn(server); + + // And we give it infinite time to shutdown + final GrpcServerLifecycle lifecycle = new GrpcServerLifecycle(this.factory, ofSeconds(-1)); + + lifecycle.start(); + + assertFalse(server.isShutdown()); + assertFalse(server.isTerminated()); + + final long start = System.currentTimeMillis(); + + lifecycle.stop(); + + final long duration = System.currentTimeMillis() - start; + // We waited for the entire duration + assertThat(duration).isBetween(5000L, 5100L); + + assertTrue(server.isShutdown()); + assertTrue(server.isTerminated()); + + } + + public class TestServer extends Server { + + private final long shutdownDelayMillis; + + private boolean isShutdown = true; + private CountDownLatch countDown = null; + + public TestServer(final long shutdownDelayMillis) { + this.shutdownDelayMillis = shutdownDelayMillis; + } + + @Override + public Server start() throws IOException { + this.countDown = new CountDownLatch(1); + this.isShutdown = false; + return this; + } + + @Override + public Server shutdown() { + this.isShutdown = true; + final Thread t = new Thread(() -> { + try { + Thread.sleep(this.shutdownDelayMillis); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + shutdownNow(); + }); + t.setName("test-server-shutdown-delay"); + t.setDaemon(true); + t.start(); + return this; + } + + @Override + public Server shutdownNow() { + this.isShutdown = true; + final CountDownLatch localCountDown = this.countDown; + this.countDown = null; + if (localCountDown != null) { + localCountDown.countDown(); + } + return this; + } + + @Override + public boolean isShutdown() { + return this.isShutdown; + } + + @Override + public boolean isTerminated() { + return this.countDown == null; + } + + @Override + public boolean awaitTermination(final long timeout, final TimeUnit unit) throws InterruptedException { + return this.countDown.await(timeout, unit); + } + + @Override + public void awaitTermination() throws InterruptedException { + this.countDown.await(); + } + + } + +} diff --git a/grpc-server-spring-boot-autoconfigure/src/test/resources/logback-test.xml b/grpc-server-spring-boot-autoconfigure/src/test/resources/logback-test.xml new file mode 100644 index 000000000..f0c8da0f9 --- /dev/null +++ b/grpc-server-spring-boot-autoconfigure/src/test/resources/logback-test.xml @@ -0,0 +1,17 @@ + + + + + + + %highlight(%d{HH:mm:ss.SSS} [%10thread] %-5level %logger{36} - %msg%n) + + + + + + + + + + diff --git a/grpc-server-spring-boot-starter/build.gradle b/grpc-server-spring-boot-starter/build.gradle new file mode 100644 index 000000000..4ed0c7f63 --- /dev/null +++ b/grpc-server-spring-boot-starter/build.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java-library' +} + +apply from: '../deploy.gradle' + +group = 'net.devh' +version = projectVersion + +dependencies { + api project(':grpc-server-spring-boot-autoconfigure') +} diff --git a/private.key.enc b/private.key.enc new file mode 100644 index 0000000000000000000000000000000000000000..af7f96d605f78e4a77486e5b8051bda6d75c0ecc GIT binary patch literal 3600 zcmV+r4)5{Y7zx6_mk(XCrPm7bwM&@iNe{dsdJbHn*F9&=Mqb(L+zbwsoD^U|A!j_l zDob1mn`BOasnIi_qsdsKY0I}pc`L_>>cDnEaM`z?8!YwSL+fjQ&z95_M#c43iq5y~ zpUfCEe57RfkcWok%$oX&#D*Z`xm?pd#(;~?PXZckmwIQe8<=~~&Gr^7C)h9kJ->&` zmYcnXU{2^2l|J_;7VqZh`|z#gY@q{;it-s5xevz;97@c()rNi04~1v8 z7Y&TyroQ76xNn$ur`DOVaK@{A^&oK{{d|G2FHLcP5}wKxr#2XQfQuGU z{pDWDu)7n9s-$GWPi9s-jo0HTo+}4O+d)Apudo95u4`|@gWdX(j+v9bipAk0eGzr= z6yXu#VZGyuPJfn;rx=>vq$II(v)Kd<-9po>JX!n6buxfmDS(7)H~}#H42Xd{0Ro!v`!i`z z|H4)yBnQfgogF7A%}s+bC{HHk73`y?suh$6<2wk)0$-51URujg>OL1^$XP>=U7W&y z^gS|)5<42T{7h@O)9P9Z1tgy2b$C3AHMn=)-B^rY)k{Ah_c-ENQvWJRRTH2EI4^;b4(Xu2=ihcl8WIpo!T;

gw~3g6)KJ@i6ZBht`h@9pw)bvkz4Qsz+V{bfqu49P4z3~fgAuvAv66VHpHm*| zcz^4*{LyQIc!4j+0z&ws!L}v-Ksy{$8(0@q{S4U9thp^xXk#kg+=t#9IzqtB5h%*65{(}_N4vi%4tCkB+JfvD_-*1OID#_ud z?%#vqF`bzV1#8@lY11_^RV|3KdFmief;s@NSQnTpLkl#`>LH1IEJ?i=-M6KLCTPJ~Dl0%pMXE1;g7X zZSkZ~*Y~`t;X^pfo%OCn-FsahzBOWzU9;*P4m068_;J$Mo6D0slmA~8Xl}{ZmGJYh zL-1lVrVgkiTPzY1*s*Fz%r#(od_6P5_bYzf*Xozd2PhE(c1)J_E_b+JxYv$Dm#%+!URUeTJ2I& z)G{zDU*j454o2Xylw@u)BN0{7aPX=7j-Y)3(!miSH_u?w!A4oSIXgg~=Yga9cZ;=m zEc_vdRfEVl*s|?iiBMWI`q<}g3#2=W0+Q7N88KVIMJ8_ubY@omrFZ1X*OpjnrS0}b zEpH-V@Pi7K-prO-FWIZ7wpH7ayjpo32pI3@$J>mGbh<;ZV<$g+sjgu#k&?5slWOu>x-zdWJ5*LXMs zI*)O4u}p$&U=ftZS{!(KsZsB=uHE{Pmu)sH^RlY&Kd!Ml%>MdrQ&WvJ(oe(#faa4Q zNKnN<7Uq(L14b?-rK=o|B)QxsdhA+LG5_0z@Nd;b^~ZnYRmhkS!yS>N?kRX$$WT_# z@1Wt{S=|J~G}!V2=OF91wSvF*y=XBLP|4RpL zjZkldu^1g;euKo2HAI@FNg2nF_#6L3dYD-6TnD?$T~W3P8*yFv->lD}A^}L@xojQ_ z$=hlUsYwg0-Ohe4i~MikgFjF2dmZY!o@Q$pdKkmR{`i>>2Ol}!6*A8S>wau^kH z8@^!395d@K)TI@N_K)TrhJ zAvH2KuV0v{YPLtZw-=(g%n1&5{+nCGxABm)Vhdx^lD0D|KSW8qlu?A;KHX)zl8 z$8gV)!YqcJ`npWKIDR&Fh;>yhCO*nHMD)cif_ z7WH%YSG$m7WQ04NZWuYwOQ}L-aZ$>&13u(9!8(>7+K^Q%_SFO{w(LTh7ohgl6!E%o z^UdR9ge^XpHyoDU16^M;Fz)H~0qW8wzu4gHxh4elIhD`I3{%%q}NM3$~$k5N|d-j&BNlFr(ZN0r`m_SaQ|_(}*wX z=jsC*(ufMVWmsffrr)T=a`?6Btxl{c15MT2)CSy1eO1UnbQic+mfWx)Cm-*A)`}K)E{bYEk4i9^T!>>aWmYqwKLCVt){NhwKy;&o0CTspc?Iz2mLJ zOFWi+06^A}BTS%E6q|_wAtvq#{)($U^@6VOr<`pE;@-7C|5j`#vCckP5n#5}56xWG zfy}Te$v?gnd0uSoFP>I0y4rLF;uH@egI7f`lY`ruC+A#6=y`hf z(M0*seJl#IhSd|+=_x?M8aa463~#IRoLmC2aAeCz-^EgGV1BVX5c{b zofyDiaFs0ASROyo3V`np1&mh&;;OOko(Px%&p97LQo#E+z5IxqW&~q__6${ihhQTX)}K z$LVdCRUXGT{rdO8@I;oF0;ihN+mQCRQz6QbO#VQt$AObiCkFG-QTC4>vkEF1Qack>n>M`#=*)ohG1x7o7mM8KQF&sAOl{G z0_g?)Uy5=Vb?V5ZJqI$``OFU-Jw1X!B(j_Eud&hR2anxCceZTeE#?!|VlMJY&MJ#T zB%k1wm?{aM)cHrCG%jv*?ZHDCjK25&ECSe+SY>G_VzxFlZ=`0S^YO_MHzQ146k|L(67` Michael") + echo "Expected:" + echo "$EXPECTED" + sleep 1s # Give the user a chance to look at the result + + # Shutdown + echo "Triggering shutdown" + kill -s TERM $LOCAL_SERVER + kill -s TERM $LOCAL_CLIENT + sleep 1s # Wait for the shutdown logs to pass + + # Verify + if [ "$RESPONSE" = "$EXPECTED" ]; then + echo "#----------------------#" + echo "| Local example works! |" + echo "#----------------------#" + else + echo "#-----------------------#" + echo "| Local example failed! |" + echo "#-----------------------#" + exit 1 + fi +} + +## Cloud-Eureka +cloudTest() { + echo "Starting Cloud $1 test" + + # Run environment + if [[ "$1" = "consul" ]]; then + CONSUL=`docker run --name=consul -d --rm -p 8500:8500 consul` + stopCloudEnv() { + echo "Stopping consul server" + docker stop $CONSUL + } + elif [[ "$1" == "eureka" ]]; then + ./gradlew :example:cloud-eureka-server:bootRun -x jar -x classes --console=plain & + EUREKA=$! + stopCloudEnv() { + echo "Stopping eureka server" + kill -s TERM $EUREKA + } + elif [[ "$1" = "nacos" ]]; then + NACOS=`docker run --env MODE=standalone --name nacos -d --rm -p 8848:8848 nacos/nacos-server` + stopCloudEnv() { + echo "Stopping nacos server" + docker stop $NACOS + } + fi + sleep 10s # Wait for the server to start + +# mkdir -p zipkin +# cd zipkin +# echo "*" > .gitignore +# if [ ! -f zipkin.jar ]; then +# curl -sSL https://zipkin.io/quickstart.sh | bash -s +# fi +# java -jar zipkin.jar & +# ZIPKIN=$! +# sleep 10s # Wait for the server to start +# cd .. + + ./gradlew -Pdiscovery=$1 :example:cloud-grpc-server:bootRun -x jar -x classes --console=plain & + CLOUD_SERVER=$! + sleep 10s # Wait for the server to start + + ./gradlew -Pdiscovery=$1 :example:cloud-grpc-client:bootRun -x jar -x classes --console=plain & + CLOUD_CLIENT=$! + sleep 30s # Wait for the client to start and the server to be ready + sleep 60s # Wait for the discovery service to refresh + + # Test + RESPONSE=$(curl -s localhost:8080/) + echo "Response:" + echo "$RESPONSE" + EXPECTED=$(echo -e "Hello ==> Michael") + echo "Expected:" + echo "$EXPECTED" + sleep 1s # Give the user a chance to look at the result + + # Crash server + kill -s TERM $CLOUD_SERVER + echo "The server crashed (expected)" + sleep 1s # Wait for the shutdown logs to pass + + # and restart server + ./gradlew -Pdiscovery=$1 :example:cloud-grpc-server:bootRun -x jar -x classes --console=plain & + CLOUD_SERVER=$! + sleep 30s # Wait for the server to start + sleep 60s # Wait for the discovery service to refresh + + # Test again + RESPONSE2=$(curl -s localhost:8080/) + echo "Response:" + echo "$RESPONSE2" + EXPECTED=$(echo -e "Hello ==> Michael") + echo "Expected:" + echo "$EXPECTED" + sleep 1s # Give the user a chance to look at the result + + # Shutdown + echo "Triggering shutdown" + stopCloudEnv + # kill -s TERM $ZIPKIN + kill -s TERM $CLOUD_SERVER + kill -s TERM $CLOUD_CLIENT + sleep 1s # Wait for the shutdown logs to pass + + # Verify part 1 + if [ "$RESPONSE" = "$EXPECTED" ]; then + echo "#------------------------------------#" + echo "| Cloud $1 example part 1 works! |" + echo "#------------------------------------#" + else + echo "#-------------------------------------#" + echo "| Cloud $1 example part 1 failed! |" + echo "#-------------------------------------#" + exit 1 + fi + + # Verify part 2 + if [ "$RESPONSE2" = "$EXPECTED" ]; then + echo "#------------------------------------#" + echo "| Cloud $1 example part 2 works! |" + echo "#------------------------------------#" + else + echo "#-------------------------------------#" + echo "| Cloud $1 example part 2 failed! |" + echo "#-------------------------------------#" + exit 1 + fi +} + +## Security Basic Auth +securityBasicAuthTest() { + echo "Starting Security Basic Auth test" + + # Run environment + ./gradlew :example:security-grpc-server:bootRun -x jar -x classes --console=plain & + LOCAL_SERVER=$! + sleep 10s # Wait for the server to start + ./gradlew :example:security-grpc-client:bootRun -x jar -x classes --console=plain & + LOCAL_CLIENT=$! + sleep 30s # Wait for the client to start and the server to be ready + + # Test + RESPONSE=$(curl -s localhost:8080/) + echo "Response:" + echo "$RESPONSE" + EXPECTED=$(echo -e "Input:\n- name: Michael (Changeable via URL param ?name=X)\nRequest-Context:\n- auth user: user (Configure via application.yml)\nResponse:\nHello ==> Michael") + echo "Expected:" + echo "$EXPECTED" + sleep 1s # Give the user a chance to look at the result + + # Shutdown + echo "Triggering shutdown" + kill -s TERM $LOCAL_SERVER + kill -s TERM $LOCAL_CLIENT + sleep 1s # Wait for the shutdown logs to pass + + # Verify + if [ "$RESPONSE" = "$EXPECTED" ]; then + echo "#------------------------------------#" + echo "| Security Basic Auth example works! |" + echo "#------------------------------------#" + else + echo "#-------------------------------------#" + echo "| Security Basic Auth example failed! |" + echo "#-------------------------------------#" + exit 1 + fi +} + +## Tests +build +localTest +cloudTest consul +cloudTest eureka +cloudTest nacos +securityBasicAuthTest diff --git a/tests/build.gradle b/tests/build.gradle new file mode 100644 index 000000000..7da3990d3 --- /dev/null +++ b/tests/build.gradle @@ -0,0 +1,83 @@ +plugins { + id 'eclipse' + id 'com.google.protobuf' +} + +group = 'net.devh' +version = projectVersion + +eclipse { + project { + name = 'grpc-spring-boot-tests' + } + classpath { + file.beforeMerged { cp -> + def generatedGrpcFolder = new org.gradle.plugins.ide.eclipse.model.SourceFolder('src/generated/test/grpc', null); + generatedGrpcFolder.entryAttributes['ignore_optional_problems'] = 'true'; + cp.entries.add( generatedGrpcFolder ); + def generatedJavaFolder = new org.gradle.plugins.ide.eclipse.model.SourceFolder('src/generated/test/java', null); + generatedJavaFolder.entryAttributes['ignore_optional_problems'] = 'true'; + cp.entries.add( generatedJavaFolder ); + } + } +} + +compileTestJava.dependsOn(processTestResources) + +dependencies { + // compile 'io.grpc:grpc-netty' + implementation 'io.grpc:grpc-netty-shaded' + implementation 'io.grpc:grpc-protobuf' + implementation 'io.grpc:grpc-stub' +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${protobufVersion}" + } + generatedFilesBaseDir = "$projectDir/src/generated" + clean { + delete generatedFilesBaseDir + } + plugins { + grpc { + artifact = 'io.grpc:protoc-gen-grpc-java' + } + } + generateProtoTasks { + all()*.plugins { + grpc {} + } + } +} + +idea { + module { + sourceDirs += file('src/generated/test/java') + sourceDirs += file('src/generated/test/grpc') + generatedSourceDirs += file('src/generated/test/java') + generatedSourceDirs += file('src/generated/test/grpc') + } +} + +dependencies { + implementation project(':grpc-common-spring-boot') + implementation project(':grpc-server-spring-boot-starter') // exclude group: 'io.grpc', module: 'grpc-netty-shaded' + implementation project(':grpc-client-spring-boot-starter') // exclude group: 'io.grpc', module: 'grpc-netty-shaded' + if (JavaVersion.current().isJava9Compatible()) { + // Workaround for @javax.annotation.Generated + // see: https://github.com/grpc/grpc-java/issues/3633 + implementation 'jakarta.annotation:jakarta.annotation-api' + } + testImplementation 'io.grpc:grpc-testing' + testImplementation('org.springframework.boot:spring-boot-starter-test') { + exclude module: 'junit-vintage-engine' + exclude module: 'junit' + } + + testImplementation 'org.springframework.boot:spring-boot-starter-actuator' + testImplementation 'org.springframework.security:spring-security-config' + testImplementation 'org.junit.jupiter:junit-jupiter-api' + + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/advice/AbstractSimpleServerClientTest.java b/tests/src/test/java/net/devh/boot/grpc/test/advice/AbstractSimpleServerClientTest.java new file mode 100644 index 000000000..e1bd1ca9d --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/advice/AbstractSimpleServerClientTest.java @@ -0,0 +1,136 @@ +/* + * 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.advice; + +import static net.devh.boot.grpc.test.util.FutureAssertions.assertFutureThrows; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.concurrent.TimeUnit; + +import javax.annotation.PostConstruct; + +import com.google.protobuf.Empty; + +import io.grpc.Channel; +import io.grpc.Metadata; +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.internal.testing.StreamRecorder; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceBlockingStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceFutureStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceStub; + +/** + * A test checking that the server and client can start and connect to each other with proper exception handling. + * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + */ +@Slf4j +abstract class AbstractSimpleServerClientTest { + + @GrpcClient("test") + protected Channel channel; + @GrpcClient("test") + protected TestServiceStub testServiceStub; + @GrpcClient("test") + protected TestServiceBlockingStub testServiceBlockingStub; + @GrpcClient("test") + protected TestServiceFutureStub testServiceFutureStub; + + @PostConstruct + protected void init() { + // Test injection + assertNotNull(this.channel, "channel"); + assertNotNull(this.testServiceBlockingStub, "testServiceBlockingStub"); + assertNotNull(this.testServiceFutureStub, "testServiceFutureStub"); + assertNotNull(this.testServiceStub, "testServiceStub"); + } + + /** + * Test template call to check for every exception. + */ + void testGrpcCallAndVerifyMappedException(Status expectedStatus, Metadata metadata) { + + verifyManualBlockingStubCall(expectedStatus, metadata); + verifyBlockingStubCall(expectedStatus, metadata); + verifyManualFutureStubCall(expectedStatus, metadata); + verifyFutureStubCall(expectedStatus, metadata); + } + + private void verifyManualBlockingStubCall( + Status expectedStatus, Metadata expectedMetadata) { + + StatusRuntimeException actualException = + assertThrows(StatusRuntimeException.class, + () -> TestServiceGrpc.newBlockingStub(this.channel).normal(Empty.getDefaultInstance())); + + verifyStatusAndMetadata(actualException, expectedStatus, expectedMetadata); + } + + private void verifyBlockingStubCall(Status expectedStatus, Metadata expectedMetadata) { + + StatusRuntimeException actualException = + assertThrows(StatusRuntimeException.class, + () -> this.testServiceBlockingStub.normal(Empty.getDefaultInstance())); + + verifyStatusAndMetadata(actualException, expectedStatus, expectedMetadata); + } + + + private void verifyManualFutureStubCall( + Status expectedStatus, Metadata expectedMetadata) { + + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.testServiceStub.normal(Empty.getDefaultInstance(), streamRecorder); + StatusRuntimeException actualException = + assertFutureThrows(StatusRuntimeException.class, streamRecorder.firstValue(), 5, TimeUnit.SECONDS); + + verifyStatusAndMetadata(actualException, expectedStatus, expectedMetadata); + } + + + private void verifyFutureStubCall( + Status expectedStatus, Metadata expectedMetadata) { + + StatusRuntimeException actualException = + assertFutureThrows(StatusRuntimeException.class, + this.testServiceFutureStub.normal(Empty.getDefaultInstance()), + 5, + TimeUnit.SECONDS); + + verifyStatusAndMetadata(actualException, expectedStatus, expectedMetadata); + } + + private void verifyStatusAndMetadata( + StatusRuntimeException actualException, Status expectedStatus, Metadata expectedMetadata) { + + assertThat(actualException.getTrailers()) + .usingRecursiveComparison() + .isEqualTo(expectedMetadata); + assertThat(actualException.getStatus()) + .usingRecursiveComparison() + .isEqualTo(expectedStatus); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/advice/AdviceExceptionHandlingTest.java b/tests/src/test/java/net/devh/boot/grpc/test/advice/AdviceExceptionHandlingTest.java new file mode 100644 index 000000000..335dcefe3 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/advice/AdviceExceptionHandlingTest.java @@ -0,0 +1,211 @@ +/* + * 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.advice; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.security.AccessControlException; + +import org.assertj.core.groups.Tuple; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.authentication.AccountExpiredException; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; +import io.grpc.Metadata; +import io.grpc.Status; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.advice.GrpcAdviceExceptionHandler; +import net.devh.boot.grpc.server.advice.GrpcAdviceExceptionListener; +import net.devh.boot.grpc.server.autoconfigure.GrpcAdviceAutoConfiguration; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.GrpcAdviceConfig; +import net.devh.boot.grpc.test.config.GrpcAdviceConfig.TestAdviceWithMetadata.FirstLevelException; +import net.devh.boot.grpc.test.config.GrpcAdviceConfig.TestAdviceWithMetadata.MyRootRuntimeException; +import net.devh.boot.grpc.test.config.GrpcAdviceConfig.TestAdviceWithMetadata.SecondLevelException; +import net.devh.boot.grpc.test.config.GrpcAdviceConfig.TestAdviceWithMetadata.StatusMappingException; +import net.devh.boot.grpc.test.config.GrpcAdviceConfig.TestGrpcAdviceService; +import net.devh.boot.grpc.test.config.InProcessConfiguration; +import net.devh.boot.grpc.test.util.LoggerTestUtil; + +/** + * A test checking that the server and client can start and connect to each other with minimal config and a exception + * advice is applied. + * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + */ +@Slf4j +@SpringBootTest +@SpringJUnitConfig(classes = { + InProcessConfiguration.class, + GrpcAdviceConfig.class, + BaseAutoConfiguration.class +}) +@ImportAutoConfiguration(GrpcAdviceAutoConfiguration.class) +@DirtiesContext +class AdviceExceptionHandlingTest extends AbstractSimpleServerClientTest { + + private ListAppender loggingEventListAppender; + + @Autowired + private TestGrpcAdviceService testGrpcAdviceService; + + @BeforeEach + void setup() { + loggingEventListAppender = LoggerTestUtil.getListAppenderForClasses( + GrpcAdviceExceptionListener.class, + GrpcAdviceExceptionHandler.class); + } + + @Test + @DirtiesContext + void testThrownIllegalArgumentException_IsMappedAsStatus() { + + IllegalArgumentException exceptionToMap = new IllegalArgumentException("Trigger Advice"); + testGrpcAdviceService.setExceptionToSimulate(exceptionToMap); + Status expectedStatus = Status.INVALID_ARGUMENT.withDescription(exceptionToMap.getMessage()); + Metadata metadata = new Metadata(); + + testGrpcCallAndVerifyMappedException(expectedStatus, metadata); + } + + @Test + @DirtiesContext + void testThrownAccessControlException_IsMappedAsThrowable() { + + AccessControlException exceptionToMap = new AccessControlException("Trigger Advice"); + testGrpcAdviceService.setExceptionToSimulate(exceptionToMap); + Status expectedStatus = Status.UNKNOWN; + Metadata metadata = new Metadata(); + + testGrpcCallAndVerifyMappedException(expectedStatus, metadata); + } + + @Test + @DirtiesContext + void testThrownClassCastException_IsMappedAsStatusRuntimeExceptionAndWithMetadata() { + + ClassCastException exceptionToMap = new ClassCastException("Casting with classes failed."); + testGrpcAdviceService.setExceptionToSimulate(exceptionToMap); + Status expectedStatus = Status.FAILED_PRECONDITION.withDescription(exceptionToMap.getMessage()); + Metadata metadata = GrpcMetaDataUtils.createExpectedAsciiHeader(); + + testGrpcCallAndVerifyMappedException(expectedStatus, metadata); + } + + @Test + @DirtiesContext + void testThrownAccountExpiredException_IsNotMappedAndResultsInInvocationException() { + + AccountExpiredException exceptionToMap = + new AccountExpiredException("Trigger Advice"); // not mapped in GrpcAdviceConfig + testGrpcAdviceService.setExceptionToSimulate(exceptionToMap); + Status expectedStatus = + Status.INTERNAL.withDescription("There was a server error trying to handle an exception"); + Metadata metadata = new Metadata(); + + testGrpcCallAndVerifyMappedException(expectedStatus, metadata); + assertThat(loggingEventListAppender.list) + .extracting(ILoggingEvent::getMessage, ILoggingEvent::getLevel) + .contains(Tuple.tuple("Exception caught during gRPC execution: ", Level.DEBUG)) + .contains(Tuple.tuple( + "Exception thrown during invocation of annotated @GrpcExceptionHandler method: ", + Level.ERROR)); + } + + @Test + @DirtiesContext + void testThrownFirstLevelException_IsMappedAsStatusExceptionWithMetadata() { + + FirstLevelException exceptionToMap = new FirstLevelException("Trigger Advice"); + testGrpcAdviceService.setExceptionToSimulate(exceptionToMap); + Status expectedStatus = Status.NOT_FOUND.withDescription(exceptionToMap.getMessage()); + Metadata metadata = GrpcMetaDataUtils.createExpectedAsciiHeader(); + + testGrpcCallAndVerifyMappedException(expectedStatus, metadata); + } + + @Test + @DirtiesContext + void testThrownStatusMappingException_IsResolvedAsInternalServerError() { + + StatusMappingException exceptionToMap = new StatusMappingException("Trigger Advice"); + testGrpcAdviceService.setExceptionToSimulate(exceptionToMap); + Status expectedStatus = + Status.INTERNAL.withDescription("There was a server error trying to handle an exception"); + Metadata metadata = new Metadata(); + + testGrpcCallAndVerifyMappedException(expectedStatus, metadata); + + assertThat(loggingEventListAppender.list) + .extracting(ILoggingEvent::getMessage, ILoggingEvent::getLevel) + .contains(Tuple.tuple("Exception caught during gRPC execution: ", Level.DEBUG)) + .contains(Tuple.tuple( + "Exception thrown during invocation of annotated @GrpcExceptionHandler method: ", + Level.ERROR)); + } + + @Test + @DirtiesContext + void testThrownRootDepth_IsMappedCorrectlyWithRootException() { + + MyRootRuntimeException rootRuntimeException = new MyRootRuntimeException("root exception triggered."); + + testGrpcAdviceService.setExceptionToSimulate(rootRuntimeException); + Status expectedStatus = Status.DEADLINE_EXCEEDED.withDescription(rootRuntimeException.getMessage()); + Metadata metadata = new Metadata(); + + testGrpcCallAndVerifyMappedException(expectedStatus, metadata); + } + + @Test + @DirtiesContext + void testThrownSecondLevenDepth_IsMappedCorrectlyWithSecondLevelException() { + + SecondLevelException secondLevelException = + new SecondLevelException("level under first level and second level under root triggered."); + + testGrpcAdviceService.setExceptionToSimulate(secondLevelException); + Status expectedStatus = Status.ABORTED.withDescription(secondLevelException.getMessage()); + Metadata metadata = new Metadata(); + + testGrpcCallAndVerifyMappedException(expectedStatus, metadata); + } + + + + @BeforeAll + public static void beforeAll() { + log.info("--- Starting tests with successful advice exception handling ---"); + } + + @AfterAll + public static void afterAll() { + log.info("--- Ending tests with successful advice exception handling ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/advice/AdviceIsPresentAutoConfigurationTest.java b/tests/src/test/java/net/devh/boot/grpc/test/advice/AdviceIsPresentAutoConfigurationTest.java new file mode 100644 index 000000000..55a2fb94f --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/advice/AdviceIsPresentAutoConfigurationTest.java @@ -0,0 +1,130 @@ +/* + * 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.advice; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.advice.GrpcAdviceDiscoverer; +import net.devh.boot.grpc.server.advice.GrpcExceptionHandler; +import net.devh.boot.grpc.server.autoconfigure.GrpcAdviceAutoConfiguration; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.GrpcAdviceConfig; +import net.devh.boot.grpc.test.config.GrpcAdviceConfig.TestAdviceForInheritedExceptions; +import net.devh.boot.grpc.test.config.GrpcAdviceConfig.TestAdviceWithMetadata; +import net.devh.boot.grpc.test.config.GrpcAdviceConfig.TestAdviceWithOutMetadata; + +/** + * A test to verify that the grpc exception advice auto configuration works. + * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + */ +@Slf4j +@SpringBootTest +@SpringJUnitConfig(classes = {GrpcAdviceConfig.class, BaseAutoConfiguration.class}) +@ImportAutoConfiguration(GrpcAdviceAutoConfiguration.class) +@DirtiesContext +class AdviceIsPresentAutoConfigurationTest { + + private static final int ADVICE_CLASSES = 3; + private static final int ADVICE_METHODS = 7; + + + @Autowired + private GrpcAdviceDiscoverer grpcAdviceDiscoverer; + + @Autowired + private TestAdviceWithOutMetadata testAdviceWithOutMetadata; + @Autowired + private TestAdviceWithMetadata testAdviceWithMetadata; + @Autowired + private TestAdviceForInheritedExceptions testAdviceForInheritedExceptions; + + + @Test + @DirtiesContext + void testAdviceIsPresentWithExceptionMapping() { + log.info("--- Starting tests with advice auto discovery ---"); + + Map expectedAdviceBeans = new HashMap<>(); + expectedAdviceBeans.put("grpcAdviceWithBean", testAdviceWithOutMetadata); + expectedAdviceBeans.put(TestAdviceWithMetadata.class.getName(), testAdviceWithMetadata); + expectedAdviceBeans.put(TestAdviceForInheritedExceptions.class.getName(), testAdviceForInheritedExceptions); + Set expectedAdviceMethods = expectedMethods(); + + Map actualAdviceBeans = grpcAdviceDiscoverer.getAnnotatedBeans(); + Set actualAdviceMethods = grpcAdviceDiscoverer.getAnnotatedMethods(); + + assertThat(actualAdviceBeans) + .hasSize(ADVICE_CLASSES) + .containsExactlyInAnyOrderEntriesOf(expectedAdviceBeans); + assertThat(actualAdviceMethods) + .hasSize(ADVICE_METHODS) + .containsExactlyInAnyOrderElementsOf(expectedAdviceMethods); + } + + // ################### + // ### H E L P E R ### + // ################### + + private Set expectedMethods() { + new HashSet<>(); + Set methodsWithMetadata = + Arrays.stream(testAdviceWithMetadata.getClass().getDeclaredMethods()).collect(Collectors.toSet()); + Set methodsWithOutMetadata = + Arrays.stream(testAdviceWithOutMetadata.getClass().getDeclaredMethods()).collect(Collectors.toSet()); + Set methodsForInheritedExceptions = + Arrays.stream(testAdviceForInheritedExceptions.getClass().getDeclaredMethods()) + .collect(Collectors.toSet()); + + return Stream.of(methodsWithMetadata, methodsWithOutMetadata, methodsForInheritedExceptions) + .flatMap(Collection::stream) + .filter(method -> method.isAnnotationPresent(GrpcExceptionHandler.class)) + .collect(Collectors.toSet()); + } + + @BeforeAll + public static void beforeAll() { + log.info("--- Starting tests with successful advice pickup ---"); + } + + @AfterAll + public static void afterAll() { + log.info("--- Ending tests with successful advice pickup ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/advice/AdviceNotPresentAutoConfigurationTest.java b/tests/src/test/java/net/devh/boot/grpc/test/advice/AdviceNotPresentAutoConfigurationTest.java new file mode 100644 index 000000000..3137b299d --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/advice/AdviceNotPresentAutoConfigurationTest.java @@ -0,0 +1,87 @@ +/* + * 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.advice; + +import java.util.Map; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.advice.GrpcAdvice; +import net.devh.boot.grpc.server.advice.GrpcExceptionHandler; +import net.devh.boot.grpc.server.autoconfigure.GrpcAdviceAutoConfiguration; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; + +/** + * A test to verify that the grpc exception advice is not automatically picked up, if no {@link GrpcAdvice @GrpcAdvice} + * is present. + * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + */ +@Slf4j +@SpringBootTest +@SpringJUnitConfig(classes = BaseAutoConfiguration.class) +@ImportAutoConfiguration(GrpcAdviceAutoConfiguration.class) +@DirtiesContext +class AdviceNotPresentAutoConfigurationTest { + + @Autowired + private ApplicationContext applicationContext; + + @Test + @DirtiesContext + void testGrpcAdviceNotPresent() { + log.info("--- Starting tests with no present advice - auto discovery ---"); + + Map actualAdviceBeans = applicationContext.getBeansWithAnnotation(GrpcAdvice.class); + + Assertions.assertThat(actualAdviceBeans).isEmpty(); + } + + @Test + @DirtiesContext + void testGrpcExceptionHandlerNotPresent() { + log.info("--- Starting tests with no present advice - auto discovery ---"); + + Map actualExceptionHandler = + applicationContext.getBeansWithAnnotation(GrpcExceptionHandler.class); + + Assertions.assertThat(actualExceptionHandler).isEmpty(); + } + + + @BeforeAll + public static void beforeAll() { + log.info("--- Starting tests for no present advice ---"); + } + + @AfterAll + public static void afterAll() { + log.info("--- Ending tests with for no present advice ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/advice/GrpcMetaDataUtils.java b/tests/src/test/java/net/devh/boot/grpc/test/advice/GrpcMetaDataUtils.java new file mode 100644 index 000000000..228a319b1 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/advice/GrpcMetaDataUtils.java @@ -0,0 +1,41 @@ +/* + * 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.advice; + +import io.grpc.Metadata; + +public class GrpcMetaDataUtils { + + private GrpcMetaDataUtils() { + throw new UnsupportedOperationException("Util class not to be instantiated."); + } + + public static Metadata createExpectedAsciiHeader() { + + return createAsciiHeader("HEADER_KEY", "HEADER_VALUE"); + } + + + public static Metadata createAsciiHeader(String key, String value) { + + Metadata metadata = new Metadata(); + Metadata.Key asciiKey = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER); + metadata.put(asciiKey, value); + return metadata; + } +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/codec/AbstractCodecTest.java b/tests/src/test/java/net/devh/boot/grpc/test/codec/AbstractCodecTest.java new file mode 100644 index 000000000..bc7918218 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/codec/AbstractCodecTest.java @@ -0,0 +1,118 @@ +/* + * 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.codec; + +import static org.assertj.core.api.Assumptions.assumeThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Objects; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import com.google.protobuf.Empty; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.Metadata; +import io.grpc.Metadata.Key; +import io.grpc.MethodDescriptor; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.internal.GrpcUtil; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorRegistry; +import net.devh.boot.grpc.server.interceptor.GlobalServerInterceptorRegistry; +import net.devh.boot.grpc.test.proto.TestServiceGrpc; + +/** + * Tests related to codecs. + */ +abstract class AbstractCodecTest { + + private final String codec; + + @GrpcClient("test") + protected Channel channel; + + @Autowired + private GlobalClientInterceptorRegistry clientRegistry; + + @Autowired + private GlobalServerInterceptorRegistry serverRegistry; + + @Autowired + private CodecValidatingClientInterceptor clientValidator; + + @Autowired + private CodecValidatingServerInterceptor serverValidator; + + AbstractCodecTest(final String codec) { + this.codec = codec; + } + + @Test + void testTransmission() { + assumeThat(this.clientRegistry.getClientInterceptors()).contains(this.clientValidator); + assumeThat(this.serverRegistry.getServerInterceptors()).contains(this.serverValidator); + + assertEquals("1.2.3", TestServiceGrpc.newBlockingStub(this.channel).withCompression(this.codec) + .normal(Empty.getDefaultInstance()).getVersion()); + } + + protected static final class CodecValidatingClientInterceptor implements ClientInterceptor { + + private final String compressor; + + public CodecValidatingClientInterceptor(final String compressor) { + this.compressor = compressor; + } + + @Override + public ClientCall interceptCall(final MethodDescriptor method, + final CallOptions callOptions, final Channel next) { + assertEquals(this.compressor, callOptions.getCompressor()); + return next.newCall(method, callOptions); + } + + } + + protected static final class CodecValidatingServerInterceptor implements ServerInterceptor { + + private static final Key ENCODING = Key.of(GrpcUtil.MESSAGE_ENCODING, Metadata.ASCII_STRING_MARSHALLER); + + private final String compressor; + + public CodecValidatingServerInterceptor(final String compressor) { + this.compressor = compressor; + } + + @Override + public Listener interceptCall(final ServerCall call, final Metadata headers, + final ServerCallHandler next) { + assertEquals(this.compressor, Objects.toString(headers.get(ENCODING), "identity")); + return next.startCall(call, headers); + } + + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/codec/BeanCodecTest.java b/tests/src/test/java/net/devh/boot/grpc/test/codec/BeanCodecTest.java new file mode 100644 index 000000000..96e1e3fb5 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/codec/BeanCodecTest.java @@ -0,0 +1,96 @@ +/* + * 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.codec; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import org.springframework.boot.test.context.SpringBootTest; +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.Codec; +import net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor; +import net.devh.boot.grpc.common.codec.GrpcCodec; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; + +@SpringBootTest(properties = { + "grpc.client.GLOBAL.address=localhost:9090", + "grpc.client.GLOBAL.negotiationType=PLAINTEXT" +}) +@SpringJUnitConfig(classes = { + BeanCodecTest.CustomConfiguration.class, + ServiceConfiguration.class, + BaseAutoConfiguration.class}) +@DirtiesContext +public class BeanCodecTest extends AbstractCodecTest { + + private static final String CODEC = "bean"; + + public BeanCodecTest() { + super(CODEC); + } + + @Configuration + public static class CustomConfiguration { + + @GrpcGlobalClientInterceptor + CodecValidatingClientInterceptor gcic() { + return new CodecValidatingClientInterceptor(CODEC); + } + + @GrpcGlobalServerInterceptor + CodecValidatingServerInterceptor gsic() { + return new CodecValidatingServerInterceptor(CODEC); + } + + @Bean + CustomCodec customCodec() { + return new CustomCodec(); + } + + } + + @GrpcCodec + public static final class CustomCodec implements Codec { + + @Override + public String getMessageEncoding() { + return CODEC; + } + + @Override + public OutputStream compress(final OutputStream os) throws IOException { + return new GZIPOutputStream(os); + } + + @Override + public InputStream decompress(final InputStream is) throws IOException { + return new GZIPInputStream(is); + } + + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/codec/CustomCodecTest.java b/tests/src/test/java/net/devh/boot/grpc/test/codec/CustomCodecTest.java new file mode 100644 index 000000000..5962b6ed3 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/codec/CustomCodecTest.java @@ -0,0 +1,98 @@ +/* + * 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.codec; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import org.springframework.boot.test.context.SpringBootTest; +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.Codec; +import net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor; +import net.devh.boot.grpc.common.codec.CodecType; +import net.devh.boot.grpc.common.codec.GrpcCodecDefinition; +import net.devh.boot.grpc.common.codec.GrpcCodecDiscoverer; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; + +@SpringBootTest(properties = { + "grpc.client.GLOBAL.address=localhost:9090", + "grpc.client.GLOBAL.negotiationType=PLAINTEXT" +}) +@SpringJUnitConfig(classes = { + CustomCodecTest.CustomConfiguration.class, + ServiceConfiguration.class, + BaseAutoConfiguration.class}) +@DirtiesContext +public class CustomCodecTest extends AbstractCodecTest { + + private static final String CODEC = "custom"; + + public CustomCodecTest() { + super(CODEC); + } + + @Configuration + public static class CustomConfiguration { + + @GrpcGlobalClientInterceptor + CodecValidatingClientInterceptor gcic() { + return new CodecValidatingClientInterceptor(CODEC); + } + + @GrpcGlobalServerInterceptor + CodecValidatingServerInterceptor gsic() { + return new CodecValidatingServerInterceptor(CODEC); + } + + @Bean + GrpcCodecDiscoverer grpcCodecDiscoverer() { + return () -> Arrays.asList(new GrpcCodecDefinition(new CustomCodecTest.CustomCodec(), true, CodecType.ALL)); + } + + } + + public static final class CustomCodec implements Codec { + + @Override + public String getMessageEncoding() { + return CODEC; + } + + @Override + public OutputStream compress(final OutputStream os) throws IOException { + return new GZIPOutputStream(os); + } + + @Override + public InputStream decompress(final InputStream is) throws IOException { + return new GZIPInputStream(is); + } + + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/codec/GzipCodecTest.java b/tests/src/test/java/net/devh/boot/grpc/test/codec/GzipCodecTest.java new file mode 100644 index 000000000..706019e0d --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/codec/GzipCodecTest.java @@ -0,0 +1,78 @@ +/* + * 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.codec; + +import java.util.Arrays; + +import org.springframework.boot.test.context.SpringBootTest; +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 net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor; +import net.devh.boot.grpc.common.codec.GrpcCodecDefinition; +import net.devh.boot.grpc.common.codec.GrpcCodecDiscoverer; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; + +/** + * A test checking that the server and client can start and connect to each other with minimal config and no/the + * identity codec. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@SpringBootTest(properties = { + "grpc.client.GLOBAL.address=localhost:9090", + "grpc.client.GLOBAL.negotiationType=PLAINTEXT" +}) +@SpringJUnitConfig(classes = { + GzipCodecTest.CustomConfiguration.class, + ServiceConfiguration.class, + BaseAutoConfiguration.class}) +@DirtiesContext +public class GzipCodecTest extends AbstractCodecTest { + + private static final String CODEC = "gzip"; + + public GzipCodecTest() { + super(CODEC); + } + + @Configuration + public static class CustomConfiguration { + + @GrpcGlobalClientInterceptor + CodecValidatingClientInterceptor gcic() { + return new CodecValidatingClientInterceptor(CODEC); + } + + @GrpcGlobalServerInterceptor + CodecValidatingServerInterceptor gsic() { + return new CodecValidatingServerInterceptor(CODEC); + } + + @Bean + GrpcCodecDiscoverer grpcCodecDiscoverer() { + return () -> Arrays.asList(GrpcCodecDefinition.GZIP_DEFINITION); + } + + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/codec/IdentityCodecTest.java b/tests/src/test/java/net/devh/boot/grpc/test/codec/IdentityCodecTest.java new file mode 100644 index 000000000..adabe3174 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/codec/IdentityCodecTest.java @@ -0,0 +1,78 @@ +/* + * 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.codec; + +import java.util.Arrays; + +import org.springframework.boot.test.context.SpringBootTest; +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 net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor; +import net.devh.boot.grpc.common.codec.GrpcCodecDefinition; +import net.devh.boot.grpc.common.codec.GrpcCodecDiscoverer; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; + +/** + * A test checking that the server and client can start and connect to each other with minimal config and no/the + * identity codec. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@SpringBootTest(properties = { + "grpc.client.GLOBAL.address=localhost:9090", + "grpc.client.GLOBAL.negotiationType=PLAINTEXT" +}) +@SpringJUnitConfig(classes = { + IdentityCodecTest.CustomConfiguration.class, + ServiceConfiguration.class, + BaseAutoConfiguration.class}) +@DirtiesContext +public class IdentityCodecTest extends AbstractCodecTest { + + private static final String CODEC = "identity"; + + public IdentityCodecTest() { + super(CODEC); + } + + @Configuration + public static class CustomConfiguration { + + @GrpcGlobalClientInterceptor + CodecValidatingClientInterceptor gcic() { + return new CodecValidatingClientInterceptor(CODEC); + } + + @GrpcGlobalServerInterceptor + CodecValidatingServerInterceptor gsic() { + return new CodecValidatingServerInterceptor(CODEC); + } + + @Bean + GrpcCodecDiscoverer grpcCodecDiscoverer() { + return () -> Arrays.asList(GrpcCodecDefinition.IDENTITY_DEFINITION); + } + + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/config/AnnotatedSecurityConfiguration.java b/tests/src/test/java/net/devh/boot/grpc/test/config/AnnotatedSecurityConfiguration.java new file mode 100644 index 000000000..cd14842b4 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/config/AnnotatedSecurityConfiguration.java @@ -0,0 +1,32 @@ +/* + * 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.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; + +@Configuration +// proxyTargetClass is required, if you use annotation driven security! +// However, you will receive a warning that TestServiceImpl#bindService() method is final. +// You cannot avoid that warning (without massive amount of work), but it is safe to ignore it. +// The #bindService() method uses a reference to 'this', which will be used to invoke the methods. +// If the method is not final it will delegate to the original instance and thus it will bypass any security layer that +// you intend to add, unless you re-implement the #bindService() method on the outermost layer (which Spring does not). +@EnableGlobalMethodSecurity(securedEnabled = true, proxyTargetClass = true) +public class AnnotatedSecurityConfiguration { +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/config/AwaitableServerClientCallConfiguration.java b/tests/src/test/java/net/devh/boot/grpc/test/config/AwaitableServerClientCallConfiguration.java new file mode 100644 index 000000000..12ef054f9 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/config/AwaitableServerClientCallConfiguration.java @@ -0,0 +1,184 @@ +/* + * 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.config; + +import static net.devh.boot.grpc.common.util.InterceptorOrder.ORDER_FIRST; +import static net.devh.boot.grpc.common.util.InterceptorOrder.ORDER_LAST; + +import java.util.concurrent.CountDownLatch; + +import org.springframework.context.annotation.Configuration; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.ForwardingClientCall.SimpleForwardingClientCall; +import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener; +import io.grpc.ForwardingServerCallListener.SimpleForwardingServerCallListener; +import io.grpc.Metadata; +import io.grpc.MethodDescriptor; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.Status; +import net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor; +import net.devh.boot.grpc.client.interceptor.OrderedClientInterceptor; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; +import net.devh.boot.grpc.server.interceptor.OrderedServerInterceptor; + +/** + * Helper configuration that can be used to await the completion/closing of the next calls. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Configuration +public class AwaitableServerClientCallConfiguration { + + private static CountDownLatch serverCounter; + private static CountDownLatch clientCounter; + + /** + * A testing server interceptor, that allows awaiting the completion of the server call that is otherwise closed + * asynchronously. + * + * @return A testing server interceptor bean. + */ + @GrpcGlobalServerInterceptor + ServerInterceptor awaitableServerInterceptor() { + return new OrderedServerInterceptor(new ServerInterceptor() { + + @Override + public Listener interceptCall( + final ServerCall call, + final Metadata headers, + final ServerCallHandler next) { + + if (serverCounter == null || serverCounter.getCount() == 0) { + return next.startCall(call, headers); + } else { + final CountDownLatch thatCounter = serverCounter; + return new SimpleForwardingServerCallListener(next.startCall(call, headers)) { + + @Override + public void onComplete() { + super.onComplete(); + thatCounter.countDown(); + } + + @Override + public void onCancel() { + super.onCancel(); + thatCounter.countDown(); + } + + }; + } + } + + }, ORDER_FIRST); + } + + /** + * A testing client interceptor, that allows awaiting the completion of the client call that is otherwise closed + * asynchronously. + * + * @return A testing client interceptor bean. + */ + @GrpcGlobalClientInterceptor + ClientInterceptor awaitableClientInterceptor() { + return new OrderedClientInterceptor(new ClientInterceptor() { + + @Override + public ClientCall interceptCall( + final MethodDescriptor method, + final CallOptions callOptions, + final Channel next) { + + if (clientCounter == null || clientCounter.getCount() == 0) { + return next.newCall(method, callOptions); + } else { + final CountDownLatch thatCounter = clientCounter; + return new SimpleForwardingClientCall(next.newCall(method, callOptions)) { + + @Override + public void start(final Listener responseListener, final Metadata headers) { + super.start(new SimpleForwardingClientCallListener(responseListener) { + + @Override + public void onClose(final Status status, final Metadata trailers) { + super.onClose(status, trailers); + thatCounter.countDown(); + } + + }, headers); + } + + }; + } + } + + }, ORDER_LAST); + } + + /** + * Returns a {@link CountDownLatch} that will be used in the next server calls and can be used to await the + * {@link ServerCall#close(Status, Metadata) ServerCall close}. This method must be called before the call is + * started. + * + * @param count The number of call closes to await. + * @return The counter used to await the server call close. + */ + public static CountDownLatch awaitNextServerCallCloses(final int count) { + final CountDownLatch newCounter = new CountDownLatch(count); + serverCounter = newCounter; + return newCounter; + } + + /** + * Returns a {@link CountDownLatch} that will be used in the next client calls and can be used to await the + * {@link io.grpc.ClientCall.Listener#onClose(Status, Metadata) ClientCall close}. This method must be called before + * the call is started. + * + * @param count The number of call closes to await. + * @return The counter used to await the client call close. + */ + public static CountDownLatch awaitNextClientCallCloses(final int count) { + final CountDownLatch newCounter = new CountDownLatch(count); + clientCounter = newCounter; + return newCounter; + } + + /** + * Returns a {@link CountDownLatch} that will be used in the next server and client calls and can be used to await + * the respective closes. This method must be called before the call is started. + * + * @param count The number of call closes to await. + * @return The counter used to await the client call close. + * @see #awaitNextClientCallCloses(int) + * @see #awaitNextServerCallCloses(int) + */ + public static CountDownLatch awaitNextServerAndClientCallCloses(final int count) { + final CountDownLatch newCounter = new CountDownLatch(2 * count); + serverCounter = newCounter; + clientCounter = newCounter; + return newCounter; + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/config/BaseAutoConfiguration.java b/tests/src/test/java/net/devh/boot/grpc/test/config/BaseAutoConfiguration.java new file mode 100644 index 000000000..25e83014a --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/config/BaseAutoConfiguration.java @@ -0,0 +1,35 @@ +/* + * 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.config; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.context.annotation.Configuration; + +import net.devh.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration; +import net.devh.boot.grpc.common.autoconfigure.GrpcCommonCodecAutoConfiguration; +import net.devh.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration; +import net.devh.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration; +import net.devh.boot.grpc.server.autoconfigure.GrpcServerSecurityAutoConfiguration; + +@Configuration +@ImportAutoConfiguration({GrpcCommonCodecAutoConfiguration.class, GrpcServerAutoConfiguration.class, + GrpcServerFactoryAutoConfiguration.class, GrpcServerSecurityAutoConfiguration.class, + GrpcClientAutoConfiguration.class}) +public class BaseAutoConfiguration { + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/config/BeanAnnotatedServiceConfig.java b/tests/src/test/java/net/devh/boot/grpc/test/config/BeanAnnotatedServiceConfig.java new file mode 100644 index 000000000..e61374382 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/config/BeanAnnotatedServiceConfig.java @@ -0,0 +1,55 @@ +/* + * 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.config; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.google.protobuf.Empty; + +import io.grpc.stub.StreamObserver; +import net.devh.boot.grpc.server.service.GrpcService; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceImplBase; + +@Configuration +public class BeanAnnotatedServiceConfig { + + @Bean + AtomicBoolean invocationCheck() { + return new AtomicBoolean(false); + } + + @GrpcService + TestServiceImplBase testServiceImplBase(final AtomicBoolean invocationCheck) { + return new TestServiceImplBase() { + + @Override + public void normal(final Empty request, final StreamObserver responseObserver) { + invocationCheck.set(true); + final SomeType version = SomeType.newBuilder().setVersion("1.2.3").build(); + responseObserver.onNext(version); + responseObserver.onCompleted(); + } + + }; + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/config/GrpcAdviceConfig.java b/tests/src/test/java/net/devh/boot/grpc/test/config/GrpcAdviceConfig.java new file mode 100644 index 000000000..3d95b36f1 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/config/GrpcAdviceConfig.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2016-2020 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.config; + +import java.security.AccessControlException; + +import org.assertj.core.api.Assertions; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.ConversionFailedException; +import org.springframework.security.authentication.AccountExpiredException; + +import com.google.protobuf.Empty; + +import io.grpc.Metadata; +import io.grpc.Status; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; +import io.grpc.stub.StreamObserver; +import net.devh.boot.grpc.server.advice.GrpcAdvice; +import net.devh.boot.grpc.server.advice.GrpcExceptionHandler; +import net.devh.boot.grpc.server.service.GrpcService; +import net.devh.boot.grpc.test.advice.GrpcMetaDataUtils; +import net.devh.boot.grpc.test.config.GrpcAdviceConfig.TestAdviceWithMetadata.MyRootRuntimeException; +import net.devh.boot.grpc.test.config.GrpcAdviceConfig.TestAdviceWithMetadata.SecondLevelException; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc; + +@Configuration +public class GrpcAdviceConfig { + + @GrpcService + public static class TestGrpcAdviceService extends TestServiceGrpc.TestServiceImplBase { + + private RuntimeException throwableToSimulate; + + @Override + public void normal(final Empty request, final StreamObserver responseObserver) { + + Assertions.assertThat(throwableToSimulate).isNotNull(); + throw throwableToSimulate; + } + + public void setExceptionToSimulate(E exception) { + throwableToSimulate = exception; + } + } + + @GrpcAdvice + @Bean + public TestAdviceWithOutMetadata grpcAdviceWithBean() { + return new TestAdviceWithOutMetadata(); + } + + public static class TestAdviceWithOutMetadata { + + @GrpcExceptionHandler + public Status handleIllegalArgumentException(IllegalArgumentException e) { + return Status.INVALID_ARGUMENT.withCause(e).withDescription(e.getMessage()); + } + + @GrpcExceptionHandler({ConversionFailedException.class, AccessControlException.class}) + public Throwable handleConversionFailedExceptionAndAccessControlException( + ConversionFailedException e1, + AccessControlException e2) { + return (e1 != null) ? e1 : ((e2 != null) ? e2 : new RuntimeException("Should not happen.")); + } + + public Status methodNotToBePickup(AccountExpiredException e) { + Assertions.fail("Not supposed to be picked up."); + return Status.FAILED_PRECONDITION; + } + } + + @GrpcAdvice + public static class TestAdviceWithMetadata { + + @GrpcExceptionHandler(FirstLevelException.class) + public StatusException handleFirstLevelException(MyRootRuntimeException e) { + + Status status = Status.NOT_FOUND.withCause(e).withDescription(e.getMessage()); + Metadata metadata = GrpcMetaDataUtils.createExpectedAsciiHeader(); + return status.asException(metadata); + } + + @GrpcExceptionHandler(ClassCastException.class) + public StatusRuntimeException handleClassCastException() { + + Status status = Status.FAILED_PRECONDITION.withDescription("Casting with classes failed."); + Metadata metadata = GrpcMetaDataUtils.createExpectedAsciiHeader(); + return status.asRuntimeException(metadata); + } + + @GrpcExceptionHandler + public StatusRuntimeException handleStatusMappingException(StatusMappingException e) { + + throw new NullPointerException("Simulate developer error"); + } + + + public static class MyRootRuntimeException extends RuntimeException { + + public MyRootRuntimeException(String msg) { + super(msg); + } + } + + public static class FirstLevelException extends MyRootRuntimeException { + + public FirstLevelException(String msg) { + super(msg); + } + } + + public static class SecondLevelException extends FirstLevelException { + + public SecondLevelException(String msg) { + super(msg); + } + } + + public static class StatusMappingException extends RuntimeException { + + public StatusMappingException(String msg) { + super(msg); + } + } + + } + + + @GrpcAdvice + public static class TestAdviceForInheritedExceptions { + + + @GrpcExceptionHandler(SecondLevelException.class) + public Status handleSecondLevelException(SecondLevelException e) { + + return Status.ABORTED.withCause(e).withDescription(e.getMessage()); + } + + @GrpcExceptionHandler + public Status handleMyRootRuntimeException(MyRootRuntimeException e) { + + return Status.DEADLINE_EXCEEDED.withCause(e).withDescription(e.getMessage()); + } + + } + + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/config/InProcessConfiguration.java b/tests/src/test/java/net/devh/boot/grpc/test/config/InProcessConfiguration.java new file mode 100644 index 000000000..b6453b3e5 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/config/InProcessConfiguration.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.test.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.grpc.inprocess.InProcessChannelBuilder; +import net.devh.boot.grpc.client.channelfactory.GrpcChannelFactory; +import net.devh.boot.grpc.client.channelfactory.InProcessChannelFactory; +import net.devh.boot.grpc.client.config.GrpcChannelsProperties; +import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorRegistry; +import net.devh.boot.grpc.server.config.GrpcServerProperties; +import net.devh.boot.grpc.server.serverfactory.GrpcServerFactory; +import net.devh.boot.grpc.server.serverfactory.InProcessGrpcServerFactory; +import net.devh.boot.grpc.server.service.GrpcServiceDefinition; +import net.devh.boot.grpc.server.service.GrpcServiceDiscoverer; + +@Configuration +public class InProcessConfiguration { + + @Bean + GrpcChannelFactory grpcChannelFactory(final GrpcChannelsProperties properties, + final GlobalClientInterceptorRegistry globalClientInterceptorRegistry) { + return new InProcessChannelFactory(properties, globalClientInterceptorRegistry) { + + @Override + protected InProcessChannelBuilder newChannelBuilder(final String name) { + return super.newChannelBuilder("test"); // Use fixed inMemory channel name: test + } + + }; + } + + @Bean + GrpcServerFactory grpcServerFactory(final GrpcServerProperties properties, + final GrpcServiceDiscoverer discoverer) { + final InProcessGrpcServerFactory factory = new InProcessGrpcServerFactory("test", properties); + for (final GrpcServiceDefinition service : discoverer.findGrpcServices()) { + factory.addService(service); + } + return factory; + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/config/ManualSecurityConfiguration.java b/tests/src/test/java/net/devh/boot/grpc/test/config/ManualSecurityConfiguration.java new file mode 100644 index 000000000..6414fa3b9 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/config/ManualSecurityConfiguration.java @@ -0,0 +1,56 @@ +/* + * 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.config; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.access.AccessDecisionManager; +import org.springframework.security.access.AccessDecisionVoter; +import org.springframework.security.access.vote.UnanimousBased; + +import net.devh.boot.grpc.server.security.check.AccessPredicate; +import net.devh.boot.grpc.server.security.check.AccessPredicateVoter; +import net.devh.boot.grpc.server.security.check.GrpcSecurityMetadataSource; +import net.devh.boot.grpc.server.security.check.ManualGrpcSecurityMetadataSource; +import net.devh.boot.grpc.test.proto.TestServiceGrpc; + +@Configuration +public class ManualSecurityConfiguration { + + @Bean + AccessDecisionManager accessDecisionManager() { + final List> voters = new ArrayList<>(); + voters.add(new AccessPredicateVoter()); + return new UnanimousBased(voters); + } + + @Bean + GrpcSecurityMetadataSource grpcSecurityMetadataSource() { + final ManualGrpcSecurityMetadataSource source = new ManualGrpcSecurityMetadataSource(); + source.set(TestServiceGrpc.getSecureMethod(), AccessPredicate.hasRole("ROLE_CLIENT1")); + source.set(TestServiceGrpc.getSecureDrainMethod(), AccessPredicate.hasRole("ROLE_CLIENT1")); + source.set(TestServiceGrpc.getSecureSupplyMethod(), AccessPredicate.hasRole("ROLE_CLIENT1")); + source.set(TestServiceGrpc.getSecureBidiMethod(), AccessPredicate.hasRole("ROLE_CLIENT1")); + source.setDefault(AccessPredicate.permitAll()); + return source; + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/config/MetricConfiguration.java b/tests/src/test/java/net/devh/boot/grpc/test/config/MetricConfiguration.java new file mode 100644 index 000000000..3d045f469 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/config/MetricConfiguration.java @@ -0,0 +1,38 @@ +/* + * 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.config; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import net.devh.boot.grpc.client.autoconfigure.GrpcClientMetricAutoConfiguration; +import net.devh.boot.grpc.server.autoconfigure.GrpcServerMetricAutoConfiguration; + +@Configuration +@ImportAutoConfiguration({GrpcClientMetricAutoConfiguration.class, GrpcServerMetricAutoConfiguration.class}) +public class MetricConfiguration { + + @Bean + MeterRegistry meterRegistry() { + return new SimpleMeterRegistry(); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/config/OrderedClientInterceptorConfiguration.java b/tests/src/test/java/net/devh/boot/grpc/test/config/OrderedClientInterceptorConfiguration.java new file mode 100644 index 000000000..29f158ea3 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/config/OrderedClientInterceptorConfiguration.java @@ -0,0 +1,96 @@ +/* + * 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.config; + +import javax.annotation.Priority; + +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; +import net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor; + +@Configuration +public class OrderedClientInterceptorConfiguration { + + @GrpcGlobalClientInterceptor + @Priority(30) + public class SecondPriorityAnnotatedInterceptor implements ClientInterceptor { + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return next.newCall(method, callOptions); + } + } + + @GrpcGlobalClientInterceptor + @Order(20) + public class SecondOrderAnnotatedInterceptor implements ClientInterceptor { + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return next.newCall(method, callOptions); + } + } + + @GrpcGlobalClientInterceptor + public class FirstOrderedInterfaceInterceptor implements ClientInterceptor, Ordered { + public int getOrder() { + return 40; + } + + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return next.newCall(method, callOptions); + } + } + + @GrpcGlobalClientInterceptor + @Order(10) + public class FirstOrderAnnotatedInterceptor implements ClientInterceptor { + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return next.newCall(method, callOptions); + } + } + + @GrpcGlobalClientInterceptor + public class SecondOrderedInterfaceInterceptor implements ClientInterceptor, Ordered { + public int getOrder() { + return 50; + } + + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return next.newCall(method, callOptions); + } + } + + @GrpcGlobalClientInterceptor + @Priority(5) + public class FirstPriorityAnnotatedInterceptor implements ClientInterceptor { + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return next.newCall(method, callOptions); + } + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/config/OrderedServerInterceptorConfiguration.java b/tests/src/test/java/net/devh/boot/grpc/test/config/OrderedServerInterceptorConfiguration.java new file mode 100644 index 000000000..92366d011 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/config/OrderedServerInterceptorConfiguration.java @@ -0,0 +1,95 @@ +/* + * 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.config; + +import javax.annotation.Priority; + +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; + +@Configuration +public class OrderedServerInterceptorConfiguration { + + @GrpcGlobalServerInterceptor + @Priority(30) + public class SecondPriorityAnnotatedInterceptor implements ServerInterceptor { + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, + ServerCallHandler next) { + return next.startCall(call, headers); + } + } + + @GrpcGlobalServerInterceptor + @Order(20) + public class SecondOrderAnnotatedInterceptor implements ServerInterceptor { + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, + ServerCallHandler next) { + return next.startCall(call, headers); + } + } + + @GrpcGlobalServerInterceptor + public class FirstOrderedInterfaceInterceptor implements ServerInterceptor, Ordered { + public int getOrder() { + return 40; + } + + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, + ServerCallHandler next) { + return next.startCall(call, headers); + } + } + + @GrpcGlobalServerInterceptor + @Order(10) + public class FirstOrderAnnotatedInterceptor implements ServerInterceptor { + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, + ServerCallHandler next) { + return next.startCall(call, headers); + } + } + + @GrpcGlobalServerInterceptor + public class SecondOrderedInterfaceInterceptor implements ServerInterceptor, Ordered { + public int getOrder() { + return 50; + } + + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, + ServerCallHandler next) { + return next.startCall(call, headers); + } + } + + @GrpcGlobalServerInterceptor + @Priority(5) + public class FirstPriorityAnnotatedInterceptor implements ServerInterceptor { + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, + ServerCallHandler next) { + return next.startCall(call, headers); + } + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/config/ScopedServiceConfiguration.java b/tests/src/test/java/net/devh/boot/grpc/test/config/ScopedServiceConfiguration.java new file mode 100644 index 000000000..161b4ae36 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/config/ScopedServiceConfiguration.java @@ -0,0 +1,45 @@ +/* + * 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.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; + +import net.devh.boot.grpc.server.scope.GrpcRequestScope; +import net.devh.boot.grpc.test.server.ScopedTestServiceImpl; +import net.devh.boot.grpc.test.server.ScopedTestServiceImpl.RequestId; +import net.devh.boot.grpc.test.server.TestServiceImpl; + +@Configuration +public class ScopedServiceConfiguration extends ServiceConfiguration { + + @Override + @Bean + TestServiceImpl testService() { + return new ScopedTestServiceImpl(); + } + + @Bean + @Scope(scopeName = GrpcRequestScope.GRPC_REQUEST_SCOPE_NAME, proxyMode = ScopedProxyMode.TARGET_CLASS) + RequestId requestId() { + return new RequestId(); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/config/ServiceConfiguration.java b/tests/src/test/java/net/devh/boot/grpc/test/config/ServiceConfiguration.java new file mode 100644 index 000000000..518994f43 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/config/ServiceConfiguration.java @@ -0,0 +1,33 @@ +/* + * 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.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import net.devh.boot.grpc.test.server.TestServiceImpl; + +@Configuration +public class ServiceConfiguration { + + @Bean + TestServiceImpl testService() { + return new TestServiceImpl(); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/config/WithBasicAuthSecurityConfiguration.java b/tests/src/test/java/net/devh/boot/grpc/test/config/WithBasicAuthSecurityConfiguration.java new file mode 100644 index 000000000..1665757e2 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/config/WithBasicAuthSecurityConfiguration.java @@ -0,0 +1,120 @@ +/* + * 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.config; + +import static net.devh.boot.grpc.client.security.CallCredentialsHelper.basicAuth; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +import com.google.common.collect.ImmutableMap; + +import io.grpc.CallCredentials; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.inject.StubTransformer; +import net.devh.boot.grpc.client.security.CallCredentialsHelper; +import net.devh.boot.grpc.server.security.authentication.BasicGrpcAuthenticationReader; +import net.devh.boot.grpc.server.security.authentication.CompositeGrpcAuthenticationReader; +import net.devh.boot.grpc.server.security.authentication.GrpcAuthenticationReader; + +@Slf4j +@Configuration +public class WithBasicAuthSecurityConfiguration { + + // Server-Side + + // private static final String ANONYMOUS_KEY = UUID.randomUUID().toString(); + + @Bean + PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(10); + } + + @Bean + UserDetailsService userDetailsService() { + return username -> { + log.debug("Searching user: {}", username); + if (username.length() > 10) { + throw new UsernameNotFoundException("Could not find user!"); + } + final List authorities = + Arrays.asList(new SimpleGrantedAuthority("ROLE_" + username.toUpperCase())); + return new User(username, passwordEncoder().encode(username), authorities); + }; + } + + @Bean + DaoAuthenticationProvider daoAuthenticationProvider() { + final DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); + provider.setUserDetailsService(userDetailsService()); + provider.setPasswordEncoder(passwordEncoder()); + return provider; + } + + // @Bean + // AnonymousAuthenticationProvider anonymousAuthenticationProvider() { + // return new AnonymousAuthenticationProvider(ANONYMOUS_KEY); + // } + + @Bean + AuthenticationManager authenticationManager() { + final List providers = new ArrayList<>(); + providers.add(daoAuthenticationProvider()); + // providers.add(anonymousAuthenticationProvider()); + return new ProviderManager(providers); + } + + @Bean + GrpcAuthenticationReader authenticationReader() { + final List readers = new ArrayList<>(); + readers.add(new BasicGrpcAuthenticationReader()); + // readers.add(new AnonymousAuthenticationReader(ANONYMOUS_KEY)); + return new CompositeGrpcAuthenticationReader(readers); + } + + // Client-Side + + @Bean + StubTransformer mappedCredentialsStubTransformer() { + return CallCredentialsHelper.mappedCredentialsStubTransformer(ImmutableMap.builder() + .put("test", testCallCredentials("client1")) + .put("noPerm", testCallCredentials("client2")) + .put("unknownUser", testCallCredentials("unknownUser")) + // .put("noAuth", null) + .build()); + } + + private CallCredentials testCallCredentials(final String username) { + return basicAuth(username, username); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/config/WithCertificateSecurityConfiguration.java b/tests/src/test/java/net/devh/boot/grpc/test/config/WithCertificateSecurityConfiguration.java new file mode 100644 index 000000000..f8fee5072 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/config/WithCertificateSecurityConfiguration.java @@ -0,0 +1,67 @@ +/* + * 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.config; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.ProviderManager; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetailsService; + +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.security.authentication.CompositeGrpcAuthenticationReader; +import net.devh.boot.grpc.server.security.authentication.GrpcAuthenticationReader; +import net.devh.boot.grpc.server.security.authentication.SSLContextGrpcAuthenticationReader; +import net.devh.boot.grpc.server.security.authentication.X509CertificateAuthenticationProvider; + +@Slf4j +@Configuration +public class WithCertificateSecurityConfiguration { + + @Bean + UserDetailsService userDetailsService() { + return username -> { + log.debug("Searching user: {}", username); + final List authorities = + Arrays.asList(new SimpleGrantedAuthority("ROLE_" + username.toUpperCase())); + return new User(username, "", authorities); + }; + } + + @Bean + AuthenticationManager authenticationManager() { + final List providers = new ArrayList<>(); + providers.add(new X509CertificateAuthenticationProvider(userDetailsService())); + return new ProviderManager(providers); + } + + @Bean + GrpcAuthenticationReader authenticationReader() { + final List readers = new ArrayList<>(); + readers.add(new SSLContextGrpcAuthenticationReader()); + return new CompositeGrpcAuthenticationReader(readers); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/inject/CustomGrpc.java b/tests/src/test/java/net/devh/boot/grpc/test/inject/CustomGrpc.java new file mode 100644 index 000000000..dacf9ae2a --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/inject/CustomGrpc.java @@ -0,0 +1,87 @@ +/* + * 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 io.grpc.CallOptions; +import io.grpc.Channel; + +/** + * Fake generated grpc class. + */ +public class CustomGrpc { + + public static CustomAccessibleStub custom(final Channel channel) { + return new CustomAccessibleStub(channel); + } + + public static FactoryMethodAccessibleStub newFactoryMethodAccessibleStubStub(final Channel channel) { + return new FactoryMethodAccessibleStub(channel); + } + + public static class CustomAccessibleStub extends CustomStub { + + private CustomAccessibleStub(final Channel channel) { + super(channel); + } + + private CustomAccessibleStub(final Channel channel, final CallOptions callOptions) { + super(channel, callOptions); + } + + @Override + protected CustomAccessibleStub build(final Channel channel, final CallOptions callOptions) { + return new CustomAccessibleStub(channel, callOptions); + } + + } + + public static class FactoryMethodAccessibleStub extends OtherStub { + + private FactoryMethodAccessibleStub(final Channel channel) { + super(channel); + } + + private FactoryMethodAccessibleStub(final Channel channel, final CallOptions callOptions) { + super(channel, callOptions); + } + + @Override + protected FactoryMethodAccessibleStub build(final Channel channel, final CallOptions callOptions) { + return new FactoryMethodAccessibleStub(channel, callOptions); + } + + } + + public static class ConstructorAccessibleStub extends OtherStub { + + public ConstructorAccessibleStub(final Channel channel) { + super(channel); + } + + public ConstructorAccessibleStub(final Channel channel, final CallOptions callOptions) { + super(channel, callOptions); + } + + @Override + protected ConstructorAccessibleStub build(final Channel channel, final CallOptions callOptions) { + return new ConstructorAccessibleStub(channel, callOptions); + } + + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/inject/CustomStub.java b/tests/src/test/java/net/devh/boot/grpc/test/inject/CustomStub.java new file mode 100644 index 000000000..3da91cccb --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/inject/CustomStub.java @@ -0,0 +1,40 @@ +/* + * 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 io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.stub.AbstractStub; + +/** + * Simulates a custom stub type provided by a third party library, that requires a custom + * {@link net.devh.boot.grpc.client.stubfactory.StubFactory StubFactory}. + * + * @param The type of the stub implementation. + */ +public abstract class CustomStub> extends AbstractStub { + + protected CustomStub(final Channel channel) { + super(channel); + } + + protected CustomStub(final Channel channel, final CallOptions callOptions) { + super(channel, callOptions); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/inject/GrpcClientInjectionTest.java b/tests/src/test/java/net/devh/boot/grpc/test/inject/GrpcClientInjectionTest.java new file mode 100644 index 000000000..bd72be1b4 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/inject/GrpcClientInjectionTest.java @@ -0,0 +1,177 @@ +/* + * 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 javax.annotation.PostConstruct; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import io.grpc.Channel; +import io.grpc.stub.AbstractStub; +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.ServiceConfiguration; +import net.devh.boot.grpc.test.inject.CustomGrpc.ConstructorAccessibleStub; +import net.devh.boot.grpc.test.inject.CustomGrpc.CustomAccessibleStub; +import net.devh.boot.grpc.test.inject.CustomGrpc.FactoryMethodAccessibleStub; +import net.devh.boot.grpc.test.inject.GrpcClientInjectionTest.TestConfig; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceBlockingStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceFutureStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceStub; + +/** + * A test checking that the client injection works. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@SpringBootTest +@SpringJUnitConfig(classes = {TestConfig.class, InProcessConfiguration.class, ServiceConfiguration.class, + BaseAutoConfiguration.class}) +@DirtiesContext +class GrpcClientInjectionTest { + + @GrpcClient("test") + Channel channel; + @GrpcClient("test") + TestServiceStub stub; + @GrpcClient("test") + TestServiceBlockingStub blockingStub; + @GrpcClient("test") + TestServiceFutureStub futureStub; + @GrpcClient("test") + ConstructorAccessibleStub constructorStub; + @GrpcClient("test") + FactoryMethodAccessibleStub factoryMethodStub; + @GrpcClient("test") + CustomAccessibleStub customStub; + + Channel channelSetted; + TestServiceStub stubSetted; + TestServiceBlockingStub blockingStubSetted; + TestServiceFutureStub futureStubSetted; + ConstructorAccessibleStub constructorStubSetted; + FactoryMethodAccessibleStub factoryMethodStubSetted; + CustomAccessibleStub customStubSetted; + + @PostConstruct + public void init() { + // Test injection + assertNotNull(this.channel, "channel"); + assertNotNull(this.stub, "stub"); + assertNotNull(this.blockingStub, "blockingStub"); + assertNotNull(this.futureStub, "futureStub"); + assertNotNull(this.constructorStub, "constructorStub"); + assertNotNull(this.factoryMethodStub, "factoryMethodStub"); + assertNotNull(this.customStub, "customStub"); + } + + @GrpcClient("test") + void inject(final Channel channel) { + assertNotNull(channel, "channel"); + this.channelSetted = channel; + } + + @GrpcClient("test") + void inject(final TestServiceStub stub) { + assertNotNull(stub, "stub"); + this.stubSetted = stub; + } + + @GrpcClient("test") + void inject(final TestServiceBlockingStub stub) { + assertNotNull(stub, "stub"); + this.blockingStubSetted = stub; + } + + @GrpcClient("test") + void inject(final TestServiceFutureStub stub) { + assertNotNull(stub, "stub"); + this.futureStubSetted = stub; + } + + @GrpcClient("test") + void inject(final ConstructorAccessibleStub stub) { + assertNotNull(stub, "stub"); + this.constructorStubSetted = stub; + } + + @GrpcClient("test") + void inject(final FactoryMethodAccessibleStub stub) { + assertNotNull(stub, "stub"); + this.factoryMethodStubSetted = stub; + } + + @GrpcClient("test") + void inject(final CustomAccessibleStub stub) { + assertNotNull(stub, "stub"); + this.customStubSetted = stub; + } + + @Test + void testAllSet() { + // Field injection + assertNotNull(this.channel, "channel"); + assertNotNull(this.stub, "stub"); + assertNotNull(this.blockingStub, "blockingStub"); + assertNotNull(this.futureStub, "futureStub"); + assertNotNull(this.constructorStub, "constructorStub"); + assertNotNull(this.factoryMethodStub, "factoryMethodStub"); + assertNotNull(this.customStub, "customStub"); + // Setter injection + assertNotNull(this.channelSetted, "channelSetted"); + assertNotNull(this.stubSetted, "stubSetted"); + assertNotNull(this.blockingStubSetted, "blockingStubSetted"); + assertNotNull(this.futureStubSetted, "futureStubSetted"); + assertNotNull(this.constructorStubSetted, "constructorStubSetted"); + assertNotNull(this.factoryMethodStubSetted, "factoryMethodStubSetted"); + assertNotNull(this.customStubSetted, "customStubSetted"); + } + + @TestConfiguration + 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"; + } + + }; + } + + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/inject/OtherStub.java b/tests/src/test/java/net/devh/boot/grpc/test/inject/OtherStub.java new file mode 100644 index 000000000..f1321cc0f --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/inject/OtherStub.java @@ -0,0 +1,41 @@ +/* + * 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 io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.stub.AbstractStub; +import net.devh.boot.grpc.client.stubfactory.FallbackStubFactory; + +/** + * Simulates a custom stub type provided by a third party library, that can be created by the + * {@link FallbackStubFactory}. + * + * @param The type of the stub implementation. + */ +public abstract class OtherStub> extends AbstractStub { + + protected OtherStub(final Channel channel) { + super(channel); + } + + protected OtherStub(final Channel channel, final CallOptions callOptions) { + super(channel, callOptions); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/interceptor/DefaultServerInterceptorTest.java b/tests/src/test/java/net/devh/boot/grpc/test/interceptor/DefaultServerInterceptorTest.java new file mode 100644 index 000000000..823927671 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/interceptor/DefaultServerInterceptorTest.java @@ -0,0 +1,76 @@ +/* + * 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.interceptor; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import io.grpc.ServerInterceptor; +import net.devh.boot.grpc.server.interceptor.GlobalServerInterceptorRegistry; +import net.devh.boot.grpc.server.metric.MetricCollectingServerInterceptor; +import net.devh.boot.grpc.server.scope.GrpcRequestScope; +import net.devh.boot.grpc.server.security.interceptors.AuthenticatingServerInterceptor; +import net.devh.boot.grpc.server.security.interceptors.AuthorizationCheckingServerInterceptor; +import net.devh.boot.grpc.server.security.interceptors.ExceptionTranslatingServerInterceptor; +import net.devh.boot.grpc.test.config.ManualSecurityConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.config.WithBasicAuthSecurityConfiguration; + +@SpringBootTest +@SpringJUnitConfig(classes = {ServiceConfiguration.class, WithBasicAuthSecurityConfiguration.class, + ManualSecurityConfiguration.class}) +@EnableAutoConfiguration +@DirtiesContext +public class DefaultServerInterceptorTest { + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private GlobalServerInterceptorRegistry registry; + + @Test + void testOrderingOfTheDefaultInterceptors() { + List expected = new ArrayList<>(); + expected.add(this.applicationContext.getBean(GrpcRequestScope.class)); + expected.add(this.applicationContext.getBean(MetricCollectingServerInterceptor.class)); + expected.add(this.applicationContext.getBean(ExceptionTranslatingServerInterceptor.class)); + expected.add(this.applicationContext.getBean(AuthenticatingServerInterceptor.class)); + expected.add(this.applicationContext.getBean(AuthorizationCheckingServerInterceptor.class)); + + List actual = new ArrayList<>(this.registry.getServerInterceptors()); + assertEquals(expected, actual); + + Collections.shuffle(actual); + AnnotationAwareOrderComparator.sort(actual); + assertEquals(expected, actual); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/interceptor/OrderedClientInterceptorTest.java b/tests/src/test/java/net/devh/boot/grpc/test/interceptor/OrderedClientInterceptorTest.java new file mode 100644 index 000000000..b4a5b4152 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/interceptor/OrderedClientInterceptorTest.java @@ -0,0 +1,75 @@ +/* + * 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.interceptor; + +import java.util.List; + +import org.junit.Assert; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.google.common.collect.Iterators; + +import io.grpc.ClientInterceptor; +import net.devh.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration; +import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorRegistry; +import net.devh.boot.grpc.test.config.OrderedClientInterceptorConfiguration; + +@SpringBootTest +@SpringJUnitConfig(classes = {OrderedClientInterceptorConfiguration.class, GrpcClientAutoConfiguration.class}) +@DirtiesContext +public class OrderedClientInterceptorTest { + + @Autowired + GlobalClientInterceptorRegistry registry; + + @Test + public void testOrderingByOrderAnnotation() { + int firstInterceptorIndex = findIndexOfClass(registry.getClientInterceptors(), + OrderedClientInterceptorConfiguration.FirstOrderAnnotatedInterceptor.class); + int secondInterceptorIndex = findIndexOfClass(registry.getClientInterceptors(), + OrderedClientInterceptorConfiguration.SecondOrderAnnotatedInterceptor.class); + Assert.assertTrue(firstInterceptorIndex < secondInterceptorIndex); + } + + @Test + public void testOrderingByPriorityAnnotation() { + int firstInterceptorIndex = findIndexOfClass(registry.getClientInterceptors(), + OrderedClientInterceptorConfiguration.FirstPriorityAnnotatedInterceptor.class); + int secondInterceptorIndex = findIndexOfClass(registry.getClientInterceptors(), + OrderedClientInterceptorConfiguration.SecondPriorityAnnotatedInterceptor.class); + Assert.assertTrue(firstInterceptorIndex < secondInterceptorIndex); + } + + @Test + public void testOrderingByOrderedInterface() { + int firstInterceptorIndex = findIndexOfClass(registry.getClientInterceptors(), + OrderedClientInterceptorConfiguration.FirstOrderedInterfaceInterceptor.class); + int secondInterceptorIndex = findIndexOfClass(registry.getClientInterceptors(), + OrderedClientInterceptorConfiguration.SecondOrderedInterfaceInterceptor.class); + Assert.assertTrue(firstInterceptorIndex < secondInterceptorIndex); + } + + private int findIndexOfClass(List interceptors, Class clazz) { + return Iterators.indexOf(interceptors.iterator(), clazz::isInstance); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/interceptor/OrderedServerInterceptorTest.java b/tests/src/test/java/net/devh/boot/grpc/test/interceptor/OrderedServerInterceptorTest.java new file mode 100644 index 000000000..2c6f8fcdb --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/interceptor/OrderedServerInterceptorTest.java @@ -0,0 +1,75 @@ +/* + * 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.interceptor; + +import java.util.List; + +import org.junit.Assert; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.google.common.collect.Iterators; + +import io.grpc.ServerInterceptor; +import net.devh.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration; +import net.devh.boot.grpc.server.interceptor.GlobalServerInterceptorRegistry; +import net.devh.boot.grpc.test.config.OrderedServerInterceptorConfiguration; + +@SpringBootTest +@SpringJUnitConfig(classes = {OrderedServerInterceptorConfiguration.class, GrpcServerAutoConfiguration.class}) +@DirtiesContext +public class OrderedServerInterceptorTest { + + @Autowired + GlobalServerInterceptorRegistry registry; + + @Test + public void testOrderingByOrderAnnotation() { + int firstInterceptorIndex = findIndexOfClass(registry.getServerInterceptors(), + OrderedServerInterceptorConfiguration.FirstOrderAnnotatedInterceptor.class); + int secondInterceptorIndex = findIndexOfClass(registry.getServerInterceptors(), + OrderedServerInterceptorConfiguration.SecondOrderAnnotatedInterceptor.class); + Assert.assertTrue(firstInterceptorIndex < secondInterceptorIndex); + } + + @Test + public void testOrderingByPriorityAnnotation() { + int firstInterceptorIndex = findIndexOfClass(registry.getServerInterceptors(), + OrderedServerInterceptorConfiguration.FirstPriorityAnnotatedInterceptor.class); + int secondInterceptorIndex = findIndexOfClass(registry.getServerInterceptors(), + OrderedServerInterceptorConfiguration.SecondPriorityAnnotatedInterceptor.class); + Assert.assertTrue(firstInterceptorIndex < secondInterceptorIndex); + } + + @Test + public void testOrderingByOrderedInterface() { + int firstInterceptorIndex = findIndexOfClass(registry.getServerInterceptors(), + OrderedServerInterceptorConfiguration.FirstOrderedInterfaceInterceptor.class); + int secondInterceptorIndex = findIndexOfClass(registry.getServerInterceptors(), + OrderedServerInterceptorConfiguration.SecondOrderedInterfaceInterceptor.class); + Assert.assertTrue(firstInterceptorIndex < secondInterceptorIndex); + } + + private int findIndexOfClass(List interceptors, Class clazz) { + return Iterators.indexOf(interceptors.iterator(), clazz::isInstance); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/interceptor/PickupClientInterceptorTest.java b/tests/src/test/java/net/devh/boot/grpc/test/interceptor/PickupClientInterceptorTest.java new file mode 100644 index 000000000..ec07e03da --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/interceptor/PickupClientInterceptorTest.java @@ -0,0 +1,144 @@ +/* + * 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.interceptor; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +import io.grpc.CallOptions; +import io.grpc.Channel; +import io.grpc.ClientCall; +import io.grpc.ClientInterceptor; +import io.grpc.MethodDescriptor; +import net.devh.boot.grpc.client.autoconfigure.GrpcClientAutoConfiguration; +import net.devh.boot.grpc.client.interceptor.AnnotationGlobalClientInterceptorConfigurer; +import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorConfigurer; +import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorRegistry; +import net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor; + +/** + * Tests that {@link GrpcGlobalClientInterceptor}, {@link GlobalClientInterceptorConfigurer} and + * {@link GlobalClientInterceptorRegistry} work as expected. + */ +@SpringBootTest +class PickupClientInterceptorTest { + + @Autowired + AnnotationGlobalClientInterceptorConfigurer annotationGlobalClientInterceptorConfigurer; + + @Autowired + GlobalClientInterceptorRegistry globalClientInterceptorRegistry; + + @Test + void test() { + final List interceptors = new ArrayList<>(); + this.annotationGlobalClientInterceptorConfigurer.configureClientInterceptors(interceptors); + assertThat(interceptors).containsExactlyInAnyOrder( + new ConfigAnnotatedClientInterceptor(), + new ClassAnnotatedClientInterceptor()); + + assertThat(this.globalClientInterceptorRegistry.getClientInterceptors()).containsExactlyInAnyOrder( + new ConfigAnnotatedClientInterceptor(), + new ClassAnnotatedClientInterceptor(), + new ConfigurerClientInterceptor()); + + } + + @SpringBootConfiguration + @ImportAutoConfiguration(GrpcClientAutoConfiguration.class) + @ComponentScan(basePackageClasses = ClassAnnotatedClientInterceptor.class) + public static class TestConfig { + + @GrpcGlobalClientInterceptor + ConfigAnnotatedClientInterceptor configAnnotatedClientInterceptor() { + return new ConfigAnnotatedClientInterceptor(); + } + + @Bean + GlobalClientInterceptorConfigurer globalClientInterceptorConfigurer() { + return interceptors -> interceptors.add(new ConfigurerClientInterceptor()); + } + + } + + /** + * Simple No-Op ClientInterceptor for testing purposes. + */ + static class NoOpClientInterceptor implements ClientInterceptor { + + @Override + public ClientCall interceptCall( + final MethodDescriptor method, + final CallOptions callOptions, + final Channel next) { + return next.newCall(method, callOptions); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + // Fake equality for test simplifications + public boolean equals(final Object obj) { + return obj != null && getClass().equals(obj.getClass()); + } + + } + + /** + * Used to check that {@link GrpcGlobalClientInterceptor} works in {@link Configuration}s. + */ + static class ConfigAnnotatedClientInterceptor extends NoOpClientInterceptor { + } + + /** + * Used to check that {@link GrpcGlobalClientInterceptor} works on bean classes themselves. + */ + @GrpcGlobalClientInterceptor + static class ClassAnnotatedClientInterceptor extends NoOpClientInterceptor { + } + + /** + * Used to check that {@link GlobalClientInterceptorConfigurer} work. + */ + @Component + static class ConfigurerClientInterceptor extends NoOpClientInterceptor { + } + + /** + * Used to check that {@link ClientInterceptor} aren't picked up randomly. + */ + @Component + static class DoNotPickMeUpClientInterceptor extends NoOpClientInterceptor { + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/interceptor/PickupServerInterceptorTest.java b/tests/src/test/java/net/devh/boot/grpc/test/interceptor/PickupServerInterceptorTest.java new file mode 100644 index 000000000..9c5d7690c --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/interceptor/PickupServerInterceptorTest.java @@ -0,0 +1,150 @@ +/* + * 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.interceptor; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.stereotype.Component; + +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.ServerCall.Listener; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import net.devh.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration; +import net.devh.boot.grpc.server.interceptor.AnnotationGlobalServerInterceptorConfigurer; +import net.devh.boot.grpc.server.interceptor.GlobalServerInterceptorConfigurer; +import net.devh.boot.grpc.server.interceptor.GlobalServerInterceptorRegistry; +import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; +import net.devh.boot.grpc.server.scope.GrpcRequestScope; + +/** + * Tests that {@link GrpcGlobalServerInterceptor}, {@link GlobalServerInterceptorConfigurer} and + * {@link GlobalServerInterceptorRegistry} work as expected. + */ +@SpringBootTest +class PickupServerInterceptorTest { + + @Autowired + AnnotationGlobalServerInterceptorConfigurer annotationGlobalServerInterceptorConfigurer; + + @Autowired + GlobalServerInterceptorRegistry globalServerInterceptorRegistry; + + @Autowired + GrpcRequestScope grpcRequestScope; + + @Test + void test() { + final List interceptors = new ArrayList<>(); + this.annotationGlobalServerInterceptorConfigurer.configureServerInterceptors(interceptors); + assertThat(interceptors).containsExactlyInAnyOrder( + new ConfigAnnotatedServerInterceptor(), + new ClassAnnotatedServerInterceptor(), + this.grpcRequestScope); + + assertThat(this.globalServerInterceptorRegistry.getServerInterceptors()).containsExactlyInAnyOrder( + new ConfigAnnotatedServerInterceptor(), + new ClassAnnotatedServerInterceptor(), + new ConfigurerServerInterceptor(), + this.grpcRequestScope); + + } + + @SpringBootConfiguration + @ImportAutoConfiguration(GrpcServerAutoConfiguration.class) + @ComponentScan(basePackageClasses = ClassAnnotatedServerInterceptor.class) + public static class TestConfig { + + @GrpcGlobalServerInterceptor + ConfigAnnotatedServerInterceptor configAnnotatedServerInterceptor() { + return new ConfigAnnotatedServerInterceptor(); + } + + @Bean + GlobalServerInterceptorConfigurer globalServerInterceptorConfigurer() { + return interceptors -> interceptors.add(new ConfigurerServerInterceptor()); + } + + } + + /** + * Simple No-Op ServerInterceptor for testing purposes. + */ + static class NoOpServerInterceptor implements ServerInterceptor { + + @Override + public Listener interceptCall( + final ServerCall call, + final Metadata headers, + final ServerCallHandler next) { + return next.startCall(call, headers); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + // Fake equality for test simplifications + public boolean equals(final Object obj) { + return obj != null && getClass().equals(obj.getClass()); + } + + } + + /** + * Used to check that {@link GrpcGlobalServerInterceptor} works in {@link Configuration}s. + */ + static class ConfigAnnotatedServerInterceptor extends NoOpServerInterceptor { + } + + /** + * Used to check that {@link GrpcGlobalServerInterceptor} works on bean classes themselves. + */ + @GrpcGlobalServerInterceptor + static class ClassAnnotatedServerInterceptor extends NoOpServerInterceptor { + } + + /** + * Used to check that {@link GlobalServerInterceptorConfigurer} work. + */ + @Component + static class ConfigurerServerInterceptor extends NoOpServerInterceptor { + } + + /** + * Used to check that {@link ServerInterceptor} aren't picked up randomly. + */ + @Component + static class DoNotPickMeUpServerInterceptor extends NoOpServerInterceptor { + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricAutoConfigurationTest.java b/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricAutoConfigurationTest.java new file mode 100644 index 000000000..ad605e189 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricAutoConfigurationTest.java @@ -0,0 +1,68 @@ +/* + * 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.metric; + +import static net.devh.boot.grpc.test.server.TestServiceImpl.METHOD_COUNT; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.autoconfigure.GrpcServerMetricAutoConfiguration; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.MetricConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; + +/** + * A test to verify that the server auto configuration works. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest +@SpringJUnitConfig(classes = {MetricConfiguration.class, ServiceConfiguration.class, BaseAutoConfiguration.class}) +@ImportAutoConfiguration(GrpcServerMetricAutoConfiguration.class) +@DirtiesContext +public class MetricAutoConfigurationTest { + + @Autowired + private MeterRegistry meterRegistry; + + @Test + @DirtiesContext + public void testAutoDiscovery() { + log.info("--- Starting tests with auto discovery ---"); + for (final Meter meter : meterRegistry.getMeters()) { + log.debug("Found meter: {}", meter.getId()); + } + assertEquals(METHOD_COUNT * 2, + this.meterRegistry.getMeters().stream().filter(Counter.class::isInstance).count()); + assertEquals(METHOD_COUNT, this.meterRegistry.getMeters().stream().filter(Timer.class::isInstance).count()); + log.info("--- Test completed ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricCollectingClientInterceptorTest.java b/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricCollectingClientInterceptorTest.java new file mode 100644 index 000000000..f2f9d770b --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricCollectingClientInterceptorTest.java @@ -0,0 +1,78 @@ +/* + * 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.metric; + +import static io.grpc.Status.Code.OK; +import static io.grpc.Status.Code.UNKNOWN; +import static net.devh.boot.grpc.common.metric.MetricConstants.METRIC_NAME_CLIENT_PROCESSING_DURATION; +import static net.devh.boot.grpc.common.metric.MetricConstants.METRIC_NAME_CLIENT_REQUESTS_SENT; +import static net.devh.boot.grpc.test.server.TestServiceImpl.METHOD_COUNT; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.metric.MetricCollectingClientInterceptor; +import net.devh.boot.grpc.test.proto.TestServiceGrpc; + +@Slf4j +class MetricCollectingClientInterceptorTest { + + @Test + void testClientPreRegistration() { + log.info("--- Starting tests with client pre-registration ---"); + final MeterRegistry meterRegistry = new SimpleMeterRegistry(); + assertEquals(0, meterRegistry.getMeters().size()); + final MetricCollectingClientInterceptor mcci = new MetricCollectingClientInterceptor(meterRegistry); + mcci.preregisterService(TestServiceGrpc.getServiceDescriptor()); + + MetricTestHelper.logMeters(meterRegistry.getMeters()); + assertEquals(METHOD_COUNT * 3, meterRegistry.getMeters().size()); + log.info("--- Test completed ---"); + } + + @Test + void testClientCustomization() { + log.info("--- Starting tests with client customization ---"); + final MeterRegistry meterRegistry = new SimpleMeterRegistry(); + assertEquals(0, meterRegistry.getMeters().size()); + final MetricCollectingClientInterceptor mcci = new MetricCollectingClientInterceptor(meterRegistry, + counter -> counter.tag("type", "counter"), + timer -> timer.tag("type", "timer").publishPercentiles(0.5, 0.9, 0.99), + OK, UNKNOWN); + mcci.preregisterService(TestServiceGrpc.getServiceDescriptor()); + + MetricTestHelper.logMeters(meterRegistry.getMeters()); + assertEquals(METHOD_COUNT * 10, meterRegistry.getMeters().size()); + + final Counter counter = meterRegistry.find(METRIC_NAME_CLIENT_REQUESTS_SENT).counter(); + assertNotNull(counter); + assertEquals("counter", counter.getId().getTag("type")); + + final Timer timer = meterRegistry.find(METRIC_NAME_CLIENT_PROCESSING_DURATION).timer(); + assertNotNull(timer); + assertEquals("timer", timer.getId().getTag("type")); + log.info("--- Test completed ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricCollectingInterceptorTest.java b/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricCollectingInterceptorTest.java new file mode 100644 index 000000000..66f1c45f4 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricCollectingInterceptorTest.java @@ -0,0 +1,620 @@ +/* + * 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.metric; + +import static io.grpc.Status.Code.CANCELLED; +import static io.grpc.Status.Code.INTERNAL; +import static io.grpc.Status.Code.UNIMPLEMENTED; +import static io.grpc.Status.Code.UNKNOWN; +import static net.devh.boot.grpc.common.metric.MetricConstants.METRIC_NAME_CLIENT_PROCESSING_DURATION; +import static net.devh.boot.grpc.common.metric.MetricConstants.METRIC_NAME_CLIENT_REQUESTS_SENT; +import static net.devh.boot.grpc.common.metric.MetricConstants.METRIC_NAME_CLIENT_RESPONSES_RECEIVED; +import static net.devh.boot.grpc.common.metric.MetricConstants.METRIC_NAME_SERVER_PROCESSING_DURATION; +import static net.devh.boot.grpc.common.metric.MetricConstants.METRIC_NAME_SERVER_REQUESTS_RECEIVED; +import static net.devh.boot.grpc.common.metric.MetricConstants.METRIC_NAME_SERVER_RESPONSES_SENT; +import static net.devh.boot.grpc.common.metric.MetricConstants.TAG_METHOD_NAME; +import static net.devh.boot.grpc.common.metric.MetricConstants.TAG_STATUS_CODE; +import static net.devh.boot.grpc.test.config.AwaitableServerClientCallConfiguration.awaitNextServerAndClientCallCloses; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.google.protobuf.Empty; + +import io.grpc.StatusRuntimeException; +import io.grpc.stub.ClientCallStreamObserver; +import io.grpc.stub.StreamObserver; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.autoconfigure.GrpcClientMetricAutoConfiguration; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.common.metric.MetricConstants; +import net.devh.boot.grpc.server.autoconfigure.GrpcServerMetricAutoConfiguration; +import net.devh.boot.grpc.test.config.AwaitableServerClientCallConfiguration; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.MetricConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceBlockingStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceStub; + +/** + * A full test with Spring for both the server side and the client side interceptors. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest(properties = { + "grpc.client.GLOBAL.address=localhost:9090", + "grpc.client.GLOBAL.negotiationType=PLAINTEXT", +}) +@SpringJUnitConfig(classes = { + MetricConfiguration.class, + ServiceConfiguration.class, + BaseAutoConfiguration.class, + AwaitableServerClientCallConfiguration.class, +}) +@ImportAutoConfiguration({ + GrpcClientMetricAutoConfiguration.class, + GrpcServerMetricAutoConfiguration.class, +}) +@DirtiesContext +class MetricCollectingInterceptorTest { + + private static final Empty EMPTY = Empty.getDefaultInstance(); + + @Autowired + private MeterRegistry meterRegistry; + + @GrpcClient("test") + private TestServiceBlockingStub testService; + + @GrpcClient("test") + private TestServiceStub testStreamService; + + /** + * Test successful call. + */ + @Test + @DirtiesContext + void testMetricsSuccessfulCall() { + log.info("--- Starting tests with successful call ---"); + CountDownLatch counter = awaitNextServerAndClientCallCloses(1); + + // Invoke 1 + assertEquals("1.2.3", this.testService.normal(EMPTY).getVersion()); + + assertTimeoutPreemptively(Duration.ofSeconds(1), (Executable) counter::await); + + // Test-Client 1 + final Counter requestSentCounter = + this.meterRegistry.find(METRIC_NAME_CLIENT_REQUESTS_SENT).counter(); + assertNotNull(requestSentCounter); + assertEquals(1, requestSentCounter.count()); + + final Counter responseReceivedCounter = + this.meterRegistry.find(METRIC_NAME_CLIENT_RESPONSES_RECEIVED).counter(); + assertNotNull(responseReceivedCounter); + assertEquals(1, responseReceivedCounter.count()); + + final Timer clientTimer = + this.meterRegistry.find(METRIC_NAME_CLIENT_PROCESSING_DURATION).timer(); + assertNotNull(clientTimer); + assertEquals(1, clientTimer.count()); + assertTrue(clientTimer.max(TimeUnit.SECONDS) < 1); + + // Test-Server 1 + for (final Meter meter : this.meterRegistry.find(METRIC_NAME_SERVER_REQUESTS_RECEIVED).counters()) { + log.debug("Found meter: {}", meter.getId()); + } + final Counter requestsReceivedCounter = this.meterRegistry + .find(METRIC_NAME_SERVER_REQUESTS_RECEIVED) + .tag(TAG_METHOD_NAME, "normal") + .counter(); + assertNotNull(requestsReceivedCounter); + assertEquals(1, requestsReceivedCounter.count()); + + final Counter responsesSentCounter = this.meterRegistry + .find(METRIC_NAME_SERVER_RESPONSES_SENT) + .tag(TAG_METHOD_NAME, "normal") + .counter(); + assertNotNull(responsesSentCounter); + assertEquals(1, responsesSentCounter.count()); + + final Timer serverTimer = this.meterRegistry + .find(METRIC_NAME_SERVER_PROCESSING_DURATION) + .tag(TAG_METHOD_NAME, "normal") + .timer(); + assertNotNull(serverTimer); + assertEquals(1, serverTimer.count()); + assertTrue(serverTimer.max(TimeUnit.SECONDS) < 1); + + // Client has network overhead so it has to be slower + assertTrue(serverTimer.max(TimeUnit.SECONDS) <= clientTimer.max(TimeUnit.SECONDS)); + + // -------------------------------------------------------------------- + + counter = awaitNextServerAndClientCallCloses(1); + + // Invoke 2 + assertEquals("1.2.3", this.testService.normal(EMPTY).getVersion()); + + assertTimeoutPreemptively(Duration.ofSeconds(1), (Executable) counter::await); + + // Test-Client 2 + assertEquals(2, requestSentCounter.count()); + assertEquals(2, responseReceivedCounter.count()); + assertEquals(2, clientTimer.count()); + assertTrue(clientTimer.max(TimeUnit.SECONDS) < 1); + + // Test-Server 2 + assertEquals(2, requestsReceivedCounter.count()); + assertEquals(2, responsesSentCounter.count()); + assertEquals(2, serverTimer.count()); + assertTrue(serverTimer.max(TimeUnit.SECONDS) < 1); + + // Client has network overhead so it has to be slower + assertTrue(serverTimer.max(TimeUnit.SECONDS) <= clientTimer.max(TimeUnit.SECONDS)); + log.info("--- Test completed ---"); + } + + /** + * Test early cancelled call. + */ + @Test + @DirtiesContext + void testMetricsEarlyCancelledCall() { + log.info("--- Starting tests with early cancelled call ---"); + final AtomicReference exception = new AtomicReference<>(); + final CountDownLatch counter = awaitNextServerAndClientCallCloses(1); + + // Invoke + final ClientCallStreamObserver observer = + (ClientCallStreamObserver) this.testStreamService.echo(new StreamObserver() { + + @Override + public void onNext(final SomeType value) { + try { + fail("Should never be here"); + } catch (final RuntimeException t) { + setError(t); + throw t; + } + } + + @Override + public void onError(final Throwable t) { + setError(t); + } + + @Override + public void onCompleted() { + try { + fail("Should never be here"); + } catch (final RuntimeException t) { + setError(t); + throw t; + } + } + + private synchronized void setError(final Throwable t) { + final Throwable previous = exception.get(); + if (previous == null) { + exception.set(t); + } else { + previous.addSuppressed(t); + } + } + + }); + + assertDoesNotThrow(() -> counter.await(1, TimeUnit.SECONDS)); + + observer.cancel("Cancelled", null); + assertTimeoutPreemptively(Duration.ofSeconds(3), (Executable) counter::await); + assertThat(exception.get()) + .isNotNull() + .isInstanceOfSatisfying(StatusRuntimeException.class, + t -> assertEquals(CANCELLED, t.getStatus().getCode())); + + // Test-Client + final Counter requestSentCounter = this.meterRegistry + .find(METRIC_NAME_CLIENT_REQUESTS_SENT) + .tag(MetricConstants.TAG_METHOD_NAME, "echo") + .counter(); + assertNotNull(requestSentCounter); + assertEquals(0, requestSentCounter.count()); + + final Counter responseReceivedCounter = this.meterRegistry + .find(METRIC_NAME_CLIENT_RESPONSES_RECEIVED) + .tag(MetricConstants.TAG_METHOD_NAME, "echo") + .counter(); + assertNotNull(responseReceivedCounter); + assertEquals(0, responseReceivedCounter.count()); + + final Timer clientTimer = this.meterRegistry + .find(METRIC_NAME_CLIENT_PROCESSING_DURATION) + .tag(MetricConstants.TAG_METHOD_NAME, "echo") + .tag(TAG_STATUS_CODE, CANCELLED.name()) + .timer(); + assertNotNull(clientTimer); + assertEquals(1, clientTimer.count()); + assertTrue(clientTimer.max(TimeUnit.SECONDS) < 3); + + // Test-Server + final Counter requestsReceivedCounter = this.meterRegistry + .find(METRIC_NAME_SERVER_REQUESTS_RECEIVED) + .tag(MetricConstants.TAG_METHOD_NAME, "echo") + .counter(); + assertNotNull(requestsReceivedCounter); + assertEquals(0, requestsReceivedCounter.count()); + + final Counter responsesSentCounter = this.meterRegistry + .find(METRIC_NAME_SERVER_RESPONSES_SENT) + .tag(MetricConstants.TAG_METHOD_NAME, "echo") + .counter(); + assertNotNull(responsesSentCounter); + assertEquals(0, responsesSentCounter.count()); + + final Timer serverTimer = this.meterRegistry + .find(METRIC_NAME_SERVER_PROCESSING_DURATION) + .tag(MetricConstants.TAG_METHOD_NAME, "echo") + .tag(TAG_STATUS_CODE, CANCELLED.name()) + .timer(); + assertNotNull(serverTimer); + assertEquals(1, serverTimer.count()); + assertTrue(serverTimer.max(TimeUnit.SECONDS) < 3); + + // Client has network overhead so it has to be slower + assertTrue(serverTimer.max(TimeUnit.SECONDS) <= clientTimer.max(TimeUnit.SECONDS)); + log.info("--- Test completed ---"); + } + + /** + * Test cancelled call. + */ + @Test + @DirtiesContext + void testMetricsCancelledCall() { + log.info("--- Starting tests with cancelled call ---"); + final AtomicReference exception = new AtomicReference<>(); + final CountDownLatch counter = awaitNextServerAndClientCallCloses(1); + + // Invoke + final ClientCallStreamObserver observer = + (ClientCallStreamObserver) this.testStreamService.echo(new StreamObserver() { + + @Override + public void onNext(final SomeType value) {} + + @Override + public void onError(final Throwable t) { + setError(t); + } + + @Override + public void onCompleted() { + try { + fail("Should never be here"); + } catch (final RuntimeException t) { + setError(t); + throw t; + } + } + + private synchronized void setError(final Throwable t) { + final Throwable previous = exception.get(); + if (previous == null) { + exception.set(t); + } else { + previous.addSuppressed(t); + } + } + + }); + + observer.onNext(SomeType.getDefaultInstance()); + assertDoesNotThrow(() -> counter.await(1, TimeUnit.SECONDS)); + + observer.cancel("Cancelled", null); + assertTimeoutPreemptively(Duration.ofSeconds(3), (Executable) counter::await); + assertThat(exception.get()) + .isNotNull() + .isInstanceOfSatisfying(StatusRuntimeException.class, + t -> assertEquals(CANCELLED, t.getStatus().getCode())); + + // Test-Client + final Counter requestSentCounter = this.meterRegistry + .find(METRIC_NAME_CLIENT_REQUESTS_SENT) + .tag(MetricConstants.TAG_METHOD_NAME, "echo") + .counter(); + assertNotNull(requestSentCounter); + assertEquals(1, requestSentCounter.count()); + + final Counter responseReceivedCounter = this.meterRegistry + .find(METRIC_NAME_CLIENT_RESPONSES_RECEIVED) + .tag(MetricConstants.TAG_METHOD_NAME, "echo") + .counter(); + assertNotNull(responseReceivedCounter); + assertEquals(1, responseReceivedCounter.count()); + + final Timer clientTimer = this.meterRegistry + .find(METRIC_NAME_CLIENT_PROCESSING_DURATION) + .tag(MetricConstants.TAG_METHOD_NAME, "echo") + .tag(TAG_STATUS_CODE, CANCELLED.name()) + .timer(); + assertNotNull(clientTimer); + assertEquals(1, clientTimer.count()); + assertTrue(clientTimer.max(TimeUnit.SECONDS) < 3); + + // Test-Server + final Counter requestsReceivedCounter = this.meterRegistry + .find(METRIC_NAME_SERVER_REQUESTS_RECEIVED) + .tag(MetricConstants.TAG_METHOD_NAME, "echo") + .counter(); + assertNotNull(requestsReceivedCounter); + assertEquals(1, requestsReceivedCounter.count()); + + final Counter responsesSentCounter = this.meterRegistry + .find(METRIC_NAME_SERVER_RESPONSES_SENT) + .tag(MetricConstants.TAG_METHOD_NAME, "echo") + .counter(); + assertNotNull(responsesSentCounter); + assertEquals(1, responsesSentCounter.count()); + + final Timer serverTimer = this.meterRegistry + .find(METRIC_NAME_SERVER_PROCESSING_DURATION) + .tag(MetricConstants.TAG_METHOD_NAME, "echo") + .tag(TAG_STATUS_CODE, CANCELLED.name()) + .timer(); + assertNotNull(serverTimer); + assertEquals(1, serverTimer.count()); + assertTrue(serverTimer.max(TimeUnit.SECONDS) < 3); + + // Client has network overhead so it has to be slower + assertTrue(serverTimer.max(TimeUnit.SECONDS) <= clientTimer.max(TimeUnit.SECONDS)); + log.info("--- Test completed ---"); + } + + /** + * Test unimplemented call. + */ + @Test + @DirtiesContext + void testMetricsUniplementedCall() { + log.info("--- Starting tests with unimplemented call ---"); + + final CountDownLatch counter = awaitNextServerAndClientCallCloses(1); + + // Invoke + assertThrows(StatusRuntimeException.class, + () -> this.testService.unimplemented(EMPTY)); + + assertTimeoutPreemptively(Duration.ofSeconds(1), (Executable) counter::await); + + // Test-Client + final Counter requestSentCounter = this.meterRegistry + .find(METRIC_NAME_CLIENT_REQUESTS_SENT) + .tag(MetricConstants.TAG_METHOD_NAME, "unimplemented") + .counter(); + assertNotNull(requestSentCounter); + assertEquals(1, requestSentCounter.count()); + + final Counter responseReceivedCounter = this.meterRegistry + .find(METRIC_NAME_CLIENT_RESPONSES_RECEIVED) + .tag(MetricConstants.TAG_METHOD_NAME, "unimplemented") + .counter(); + assertNotNull(responseReceivedCounter); + assertEquals(0, responseReceivedCounter.count()); + + final Timer clientTimer = this.meterRegistry + .find(METRIC_NAME_CLIENT_PROCESSING_DURATION) + .tag(MetricConstants.TAG_METHOD_NAME, "unimplemented") + .tag(TAG_STATUS_CODE, UNIMPLEMENTED.name()) + .timer(); + assertNotNull(clientTimer); + assertEquals(1, clientTimer.count()); + assertTrue(clientTimer.max(TimeUnit.SECONDS) < 1); + + // Test-Server + final Counter requestsReceivedCounter = this.meterRegistry + .find(METRIC_NAME_SERVER_REQUESTS_RECEIVED) + .tag(MetricConstants.TAG_METHOD_NAME, "unimplemented") + .counter(); + assertNotNull(requestsReceivedCounter); + assertEquals(1, requestsReceivedCounter.count()); + + final Counter responsesSentCounter = this.meterRegistry + .find(METRIC_NAME_SERVER_RESPONSES_SENT) + .tag(MetricConstants.TAG_METHOD_NAME, "unimplemented") + .counter(); + assertNotNull(responsesSentCounter); + assertEquals(0, responsesSentCounter.count()); + + final Timer serverTimer = this.meterRegistry + .find(METRIC_NAME_SERVER_PROCESSING_DURATION) + .tag(MetricConstants.TAG_METHOD_NAME, "unimplemented") + .tag(TAG_STATUS_CODE, UNIMPLEMENTED.name()) + .timer(); + assertNotNull(serverTimer); + assertEquals(1, serverTimer.count()); + assertTrue(serverTimer.max(TimeUnit.SECONDS) < 1); + + // Client has network overhead so it has to be slower + assertTrue(serverTimer.max(TimeUnit.SECONDS) <= clientTimer.max(TimeUnit.SECONDS)); + log.info("--- Test completed ---"); + } + + /** + * Test failed call. + */ + @Test + @DirtiesContext + void testMetricsFailedCall() { + log.info("--- Starting tests with failing call ---"); + + final CountDownLatch counter = awaitNextServerAndClientCallCloses(1); + + // Invoke + assertThrows(StatusRuntimeException.class, () -> this.testService.secure(EMPTY)); + + assertTimeoutPreemptively(Duration.ofSeconds(1), (Executable) counter::await); + + // Test-Client + final Counter requestSentCounter = this.meterRegistry + .find(METRIC_NAME_CLIENT_REQUESTS_SENT) + .tag(MetricConstants.TAG_METHOD_NAME, "secure") + .counter(); + assertNotNull(requestSentCounter); + assertEquals(1, requestSentCounter.count()); + + final Counter responseReceivedCounter = this.meterRegistry + .find(METRIC_NAME_CLIENT_RESPONSES_RECEIVED) + .tag(MetricConstants.TAG_METHOD_NAME, "secure") + .counter(); + assertNotNull(responseReceivedCounter); + assertEquals(0, responseReceivedCounter.count()); + + final Timer clientTimer = this.meterRegistry + .find(METRIC_NAME_CLIENT_PROCESSING_DURATION) + .tag(MetricConstants.TAG_METHOD_NAME, "secure") + .tag(TAG_STATUS_CODE, UNKNOWN.name()) + .timer(); + assertNotNull(clientTimer); + assertEquals(1, clientTimer.count()); + assertTrue(clientTimer.max(TimeUnit.SECONDS) < 1); + + // Test-Server + final Counter requestsReceivedCounter = this.meterRegistry + .find(METRIC_NAME_SERVER_REQUESTS_RECEIVED) + .tag(MetricConstants.TAG_METHOD_NAME, "secure") + .counter(); + assertNotNull(requestsReceivedCounter); + assertEquals(1, requestsReceivedCounter.count()); + + final Counter responsesSentCounter = this.meterRegistry + .find(METRIC_NAME_SERVER_RESPONSES_SENT) + .tag(MetricConstants.TAG_METHOD_NAME, "secure") + .counter(); + assertNotNull(responsesSentCounter); + assertEquals(0, responsesSentCounter.count()); + + final Timer serverTimer = this.meterRegistry + .find(METRIC_NAME_SERVER_PROCESSING_DURATION) + .tag(MetricConstants.TAG_METHOD_NAME, "secure") + .tag(TAG_STATUS_CODE, UNKNOWN.name()) + .timer(); + assertNotNull(serverTimer); + assertEquals(1, serverTimer.count()); + assertTrue(serverTimer.max(TimeUnit.SECONDS) < 1); + + // Client has network overhead so it has to be slower + assertTrue(serverTimer.max(TimeUnit.SECONDS) <= clientTimer.max(TimeUnit.SECONDS)); + log.info("--- Test completed ---"); + } + + /** + * Test error call. + */ + @Test + @DirtiesContext + void testMetricsErrorCall() { + log.info("--- Starting tests with error status call ---"); + + final CountDownLatch counter = awaitNextServerAndClientCallCloses(1); + + // Invoke + assertThrows(StatusRuntimeException.class, () -> this.testService.error(EMPTY)); + + assertTimeoutPreemptively(Duration.ofSeconds(1), (Executable) counter::await); + + // Test-Client + final Counter requestSentCounter = this.meterRegistry + .find(METRIC_NAME_CLIENT_REQUESTS_SENT) + .tag(MetricConstants.TAG_METHOD_NAME, "error") + .counter(); + assertNotNull(requestSentCounter); + assertEquals(1, requestSentCounter.count()); + + final Counter responseReceivedCounter = this.meterRegistry + .find(METRIC_NAME_CLIENT_RESPONSES_RECEIVED) + .tag(MetricConstants.TAG_METHOD_NAME, "error") + .counter(); + assertNotNull(responseReceivedCounter); + assertEquals(0, responseReceivedCounter.count()); + + final Timer clientTimer = this.meterRegistry + .find(METRIC_NAME_CLIENT_PROCESSING_DURATION) + .tag(MetricConstants.TAG_METHOD_NAME, "error") + .tag(TAG_STATUS_CODE, INTERNAL.name()) + .timer(); + assertNotNull(clientTimer); + assertEquals(1, clientTimer.count()); + assertTrue(clientTimer.max(TimeUnit.SECONDS) < 1); + + // Test-Server + final Counter requestsReceivedCounter = this.meterRegistry + .find(METRIC_NAME_SERVER_REQUESTS_RECEIVED) + .tag(MetricConstants.TAG_METHOD_NAME, "error") + .counter(); + assertNotNull(requestsReceivedCounter); + assertEquals(1, requestsReceivedCounter.count()); + + final Counter responsesSentCounter = this.meterRegistry + .find(METRIC_NAME_SERVER_RESPONSES_SENT) + .tag(MetricConstants.TAG_METHOD_NAME, "error") + .counter(); + assertNotNull(responsesSentCounter); + assertEquals(0, responsesSentCounter.count()); + + final Timer serverTimer = this.meterRegistry + .find(METRIC_NAME_SERVER_PROCESSING_DURATION) + .tag(MetricConstants.TAG_METHOD_NAME, "error") + .tag(TAG_STATUS_CODE, INTERNAL.name()) + .timer(); + assertNotNull(serverTimer); + assertEquals(1, serverTimer.count()); + assertTrue(serverTimer.max(TimeUnit.SECONDS) < 1); + + // Client has network overhead so it has to be slower + assertTrue(serverTimer.max(TimeUnit.SECONDS) <= clientTimer.max(TimeUnit.SECONDS)); + log.info("--- Test completed ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricCollectingServerInterceptorTest.java b/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricCollectingServerInterceptorTest.java new file mode 100644 index 000000000..a00d8e842 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricCollectingServerInterceptorTest.java @@ -0,0 +1,79 @@ +/* + * 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.metric; + +import static io.grpc.Status.Code.OK; +import static io.grpc.Status.Code.UNKNOWN; +import static net.devh.boot.grpc.common.metric.MetricConstants.METRIC_NAME_SERVER_PROCESSING_DURATION; +import static net.devh.boot.grpc.common.metric.MetricConstants.METRIC_NAME_SERVER_REQUESTS_RECEIVED; +import static net.devh.boot.grpc.test.server.TestServiceImpl.METHOD_COUNT; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; + +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.metric.MetricCollectingServerInterceptor; +import net.devh.boot.grpc.test.proto.TestServiceGrpc; +import net.devh.boot.grpc.test.server.TestServiceImpl; + +@Slf4j +class MetricCollectingServerInterceptorTest { + + @Test + void testServerPreRegistration() { + log.info("--- Starting tests with server pre-registration ---"); + final MeterRegistry meterRegistry = new SimpleMeterRegistry(); + assertEquals(0, meterRegistry.getMeters().size()); + final MetricCollectingServerInterceptor mcsi = new MetricCollectingServerInterceptor(meterRegistry); + mcsi.preregisterService(TestServiceGrpc.getServiceDescriptor()); + + MetricTestHelper.logMeters(meterRegistry.getMeters()); + assertEquals(METHOD_COUNT * 3, meterRegistry.getMeters().size()); + log.info("--- Test completed ---"); + } + + @Test + void testServerCustomization() { + log.info("--- Starting tests with server customization ---"); + final MeterRegistry meterRegistry = new SimpleMeterRegistry(); + assertEquals(0, meterRegistry.getMeters().size()); + final MetricCollectingServerInterceptor mcsi = new MetricCollectingServerInterceptor(meterRegistry, + counter -> counter.tag("type", "counter"), + timer -> timer.tag("type", "timer").publishPercentiles(0.5, 0.9, 0.99), + OK, UNKNOWN); + mcsi.preregisterService(new TestServiceImpl()); + + MetricTestHelper.logMeters(meterRegistry.getMeters()); + assertEquals(METHOD_COUNT * 10, meterRegistry.getMeters().size()); + + final Counter counter = meterRegistry.find(METRIC_NAME_SERVER_REQUESTS_RECEIVED).counter(); + assertNotNull(counter); + assertEquals("counter", counter.getId().getTag("type")); + + final Timer timer = meterRegistry.find(METRIC_NAME_SERVER_PROCESSING_DURATION).timer(); + assertNotNull(timer); + assertEquals("timer", timer.getId().getTag("type")); + log.info("--- Test completed ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricCustomAutoConfigurationTest.java b/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricCustomAutoConfigurationTest.java new file mode 100644 index 000000000..e8310b58d --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricCustomAutoConfigurationTest.java @@ -0,0 +1,95 @@ +/* + * 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.metric; + +import static io.grpc.Status.Code.OK; +import static io.grpc.Status.Code.UNKNOWN; +import static net.devh.boot.grpc.test.server.TestServiceImpl.METHOD_COUNT; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Collection; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +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.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import io.grpc.BindableService; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.autoconfigure.GrpcServerMetricAutoConfiguration; +import net.devh.boot.grpc.server.metric.MetricCollectingServerInterceptor; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.MetricConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.metric.MetricCustomAutoConfigurationTest.CustomConfiguration; + +/** + * A test to verify that the server auto configuration works. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest +@SpringJUnitConfig(classes = {MetricConfiguration.class, CustomConfiguration.class, ServiceConfiguration.class, + BaseAutoConfiguration.class}) +@ImportAutoConfiguration(GrpcServerMetricAutoConfiguration.class) +@DirtiesContext +public class MetricCustomAutoConfigurationTest { + + @Autowired + private MeterRegistry meterRegistry; + + @Test + @DirtiesContext + public void testAutoDiscovery() { + log.info("--- Starting tests with custom auto discovery ---"); + assertEquals(METHOD_COUNT * 2, + this.meterRegistry.getMeters().stream().filter(Counter.class::isInstance).count()); + assertEquals(METHOD_COUNT * 2, + this.meterRegistry.getMeters().stream().filter(Timer.class::isInstance).count()); + log.info("--- Test completed ---"); + } + + @Configuration + public static class CustomConfiguration { + + @Bean + public MetricCollectingServerInterceptor metricCollectingServerInterceptor(final MeterRegistry registry, + final Collection services) { + final MetricCollectingServerInterceptor metricCollector = new MetricCollectingServerInterceptor(registry, + counter -> counter.tag("type", "counter"), + timer -> timer.tag("type", "timer").publishPercentiles(0.5, 0.9, 0.99), + OK, UNKNOWN); + log.debug("Pre-Registering custom service metrics"); + for (final BindableService service : services) { + log.debug("- {}", service); + metricCollector.preregisterService(service); + } + return metricCollector; + } + + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricFullAutoConfigurationTest.java b/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricFullAutoConfigurationTest.java new file mode 100644 index 000000000..f0b3da943 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricFullAutoConfigurationTest.java @@ -0,0 +1,76 @@ +/* + * 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.metric; + +import static net.devh.boot.grpc.test.server.TestServiceImpl.METHOD_COUNT; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import io.grpc.health.v1.HealthGrpc; +import io.grpc.reflection.v1alpha.ServerReflectionGrpc; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Timer; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.test.config.ServiceConfiguration; + +/** + * A test to verify that the server auto configuration works. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest +@SpringJUnitConfig(classes = ServiceConfiguration.class) +@EnableAutoConfiguration +@DirtiesContext +class MetricFullAutoConfigurationTest { + + @Autowired + private MeterRegistry meterRegistry; + + private static final int HEALTH_SERVICE_METHOD_COUNT = + HealthGrpc.getServiceDescriptor().getMethods().size(); + private static final int REFLECTION_SERVICE_METHOD_COUNT = + ServerReflectionGrpc.getServiceDescriptor().getMethods().size(); + private static final int TOTAL_METHOD_COUNT = + HEALTH_SERVICE_METHOD_COUNT + REFLECTION_SERVICE_METHOD_COUNT + METHOD_COUNT; + + @Test + @DirtiesContext + void testAutoDiscovery() { + log.info("--- Starting tests with full auto discovery ---"); + MetricTestHelper.logMeters(this.meterRegistry.getMeters()); + assertEquals(TOTAL_METHOD_COUNT * 2, this.meterRegistry.getMeters().stream() + .filter(Counter.class::isInstance) + .filter(m -> m.getId().getName().startsWith("grpc.")) // Only count grpc metrics + .count()); + assertEquals(TOTAL_METHOD_COUNT, this.meterRegistry.getMeters().stream() + .filter(Timer.class::isInstance) + .filter(m -> m.getId().getName().startsWith("grpc.")) // Only count grpc metrics + .count()); + log.info("--- Test completed ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricTestHelper.java b/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricTestHelper.java new file mode 100644 index 000000000..ec52b866b --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/metric/MetricTestHelper.java @@ -0,0 +1,93 @@ +/* + * 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.metric; + +import static java.util.Comparator.comparing; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.Meter.Id; +import io.micrometer.core.instrument.Tag; +import lombok.extern.slf4j.Slf4j; + +/** + * A class with helper methods related to testing metrics. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +public final class MetricTestHelper { + + private static final Comparator METER_ID_NAME_COMPARATOR = comparing(Id::getName); + private static final Comparator METER_ID_TYPE_COMPARATOR = comparing(Id::getType, comparing(Enum::name)); + private static final Comparator METER_TAG_COMPARATOR = + comparing(Tag::getKey).thenComparing(comparing(Tag::getValue)); + private static final Comparator> METER_TAGS_COMPARATOR = (l, r) -> { + final Iterator lit = l.iterator(); + final Iterator rit = r.iterator(); + while (lit.hasNext() && rit.hasNext()) { + final Tag lTag = lit.next(); + final Tag rTag = rit.next(); + final int result = METER_TAG_COMPARATOR.compare(lTag, rTag); + if (result != 0) { + return result; + } + } + return l.size() - r.size(); + }; + private static final Comparator METER_ID_TAGS_COMPARATOR = comparing(Id::getTags, METER_TAGS_COMPARATOR); + private static final Comparator METER_ID_COMPARATOR = METER_ID_TYPE_COMPARATOR + .thenComparing(METER_ID_NAME_COMPARATOR) + .thenComparing(METER_ID_TAGS_COMPARATOR); + private static final Comparator METER_COMPARATOR = comparing(Meter::getId, METER_ID_COMPARATOR); + + /** + * Logs a sorted and readable list of meters using the debug level. Useful for debugging. + * + * @param meters The meters to be logged. + */ + public static void logMeters(final Collection meters) { + if (!log.isDebugEnabled()) { + return; + } + // The original collection is usually unmodifiable + final List sortedMeters = new ArrayList<>(meters); + Collections.sort(sortedMeters, METER_COMPARATOR); + + log.debug("Found meters:"); + for (final Meter meter : sortedMeters) { + final Id id = meter.getId(); + final String type = id.getType().name(); + final String name = id.getName(); + final Map tagMap = new LinkedHashMap<>(); // Tags are already sorted + for (final Tag tag : id.getTags()) { + tagMap.put(tag.getKey(), tag.getValue()); + } + log.debug("- {} {} {}", type, name, tagMap); + } + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/scope/GrpcRequestScopeTest.java b/tests/src/test/java/net/devh/boot/grpc/test/scope/GrpcRequestScopeTest.java new file mode 100644 index 000000000..d7052cc51 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/scope/GrpcRequestScopeTest.java @@ -0,0 +1,147 @@ +/* + * 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.scope; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; +import org.opentest4j.AssertionFailedError; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import io.grpc.stub.StreamObserver; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.InProcessConfiguration; +import net.devh.boot.grpc.test.config.ScopedServiceConfiguration; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceStub; + +@Slf4j +@SpringBootTest +@SpringJUnitConfig( + classes = {InProcessConfiguration.class, ScopedServiceConfiguration.class, BaseAutoConfiguration.class}) +@DirtiesContext +public class GrpcRequestScopeTest { + + @GrpcClient("test") + protected TestServiceStub testServiceStub; + + @Test + @DirtiesContext + public void testScope() throws InterruptedException { + // Prepare + ScopedStreamObserverChecker scope1 = new ScopedStreamObserverChecker(); + StreamObserver request1 = this.testServiceStub.secureBidi(scope1); + ScopedStreamObserverChecker scope2 = new ScopedStreamObserverChecker(); + StreamObserver request2 = this.testServiceStub.secureBidi(scope2); + + // Run + request1.onNext(SomeType.getDefaultInstance()); + request1.onNext(SomeType.getDefaultInstance()); + Thread.sleep(100); + + request2.onNext(SomeType.getDefaultInstance()); + request2.onNext(SomeType.getDefaultInstance()); + Thread.sleep(100); + + request1.onNext(SomeType.getDefaultInstance()); + request2.onNext(SomeType.getDefaultInstance()); + Thread.sleep(100); + + request2.onNext(SomeType.getDefaultInstance()); + request1.onNext(SomeType.getDefaultInstance()); + Thread.sleep(100); + + request1.onCompleted(); + request2.onCompleted(); + Thread.sleep(100); + + // Assert + assertTrue(scope1.isCompleted()); + assertTrue(scope2.isCompleted()); + assertNull(scope1.getError()); + assertNull(scope2.getError()); + assertNotNull(scope1.getText()); + assertNotNull(scope2.getText()); + assertNotEquals(scope1.getText(), scope2.getText()); + log.debug("A: {} - B: {}", scope1.getText(), scope2.getText()); + } + + /** + * Helper class used to check that the scoped responses are different per request, but the same for different + * messages in the same request. + */ + private static class ScopedStreamObserverChecker implements StreamObserver { + + private String text; + private boolean completed = false; + private Throwable error; + + @Override + public void onNext(SomeType value) { + if (this.text == null) { + this.text = value.getVersion(); + } + try { + assertEquals(this.text, value.getVersion()); + } catch (AssertionFailedError e) { + if (this.error == null) { + this.error = e; + } else { + this.error.addSuppressed(e); + } + } + } + + @Override + public void onError(Throwable t) { + if (this.error == null) { + this.error = t; + } else { + this.error.addSuppressed(t); + } + this.completed = true; + } + + @Override + public void onCompleted() { + this.completed = true; + } + + public String getText() { + return this.text; + } + + public boolean isCompleted() { + return this.completed; + } + + public Throwable getError() { + return this.error; + } + + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/security/AbstractSecurityTest.java b/tests/src/test/java/net/devh/boot/grpc/test/security/AbstractSecurityTest.java new file mode 100644 index 000000000..68e759cfc --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/security/AbstractSecurityTest.java @@ -0,0 +1,262 @@ +/* + * 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.security; + +import static io.grpc.Status.Code.PERMISSION_DENIED; +import static java.util.concurrent.TimeUnit.SECONDS; +import static net.devh.boot.grpc.test.util.FutureAssertions.assertFutureEquals; +import static net.devh.boot.grpc.test.util.GrpcAssertions.assertFutureFirstEquals; +import static net.devh.boot.grpc.test.util.GrpcAssertions.assertFutureThrowsStatus; +import static net.devh.boot.grpc.test.util.GrpcAssertions.assertThrowsStatus; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.TimeUnit; +import java.util.function.BiFunction; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.springframework.test.annotation.DirtiesContext; + +import com.google.common.util.concurrent.ListenableFuture; +import com.google.protobuf.Empty; + +import io.grpc.Status.Code; +import io.grpc.internal.testing.StreamRecorder; +import io.grpc.stub.StreamObserver; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceBlockingStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceFutureStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceStub; +import net.devh.boot.grpc.test.util.DynamicTestCollection; +import net.devh.boot.grpc.test.util.TriConsumer; + +public abstract class AbstractSecurityTest { + + protected static final Empty EMPTY = Empty.getDefaultInstance(); + + @GrpcClient("test") + protected TestServiceStub serviceStub; + @GrpcClient("test") + protected TestServiceBlockingStub blockingStub; + @GrpcClient("test") + protected TestServiceFutureStub futureStub; + + @GrpcClient("noPerm") + protected TestServiceStub noPermStub; + @GrpcClient("noPerm") + protected TestServiceBlockingStub noPermBlockingStub; + @GrpcClient("noPerm") + protected TestServiceFutureStub noPermFutureStub; + + /** + * Tests for with unprotected methods. + * + * @return The tests. + */ + @Test + @DirtiesContext + @TestFactory + public DynamicTestCollection unprotectedCallTests() { + return DynamicTestCollection.create() + .add("unprotected-default", + () -> assertNormalCallSuccess(this.serviceStub, this.blockingStub, this.futureStub)) + .add("unprotected-noPerm", + () -> assertNormalCallSuccess(this.noPermStub, this.noPermBlockingStub, this.noPermFutureStub)); + } + + protected void assertNormalCallSuccess(final TestServiceStub serviceStub, + final TestServiceBlockingStub blockingStub, + final TestServiceFutureStub futureStub) { + assertUnarySuccessfulMethod(serviceStub, + TestServiceStub::normal, blockingStub, + TestServiceBlockingStub::normal, futureStub, + TestServiceFutureStub::normal); + } + + protected void assertNormalCallFailure(final TestServiceStub serviceStub, + final TestServiceBlockingStub blockingStub, + final TestServiceFutureStub futureStub, + final Code expectedCode) { + assertUnaryFailingMethod(serviceStub, + TestServiceStub::normal, blockingStub, + TestServiceBlockingStub::normal, futureStub, + TestServiceFutureStub::normal, expectedCode); + } + + /** + * Tests with unary call. + * + * @return The tests. + */ + @Test + @DirtiesContext + @TestFactory + public DynamicTestCollection unaryCallTest() { + return DynamicTestCollection.create() + .add("unary-default", + () -> assertUnaryCallSuccess(this.serviceStub, this.blockingStub, this.futureStub)) + .add("unary-noPerm", + () -> assertUnaryCallFailure(this.noPermStub, this.noPermBlockingStub, this.noPermFutureStub, + PERMISSION_DENIED)); + } + + protected void assertUnaryCallSuccess(final TestServiceStub serviceStub, + final TestServiceBlockingStub blockingStub, + final TestServiceFutureStub futureStub) { + assertUnarySuccessfulMethod(serviceStub, + TestServiceStub::secure, blockingStub, + TestServiceBlockingStub::secure, futureStub, + TestServiceFutureStub::secure); + } + + protected void assertUnaryCallFailure(final TestServiceStub serviceStub, + final TestServiceBlockingStub blockingStub, + final TestServiceFutureStub futureStub, + final Code expectedCode) { + assertUnaryFailingMethod(serviceStub, + TestServiceStub::secure, blockingStub, + TestServiceBlockingStub::secure, futureStub, + TestServiceFutureStub::secure, expectedCode); + } + + /** + * Tests with client streaming call. + * + * @return The tests. + */ + @Test + @DirtiesContext + @TestFactory + public DynamicTestCollection clientStreamingCallTests() { + return DynamicTestCollection.create() + .add("clientStreaming-default", () -> assertClientStreamingCallSuccess(this.serviceStub)) + .add("clientStreaming-noPerm", + () -> assertClientStreamingCallFailure(this.noPermStub, PERMISSION_DENIED)); + } + + protected void assertClientStreamingCallSuccess(final TestServiceStub serviceStub) { + final StreamRecorder responseRecorder = StreamRecorder.create(); + final StreamObserver requestObserver = serviceStub.secureDrain(responseRecorder); + sendAndComplete(requestObserver, "1.2.3"); + assertFutureFirstEquals(EMPTY, responseRecorder, 15, TimeUnit.SECONDS); + } + + protected void assertClientStreamingCallFailure(final TestServiceStub serviceStub, final Code expectedCode) { + final StreamRecorder responseRecorder = StreamRecorder.create(); + final StreamObserver requestObserver = serviceStub.secureDrain(responseRecorder); + // Let the server throw an exception if he receives that (assert security): + sendAndComplete(requestObserver, "explode"); + assertFutureThrowsStatus(expectedCode, responseRecorder, 15, SECONDS); + } + + /** + * Tests with server streaming call. + * + * @return The tests. + */ + @Test + @DirtiesContext + public DynamicTestCollection serverStreamingCallTests() { + return DynamicTestCollection.create() + .add("serverStreaming-default", + () -> assertServerStreamingCallSuccess(this.serviceStub)) + .add("serverStreaming-noPerm", + () -> assertServerStreamingCallFailure(this.noPermStub, PERMISSION_DENIED)); + } + + protected void assertServerStreamingCallSuccess(final TestServiceStub testStub) { + final StreamRecorder responseRecorder = StreamRecorder.create(); + testStub.secureSupply(EMPTY, responseRecorder); + assertFutureFirstEquals("1.2.3", responseRecorder, SomeType::getVersion, 5, SECONDS); + } + + protected void assertServerStreamingCallFailure(final TestServiceStub serviceStub, final Code expectedCode) { + final StreamRecorder streamRecorder = StreamRecorder.create(); + serviceStub.secureSupply(EMPTY, streamRecorder); + assertFutureThrowsStatus(expectedCode, streamRecorder, 15, SECONDS); + } + + /** + * Tests with bidirectional streaming call. + * + * @return The tests. + */ + @Test + @DirtiesContext + public DynamicTestCollection bidiStreamingCallTests() { + return DynamicTestCollection.create() + .add("bidiStreaming-default", () -> assertBidiCallSuccess(this.serviceStub)) + .add("bidiStreaming-noPerm", () -> assertBidiCallFailure(this.noPermStub, PERMISSION_DENIED)); + } + + protected void assertBidiCallSuccess(final TestServiceStub testStub) { + final StreamRecorder responseRecorder = StreamRecorder.create(); + final StreamObserver requestObserver = testStub.secureBidi(responseRecorder); + sendAndComplete(requestObserver, "1.2.3"); + assertFutureFirstEquals("1.2.3", responseRecorder, SomeType::getVersion, 5, SECONDS); + } + + protected void assertBidiCallFailure(final TestServiceStub serviceStub, final Code expectedCode) { + final StreamRecorder responseRecorder = StreamRecorder.create(); + final StreamObserver requestObserver = serviceStub.secureBidi(responseRecorder); + sendAndComplete(requestObserver, "explode"); + assertFutureThrowsStatus(expectedCode, responseRecorder, 15, SECONDS); + } + + // ------------------------------------- + + protected void assertUnarySuccessfulMethod(final TestServiceStub serviceStub, + final TriConsumer> serviceCall, + final TestServiceBlockingStub blockingStub, + final BiFunction blockingcall, + final TestServiceFutureStub futureStub, + final BiFunction> futureCall) { + + final StreamRecorder responseRecorder = StreamRecorder.create(); + serviceCall.accept(serviceStub, EMPTY, responseRecorder); + assertFutureFirstEquals("1.2.3", responseRecorder, SomeType::getVersion, 5, SECONDS); + + assertEquals("1.2.3", blockingcall.apply(blockingStub, EMPTY).getVersion()); + assertFutureEquals("1.2.3", futureCall.apply(futureStub, EMPTY), SomeType::getVersion, 5, SECONDS); + } + + protected void assertUnaryFailingMethod(final TestServiceStub serviceStub, + final TriConsumer> serviceCall, + final TestServiceBlockingStub blockingStub, + final BiFunction blockingcall, + final TestServiceFutureStub futureStub, + final BiFunction> futureCall, + final Code expectedCode) { + + final StreamRecorder responseRecorder = StreamRecorder.create(); + serviceCall.accept(serviceStub, EMPTY, responseRecorder); + assertFutureThrowsStatus(expectedCode, responseRecorder, 5, SECONDS); + + assertThrowsStatus(expectedCode, () -> blockingcall.apply(blockingStub, EMPTY)); + assertFutureThrowsStatus(expectedCode, futureCall.apply(futureStub, EMPTY), 5, SECONDS); + } + + protected void sendAndComplete(final StreamObserver requestObserver, final String message) { + requestObserver.onNext(SomeType.newBuilder().setVersion(message).build()); + requestObserver.onNext(SomeType.newBuilder().setVersion(message).build()); + requestObserver.onNext(SomeType.newBuilder().setVersion(message).build()); + requestObserver.onCompleted(); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/security/AbstractSecurityWithBasicAuthTest.java b/tests/src/test/java/net/devh/boot/grpc/test/security/AbstractSecurityWithBasicAuthTest.java new file mode 100644 index 000000000..6597e2869 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/security/AbstractSecurityWithBasicAuthTest.java @@ -0,0 +1,111 @@ +/* + * 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.security; + +import static io.grpc.Status.Code.UNAUTHENTICATED; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.springframework.test.annotation.DirtiesContext; + +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceBlockingStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceFutureStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceStub; +import net.devh.boot.grpc.test.util.DynamicTestCollection; + +public abstract class AbstractSecurityWithBasicAuthTest extends AbstractSecurityTest { + + @GrpcClient("unknownUser") + protected TestServiceStub unknownUserStub; + @GrpcClient("unknownUser") + protected TestServiceBlockingStub unknownUserBlockingStub; + @GrpcClient("unknownUser") + protected TestServiceFutureStub unknownUserFutureStub; + + @GrpcClient("noAuth") + protected TestServiceStub noAuthStub; + @GrpcClient("noAuth") + protected TestServiceBlockingStub noAuthBlockingStub; + @GrpcClient("noAuth") + protected TestServiceFutureStub noAuthFutureStub; + + @Override + @Test + @DirtiesContext + @TestFactory + public DynamicTestCollection unprotectedCallTests() { + return super.unprotectedCallTests() + .add("unprotected-unknownUser", + () -> assertNormalCallFailure(this.unknownUserStub, this.unknownUserBlockingStub, + this.unknownUserFutureStub, UNAUTHENTICATED)) + .add("unprotected-noAuth", + () -> assertNormalCallSuccess(this.noAuthStub, this.noAuthBlockingStub, this.noAuthFutureStub)); + } + + @Override + @Test + @DirtiesContext + @TestFactory + public DynamicTestCollection unaryCallTest() { + return super.unaryCallTest() + .add("unary-unknownUser", + () -> assertUnaryCallFailure(this.unknownUserStub, this.unknownUserBlockingStub, + this.unknownUserFutureStub, UNAUTHENTICATED)) + .add("unary-noAuth", + () -> assertUnaryCallFailure(this.noAuthStub, this.noAuthBlockingStub, this.noAuthFutureStub, + UNAUTHENTICATED)); + } + + @Override + @Test + @DirtiesContext + @TestFactory + public DynamicTestCollection clientStreamingCallTests() { + return super.clientStreamingCallTests() + .add("clientStreaming-unknownUser", + () -> assertClientStreamingCallFailure(this.unknownUserStub, UNAUTHENTICATED)) + .add("clientStreaming-noAuth", + () -> assertClientStreamingCallFailure(this.noAuthStub, UNAUTHENTICATED)); + } + + @Override + @Test + @DirtiesContext + @TestFactory + public DynamicTestCollection serverStreamingCallTests() { + return super.serverStreamingCallTests() + .add("serverStreaming-unknownUser", + () -> assertServerStreamingCallFailure(this.unknownUserStub, UNAUTHENTICATED)) + .add("serverStreaming-noAuth", + () -> assertServerStreamingCallFailure(this.noAuthStub, UNAUTHENTICATED)); + } + + @Override + @Test + @DirtiesContext + @TestFactory + public DynamicTestCollection bidiStreamingCallTests() { + return super.bidiStreamingCallTests() + .add("bidiStreaming-unknownUser", + () -> assertServerStreamingCallFailure(this.unknownUserStub, UNAUTHENTICATED)) + .add("bidiStreaming-noAuth", + () -> assertServerStreamingCallFailure(this.noAuthStub, UNAUTHENTICATED)); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/security/AnnotatedSecurityWithBasicAuthTest.java b/tests/src/test/java/net/devh/boot/grpc/test/security/AnnotatedSecurityWithBasicAuthTest.java new file mode 100644 index 000000000..4a4f04903 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/security/AnnotatedSecurityWithBasicAuthTest.java @@ -0,0 +1,48 @@ +/* + * 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.security; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.test.config.AnnotatedSecurityConfiguration; +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.config.WithBasicAuthSecurityConfiguration; + +/** + * A test checking that the server and client can start and connect to each other with minimal config. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest +@SpringJUnitConfig( + classes = {ServiceConfiguration.class, InProcessConfiguration.class, BaseAutoConfiguration.class, + AnnotatedSecurityConfiguration.class, WithBasicAuthSecurityConfiguration.class}) +@DirtiesContext +public class AnnotatedSecurityWithBasicAuthTest extends AbstractSecurityWithBasicAuthTest { + + public AnnotatedSecurityWithBasicAuthTest() { + log.info("--- AnnotatedSecurityWithBasicAuthConfiguration ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/security/AnnotatedSecurityWithCertificateTest.java b/tests/src/test/java/net/devh/boot/grpc/test/security/AnnotatedSecurityWithCertificateTest.java new file mode 100644 index 000000000..653a1a394 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/security/AnnotatedSecurityWithCertificateTest.java @@ -0,0 +1,59 @@ +/* + * 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.security; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.test.config.AnnotatedSecurityConfiguration; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.config.WithCertificateSecurityConfiguration; + +@Slf4j +@SpringBootTest(properties = { + "grpc.server.security.enabled=true", + "grpc.server.security.certificateChain=file:src/test/resources/certificates/server.crt", + "grpc.server.security.privateKey=file:src/test/resources/certificates/server.key", + "grpc.server.security.trustCertCollection=file:src/test/resources/certificates/trusted-clients-collection", + "grpc.server.security.clientAuth=REQUIRE", + + "grpc.client.GLOBAL.address=localhost:9090", + "grpc.client.GLOBAL.security.authorityOverride=localhost", + "grpc.client.GLOBAL.security.trustCertCollection=file:src/test/resources/certificates/trusted-servers-collection", + "grpc.client.GLOBAL.security.clientAuthEnabled=true", + + "grpc.client.test.security.certificateChain=file:src/test/resources/certificates/client1.crt", + "grpc.client.test.security.privateKey=file:src/test/resources/certificates/client1.key", + + "grpc.client.noPerm.security.certificateChain=file:src/test/resources/certificates/client2.crt", + "grpc.client.noPerm.security.privateKey=file:src/test/resources/certificates/client2.key" +}) +@SpringJUnitConfig( + classes = {ServiceConfiguration.class, BaseAutoConfiguration.class, AnnotatedSecurityConfiguration.class, + WithCertificateSecurityConfiguration.class}) +@DirtiesContext +public class AnnotatedSecurityWithCertificateTest extends AbstractSecurityTest { + + public AnnotatedSecurityWithCertificateTest() { + log.info("--- AnnotatedSecurityWithCertificateTest ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/security/ConcurrentSecurityTest.java b/tests/src/test/java/net/devh/boot/grpc/test/security/ConcurrentSecurityTest.java new file mode 100644 index 000000000..0b0552d4e --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/security/ConcurrentSecurityTest.java @@ -0,0 +1,105 @@ +/* + * 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.security; + +import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.google.protobuf.Empty; + +import io.grpc.Status.Code; +import io.grpc.internal.testing.StreamRecorder; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.server.serverfactory.GrpcServerLifecycle; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.InProcessConfiguration; +import net.devh.boot.grpc.test.config.ManualSecurityConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.config.WithBasicAuthSecurityConfiguration; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceStub; +import net.devh.boot.grpc.test.util.GrpcAssertions; + +/** + * Test that ensures that the security also works in concurrent environments. This seems to be a common problem in many + * security examples. This test can also be used to simulate heavy load on the server, you just have to increase the + * {@code parallelCount} drastically. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest +@SpringJUnitConfig( + classes = {ServiceConfiguration.class, InProcessConfiguration.class, BaseAutoConfiguration.class, + ManualSecurityConfiguration.class, WithBasicAuthSecurityConfiguration.class}) +@DirtiesContext +public class ConcurrentSecurityTest { + + @Autowired + private GrpcServerLifecycle serverLifecycle; + + @GrpcClient("test") + protected TestServiceStub testServiceStub; + + @GrpcClient("noPerm") + protected TestServiceStub brokenTestServiceStub; + + /** + * Test secured call. + * + * @throws Throwable Should never happen. + */ + @Test + @DirtiesContext + public void testSecuredCall() throws Throwable { + assertTrue("Server should be running", this.serverLifecycle.isRunning()); + final int parallelCount = 10; // Limited for automated tests, increase for in depth tests + log.info("--- Starting tests with secured call ---"); + final List runnables = new ArrayList<>(); + for (int i = 0; i < parallelCount; i++) { + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.testServiceStub.secure(Empty.getDefaultInstance(), streamRecorder); + runnables.add(() -> assertEquals("1.2.3", streamRecorder.firstValue().get().getVersion())); + } + for (int i = 0; i < parallelCount; i++) { + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.brokenTestServiceStub.secure(Empty.getDefaultInstance(), streamRecorder); + runnables.add(() -> GrpcAssertions.assertFutureThrowsStatus(Code.PERMISSION_DENIED, + streamRecorder.firstValue(), 15, TimeUnit.SECONDS)); + } + Collections.shuffle(runnables); + for (final Executable executable : runnables) { + executable.execute(); + } + log.info("--- Test completed ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/security/ManualSecurityWithBasicAuthTest.java b/tests/src/test/java/net/devh/boot/grpc/test/security/ManualSecurityWithBasicAuthTest.java new file mode 100644 index 000000000..c0b3788e6 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/security/ManualSecurityWithBasicAuthTest.java @@ -0,0 +1,48 @@ +/* + * 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.security; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.InProcessConfiguration; +import net.devh.boot.grpc.test.config.ManualSecurityConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.config.WithBasicAuthSecurityConfiguration; + +/** + * A test checking that the server and client can start and connect to each other with minimal config. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest +@SpringJUnitConfig( + classes = {ServiceConfiguration.class, InProcessConfiguration.class, BaseAutoConfiguration.class, + ManualSecurityConfiguration.class, WithBasicAuthSecurityConfiguration.class}) +@DirtiesContext +public class ManualSecurityWithBasicAuthTest extends AbstractSecurityWithBasicAuthTest { + + public ManualSecurityWithBasicAuthTest() { + log.info("--- ManualSecurityWithBasicAuthTest ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/security/ManualSecurityWithCertificateTest.java b/tests/src/test/java/net/devh/boot/grpc/test/security/ManualSecurityWithCertificateTest.java new file mode 100644 index 000000000..73d183422 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/security/ManualSecurityWithCertificateTest.java @@ -0,0 +1,59 @@ +/* + * 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.security; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ManualSecurityConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.config.WithCertificateSecurityConfiguration; + +@Slf4j +@SpringBootTest(properties = { + "grpc.server.security.enabled=true", + "grpc.server.security.certificateChain=file:src/test/resources/certificates/server.crt", + "grpc.server.security.privateKey=file:src/test/resources/certificates/server.key", + "grpc.server.security.trustCertCollection=file:src/test/resources/certificates/trusted-clients-collection", + "grpc.server.security.clientAuth=REQUIRE", + + "grpc.client.GLOBAL.address=localhost:9090", + "grpc.client.GLOBAL.security.authorityOverride=localhost", + "grpc.client.GLOBAL.security.trustCertCollection=file:src/test/resources/certificates/trusted-servers-collection", + "grpc.client.GLOBAL.security.clientAuthEnabled=true", + + "grpc.client.test.security.certificateChain=file:src/test/resources/certificates/client1.crt", + "grpc.client.test.security.privateKey=file:src/test/resources/certificates/client1.key", + + "grpc.client.noPerm.security.certificateChain=file:src/test/resources/certificates/client2.crt", + "grpc.client.noPerm.security.privateKey=file:src/test/resources/certificates/client2.key" +}) +@SpringJUnitConfig( + classes = {ServiceConfiguration.class, BaseAutoConfiguration.class, ManualSecurityConfiguration.class, + WithCertificateSecurityConfiguration.class}) +@DirtiesContext +public class ManualSecurityWithCertificateTest extends AbstractSecurityTest { + + public ManualSecurityWithCertificateTest() { + log.info("--- ManualSecurityWithCertificateTest ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/server/ScopedTestServiceImpl.java b/tests/src/test/java/net/devh/boot/grpc/test/server/ScopedTestServiceImpl.java new file mode 100644 index 000000000..fc4ac4e54 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/server/ScopedTestServiceImpl.java @@ -0,0 +1,76 @@ +/* + * 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.server; + +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Autowired; + +import io.grpc.stub.StreamObserver; +import net.devh.boot.grpc.server.service.GrpcService; +import net.devh.boot.grpc.test.proto.SomeType; + +@GrpcService +public class ScopedTestServiceImpl extends TestServiceImpl { + + @Autowired + private RequestId requestId; + + @Override + public StreamObserver secureBidi(StreamObserver responseObserver) { + return new StreamObserver() { + + @Override + public void onNext(final SomeType input) { + final SomeType version = + input.toBuilder().setVersion(ScopedTestServiceImpl.this.requestId.getId()).build(); + responseObserver.onNext(version); + } + + @Override + public void onError(final Throwable t) { + responseObserver.onError(t); + } + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + + }; + } + + /** + * Fake scoped bean used to simulate variable contents. May not be a final class. + */ + public static class RequestId { + + private final String id = UUID.randomUUID().toString(); + + public String getId() { + return this.id; + } + + @Override + public String toString() { + return getId(); + } + + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/server/TestServiceImpl.java b/tests/src/test/java/net/devh/boot/grpc/test/server/TestServiceImpl.java new file mode 100644 index 000000000..77331a0d1 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/server/TestServiceImpl.java @@ -0,0 +1,201 @@ +/* + * 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.server; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; + +import com.google.common.util.concurrent.MoreExecutors; +import com.google.protobuf.Empty; + +import io.grpc.Context; +import io.grpc.Status; +import io.grpc.stub.StreamObserver; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.security.interceptors.AuthenticatingServerInterceptor; +import net.devh.boot.grpc.server.service.GrpcService; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceImplBase; + +@Slf4j +@GrpcService +public class TestServiceImpl extends TestServiceImplBase { + + public static final int METHOD_COUNT = TestServiceGrpc.getServiceDescriptor().getMethods().size(); + + public TestServiceImpl() { + log.info("Created TestServiceImpl"); + } + + @Override + public void normal(final Empty request, final StreamObserver responseObserver) { + log.debug("normal"); + final SomeType version = SomeType.newBuilder().setVersion("1.2.3").build(); + responseObserver.onNext(version); + responseObserver.onCompleted(); + } + + @Override + public void unimplemented(final Empty request, final StreamObserver responseObserver) { + log.debug("unimplemented"); + // Not implemented (on purpose) + super.unimplemented(request, responseObserver); + } + + @Override + public void error(final Empty request, final StreamObserver responseObserver) { + log.debug("error"); + responseObserver.onError(Status.INTERNAL.asRuntimeException()); + } + + @Override + public StreamObserver echo(final StreamObserver responseObserver) { + log.debug("echo"); + return responseObserver; + } + + @Override + @Secured("ROLE_CLIENT1") + public void secure(final Empty request, final StreamObserver responseObserver) { + final Authentication authentication = assertAuthenticated("secure"); + + assertSameAuthenticatedGrcContextCancellation("secure-cancellation", authentication); + + final SomeType version = SomeType.newBuilder().setVersion("1.2.3").build(); + responseObserver.onNext(version); + responseObserver.onCompleted(); + } + + @Override + @Secured("ROLE_CLIENT1") + public StreamObserver secureDrain(final StreamObserver responseObserver) { + final Authentication authentication = assertAuthenticated("secureDrain"); + + assertSameAuthenticatedGrcContextCancellation("secureDrain-cancellation", authentication); + + return new StreamObserver() { + + @Override + public void onNext(final SomeType input) { + assertSameAuthenticated("secureDrain-onNext", authentication); + assertEquals("1.2.3", input.getVersion()); + } + + @Override + public void onError(final Throwable t) { + assertSameAuthenticated("secureDrain-onError", authentication); + responseObserver.onError(t); + } + + @Override + public void onCompleted() { + assertSameAuthenticated("secureDrain-onCompleted", authentication); + responseObserver.onNext(Empty.getDefaultInstance()); + responseObserver.onCompleted(); + } + + }; + } + + @Override + @Secured("ROLE_CLIENT1") + public void secureSupply(final Empty request, final StreamObserver responseObserver) { + final Authentication authentication = assertAuthenticated("secureListener"); + + assertSameAuthenticatedGrcContextCancellation("secureSupply-cancellation", authentication); + + responseObserver.onNext(SomeType.newBuilder().setVersion("1.2.3").build()); + responseObserver.onNext(SomeType.newBuilder().setVersion("1.2.3").build()); + responseObserver.onNext(SomeType.newBuilder().setVersion("1.2.3").build()); + responseObserver.onCompleted(); + } + + @Override + @Secured("ROLE_CLIENT1") + public StreamObserver secureBidi(final StreamObserver responseObserver) { + final Authentication authentication = assertAuthenticated("secureBidi"); + + assertSameAuthenticatedGrcContextCancellation("secureBidi-cancellation", authentication); + + return new StreamObserver() { + + @Override + public void onNext(final SomeType input) { + assertSameAuthenticated("secureBidi-onNext", authentication); + assertEquals("1.2.3", input.getVersion()); + responseObserver.onNext(input); + } + + @Override + public void onError(final Throwable t) { + assertSameAuthenticated("secureBidi-onError", authentication); + responseObserver.onError(t); + } + + @Override + public void onCompleted() { + assertSameAuthenticated("secureBidi-onCompleted", authentication); + responseObserver.onCompleted(); + } + + }; + } + + protected Authentication assertAuthenticated(final String method) { + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return assertAuthenticated(method, authentication); + } + + protected Authentication assertAuthenticated(final String method, final Authentication actual) { + assertNotNull(actual, "No user authentication"); + assertTrue(actual.isAuthenticated(), "User not authenticated!"); + log.debug("{}: {}", method, actual.getName()); + return actual; + } + + protected void assertSameAuthenticatedGrcContextCancellation(final String method, final Authentication expected) { + Context.current().addListener(context -> { + assertSameAuthenticatedGrcContextOnly(method, expected, context); + }, MoreExecutors.directExecutor()); + } + + protected Authentication assertSameAuthenticatedGrcContextOnly(final String method, final Authentication expected, + final Context context) { + return assertSameAuthenticated(method, expected, + AuthenticatingServerInterceptor.AUTHENTICATION_CONTEXT_KEY.get(context)); + } + + protected Authentication assertSameAuthenticated(final String method, final Authentication expected) { + assertSameAuthenticatedGrcContextOnly(method, expected, Context.current()); + return assertSameAuthenticated(method, expected, SecurityContextHolder.getContext().getAuthentication()); + } + + protected Authentication assertSameAuthenticated(final String method, final Authentication expected, + final Authentication actual) { + assertSame(expected, actual, method); + return assertAuthenticated(method, expected); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/server/WaitingTestService.java b/tests/src/test/java/net/devh/boot/grpc/test/server/WaitingTestService.java new file mode 100644 index 000000000..8c54a5894 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/server/WaitingTestService.java @@ -0,0 +1,81 @@ +/* + * 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.server; + +import static java.time.Duration.ofMillis; +import static java.time.Duration.ofSeconds; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + +import java.time.Duration; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; + +import com.google.protobuf.Empty; + +import io.grpc.stub.StreamObserver; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceImplBase; + +/** + * A test service implementation that spends a configurable amount of time processing a request. + */ +public class WaitingTestService extends TestServiceImplBase { + + // Queue length = 1 -> Ability to control/await server calls + private final BlockingQueue delays = new ArrayBlockingQueue<>(1); + + /** + * The next call will wait the configured amount of time before completing. Allows exactly one call to process. May + * only queue up to one call. + * + * @param delay The delay to wait for. + */ + public synchronized void nextDelay(final Duration delay) { + assertTimeoutPreemptively( + ofSeconds(1), + () -> assertDoesNotThrow(() -> this.delays.put(delay.toMillis())), + "Failed to queue delay"); + } + + /** + * Waits until all request have started processing on the server. + */ + public synchronized void awaitAllRequestsArrived() { + // Just try to set a value + nextDelay(ofMillis(-1)); + this.delays.clear(); + } + + @Override + public void normal(final Empty request, final StreamObserver responseObserver) { + // Simulate processing time + assertDoesNotThrow(this::sleep); + responseObserver.onNext(SomeType.getDefaultInstance()); + responseObserver.onCompleted(); + } + + private void sleep() throws InterruptedException { + final long delay = assertTimeoutPreemptively(ofSeconds(1), () -> this.delays.take()); + if (delay <= 0) { + throw new IllegalStateException("Bad delay: " + delay); + } + Thread.sleep(delay); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/AbstractBrokenServerClientTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/AbstractBrokenServerClientTest.java new file mode 100644 index 000000000..e3dc9bb10 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/AbstractBrokenServerClientTest.java @@ -0,0 +1,92 @@ +/* + * 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.setup; + +import static io.grpc.Status.Code.UNAVAILABLE; +import static net.devh.boot.grpc.test.util.GrpcAssertions.assertFutureThrowsStatus; +import static net.devh.boot.grpc.test.util.GrpcAssertions.assertThrowsStatus; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.springframework.test.annotation.DirtiesContext; + +import com.google.protobuf.Empty; + +import io.grpc.Channel; +import io.grpc.internal.testing.StreamRecorder; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceBlockingStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceFutureStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceStub; + +@Slf4j +public abstract class AbstractBrokenServerClientTest { + + // Don't configure client + @GrpcClient("test") + protected Channel channel; + @GrpcClient("test") + protected TestServiceStub testServiceStub; + @GrpcClient("test") + protected TestServiceBlockingStub testServiceBlockingStub; + @GrpcClient("test") + protected TestServiceFutureStub testServiceFutureStub; + + /** + * Test successful call with broken setup. + */ + @Test + @DirtiesContext + public void testSuccessfulCallWithBrokenSetup() { + log.info("--- Starting tests with successful call with broken setup ---"); + assertThrowsStatus(UNAVAILABLE, + () -> TestServiceGrpc.newBlockingStub(this.channel).normal(Empty.getDefaultInstance())); + + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.testServiceStub.normal(Empty.getDefaultInstance(), streamRecorder); + assertFutureThrowsStatus(UNAVAILABLE, streamRecorder.firstValue(), 5, TimeUnit.SECONDS); + assertThrowsStatus(UNAVAILABLE, () -> this.testServiceBlockingStub.normal(Empty.getDefaultInstance())); + assertFutureThrowsStatus(UNAVAILABLE, this.testServiceFutureStub.normal(Empty.getDefaultInstance()), + 5, TimeUnit.SECONDS); + log.info("--- Test completed ---"); + } + + /** + * Test failing call with broken setup. + */ + @Test + @DirtiesContext + public void testFailingCallWithBrokenSetup() { + log.info("--- Starting tests with failing call with broken setup ---"); + assertThrowsStatus(UNAVAILABLE, + () -> TestServiceGrpc.newBlockingStub(this.channel).unimplemented(Empty.getDefaultInstance())); + + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.testServiceStub.unimplemented(Empty.getDefaultInstance(), streamRecorder); + assertFutureThrowsStatus(UNAVAILABLE, streamRecorder.firstValue(), 5, TimeUnit.SECONDS); + assertThrowsStatus(UNAVAILABLE, () -> this.testServiceBlockingStub.unimplemented(Empty.getDefaultInstance())); + assertFutureThrowsStatus(UNAVAILABLE, this.testServiceFutureStub.unimplemented(Empty.getDefaultInstance()), + 5, TimeUnit.SECONDS); + log.info("--- Test completed ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/AbstractSimpleServerClientTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/AbstractSimpleServerClientTest.java new file mode 100644 index 000000000..23cbf1417 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/AbstractSimpleServerClientTest.java @@ -0,0 +1,112 @@ +/* + * 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.setup; + +import static io.grpc.Status.Code.UNIMPLEMENTED; +import static net.devh.boot.grpc.test.util.GrpcAssertions.assertFutureThrowsStatus; +import static net.devh.boot.grpc.test.util.GrpcAssertions.assertThrowsStatus; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import javax.annotation.PostConstruct; + +import org.junit.jupiter.api.Test; +import org.springframework.test.annotation.DirtiesContext; + +import com.google.protobuf.Empty; + +import io.grpc.Channel; +import io.grpc.internal.testing.StreamRecorder; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceBlockingStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceFutureStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceStub; + +/** + * A test checking that the server and client can start and connect to each other with minimal config. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +public abstract class AbstractSimpleServerClientTest { + + @GrpcClient("test") + protected Channel channel; + @GrpcClient("test") + protected TestServiceStub testServiceStub; + @GrpcClient("test") + protected TestServiceBlockingStub testServiceBlockingStub; + @GrpcClient("test") + protected TestServiceFutureStub testServiceFutureStub; + + @PostConstruct + protected void init() { + // Test injection + assertNotNull(this.channel, "channel"); + assertNotNull(this.testServiceBlockingStub, "testServiceBlockingStub"); + assertNotNull(this.testServiceFutureStub, "testServiceFutureStub"); + assertNotNull(this.testServiceStub, "testServiceStub"); + } + + /** + * Test successful call. + * + * @throws ExecutionException Should never happen. + * @throws InterruptedException Should never happen. + */ + @Test + @DirtiesContext + void testSuccessfulCall() throws InterruptedException, ExecutionException { + log.info("--- Starting tests with successful call ---"); + assertEquals("1.2.3", + TestServiceGrpc.newBlockingStub(this.channel).normal(Empty.getDefaultInstance()).getVersion()); + + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.testServiceStub.normal(Empty.getDefaultInstance(), streamRecorder); + assertEquals("1.2.3", streamRecorder.firstValue().get().getVersion()); + assertEquals("1.2.3", this.testServiceBlockingStub.normal(Empty.getDefaultInstance()).getVersion()); + assertEquals("1.2.3", this.testServiceFutureStub.normal(Empty.getDefaultInstance()).get().getVersion()); + log.info("--- Test completed ---"); + } + + /** + * Test failing call. + */ + @Test + @DirtiesContext + void testFailingCall() { + log.info("--- Starting tests with failing call ---"); + assertThrowsStatus(UNIMPLEMENTED, + () -> TestServiceGrpc.newBlockingStub(this.channel).unimplemented(Empty.getDefaultInstance())); + + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.testServiceStub.unimplemented(Empty.getDefaultInstance(), streamRecorder); + assertFutureThrowsStatus(UNIMPLEMENTED, streamRecorder.firstValue(), 5, TimeUnit.SECONDS); + assertThrowsStatus(UNIMPLEMENTED, () -> this.testServiceBlockingStub.unimplemented(Empty.getDefaultInstance())); + assertFutureThrowsStatus(UNIMPLEMENTED, this.testServiceFutureStub.unimplemented(Empty.getDefaultInstance()), + 5, TimeUnit.SECONDS); + log.info("--- Test completed ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/BeanAnnotatedServiceTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/BeanAnnotatedServiceTest.java new file mode 100644 index 000000000..c9196435d --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/BeanAnnotatedServiceTest.java @@ -0,0 +1,68 @@ +/* + * 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.setup; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.server.service.GrpcService; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.BeanAnnotatedServiceConfig; +import net.devh.boot.grpc.test.config.InProcessConfiguration; + +/** + * A test checking that the server picks up a {@link GrpcService} annotated bean from a {@link Configuration}. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest +@SpringJUnitConfig(classes = { + InProcessConfiguration.class, + BeanAnnotatedServiceConfig.class, + BaseAutoConfiguration.class}) +@DirtiesContext +class BeanAnnotatedServiceTest extends AbstractSimpleServerClientTest { + + public BeanAnnotatedServiceTest() { + log.info("--- BeanAnnotatedServiceTest ---"); + } + + @Autowired + AtomicBoolean invoked; + + @Override + @Test + void testSuccessfulCall() throws InterruptedException, ExecutionException { + assertFalse(this.invoked.get()); + super.testSuccessfulCall(); + assertTrue(this.invoked.get()); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/BrokenClientSelfSignedMutualSetupTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/BrokenClientSelfSignedMutualSetupTest.java new file mode 100644 index 000000000..10901091f --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/BrokenClientSelfSignedMutualSetupTest.java @@ -0,0 +1,53 @@ +/* + * 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.setup; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; + +/** + * A test checking that the server and client can start and connect to each other with minimal config. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest(properties = { + "grpc.server.security.enabled=true", + "grpc.server.security.certificateChain=file:src/test/resources/certificates/server.crt", + "grpc.server.security.privateKey=file:src/test/resources/certificates/server.key", + "grpc.server.security.trustCertCollection=file:src/test/resources/certificates/trusted-clients-collection", + "grpc.server.security.clientAuth=REQUIRE", + "grpc.client.test.security.authorityOverride=localhost", + "grpc.client.test.security.trustCertCollection=file:src/test/resources/certificates/trusted-servers-collection", + "grpc.client.test.security.clientAuthEnabled=false", // <-- client auth not enabled + "grpc.client.test.security.certificateChain=file:src/test/resources/certificates/client1.crt", + "grpc.client.test.security.privateKey=file:src/test/resources/certificates/client1.key"}) +@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) +@DirtiesContext +public class BrokenClientSelfSignedMutualSetupTest extends AbstractBrokenServerClientTest { + + public BrokenClientSelfSignedMutualSetupTest() { + log.info("--- BrokenClientSelfSignedMutualSetupTest ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/BrokenServerSelfSignedMutualSetupTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/BrokenServerSelfSignedMutualSetupTest.java new file mode 100644 index 000000000..8159f0140 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/BrokenServerSelfSignedMutualSetupTest.java @@ -0,0 +1,53 @@ +/* + * 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.setup; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; + +/** + * A test checking that the server and client can start and connect to each other with minimal config. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest(properties = { + "grpc.server.security.enabled=true", + "grpc.server.security.certificateChain=file:src/test/resources/certificates/server.crt", + "grpc.server.security.privateKey=file:src/test/resources/certificates/server.key", + "grpc.server.security.trustCertCollection=file:src/test/resources/certificates/client2.crt", // Wrong certs + "grpc.server.security.clientAuth=REQUIRE", + "grpc.client.test.security.authorityOverride=localhost", + "grpc.client.test.security.trustCertCollection=file:src/test/resources/certificates/trusted-servers-collection", + "grpc.client.test.security.clientAuthEnabled=true", + "grpc.client.test.security.certificateChain=file:src/test/resources/certificates/client1.crt", + "grpc.client.test.security.privateKey=file:src/test/resources/certificates/client1.key"}) +@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) +@DirtiesContext +public class BrokenServerSelfSignedMutualSetupTest extends AbstractBrokenServerClientTest { + + public BrokenServerSelfSignedMutualSetupTest() { + log.info("--- BrokenServerSelfSignedMutualSetupTest ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/CustomCiphersAndProtocolsSetupTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/CustomCiphersAndProtocolsSetupTest.java new file mode 100644 index 000000000..acda37aaf --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/CustomCiphersAndProtocolsSetupTest.java @@ -0,0 +1,137 @@ +/* + * 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.setup; + +import static org.junit.jupiter.api.Assertions.*; + +import javax.net.ssl.SSLHandshakeException; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.google.protobuf.Empty; + +import io.grpc.StatusRuntimeException; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.proto.TestServiceGrpc; + +@SpringBootTest(properties = { + "grpc.server.security.enabled=true", + "grpc.server.security.certificateChain=file:src/test/resources/certificates/server.crt", + "grpc.server.security.privateKey=file:src/test/resources/certificates/server.key", + "grpc.server.security.ciphers=TLS_AES_256_GCM_SHA384:ECDHE-RSA-AES256-GCM-SHA384", + "grpc.server.security.protocols=TLSv1.3:TLSv1.2", + + "grpc.client.GLOBAL.address=localhost:9090", + "grpc.client.GLOBAL.security.authorityOverride=localhost", + "grpc.client.GLOBAL.security.trustCertCollection=file:src/test/resources/certificates/trusted-servers-collection", + "grpc.client.GLOBAL.negotiationType=TLS", + + "grpc.client.tls11.security.protocols=TLSv1.1", + "grpc.client.tls11.security.ciphers=ECDHE-RSA-AES256-SHA", + + "grpc.client.tls12.security.protocols=TLSv1.2", + "grpc.client.tls12.security.ciphers=ECDHE-RSA-AES256-GCM-SHA384", + + "grpc.client.tls13.security.protocols=TLSv1.3", + "grpc.client.tls13.security.ciphers=TLS_AES_256_GCM_SHA384", + + "grpc.client.noSharedCiphers.security.protocols=TLSv1.2:TLSv1.1", + "grpc.client.noSharedCiphers.security.ciphers=ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA", + + "grpc.client.noSharedProtocols.security.protocols=TLSv1.1", + "grpc.client.noSharedProtocols.security.ciphers=ECDHE-RSA-AES128-SHA", +}) +@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) +@DirtiesContext +class CustomCiphersAndProtocolsSetupTest extends AbstractSimpleServerClientTest { + + @GrpcClient("test") + private TestServiceGrpc.TestServiceBlockingStub test; + @GrpcClient("tls11") + private TestServiceGrpc.TestServiceBlockingStub tlsV11Stub; + @GrpcClient("tls12") + private TestServiceGrpc.TestServiceBlockingStub tlsV12Stub; + @GrpcClient("tls13") + private TestServiceGrpc.TestServiceBlockingStub tlsV13Stub; + @GrpcClient("noSharedCiphers") + private TestServiceGrpc.TestServiceBlockingStub tlsNoSharedCiphersStub; + @GrpcClient("noSharedProtocols") + private TestServiceGrpc.TestServiceBlockingStub tlsNoSharedProtocolsStub; + + /** + * Tests behaviour with TLSv1.1 and shared protocols. Test should fail, as the server does not support TLSv1.1. + */ + @Test + public void testTlsV11Stub() { + + Exception exception = assertThrows(StatusRuntimeException.class, () -> { + tlsV11Stub.normal(Empty.getDefaultInstance()).getVersion(); + }); + assertTrue(exception.getCause() instanceof SSLHandshakeException); + } + + /** + * Tests behaviour with TLSv1.2 and shared protocols. Test should succeed, as the server supports TLSv1.2. + */ + @Test + public void testTlsV12Stub() { + + assertEquals("1.2.3", + tlsV12Stub.normal(Empty.getDefaultInstance()).getVersion()); + } + + /** + * Tests behaviour with TLSv1.3 and shared protocols. Test should succeed, as the server supports TLSv1.3. + */ + @Test + public void testTlsV13Stub() { + + assertEquals("1.2.3", + tlsV13Stub.normal(Empty.getDefaultInstance()).getVersion()); + } + + /** + * Tests behaviour with no shared ciphers. Test should fail with a {@link SSLHandshakeException} + */ + @Test + public void testNoSharedCiphersClientStub() { + + Exception exception = assertThrows(StatusRuntimeException.class, () -> { + tlsNoSharedCiphersStub.normal(Empty.getDefaultInstance()).getVersion(); + }); + assertTrue(exception.getCause() instanceof SSLHandshakeException); + } + + /** + * Tests behaviour with no shared protocols. Test should fail with a {@link SSLHandshakeException} as the server + * does not support TLSv1.1. + */ + @Test + public void testNoSharedProtocolsStub() { + + Exception exception = assertThrows(StatusRuntimeException.class, () -> { + tlsNoSharedProtocolsStub.normal(Empty.getDefaultInstance()).getVersion(); + }); + assertTrue(exception.getCause() instanceof SSLHandshakeException); + } +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/ImmediateConnectTests.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/ImmediateConnectTests.java new file mode 100644 index 000000000..a5f464f19 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/ImmediateConnectTests.java @@ -0,0 +1,131 @@ +/* + * 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.setup; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import io.grpc.Channel; +import io.grpc.ConnectivityState; +import io.grpc.ManagedChannel; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.config.GrpcChannelProperties; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; + +/** + * These tests check the property {@link GrpcChannelProperties#getImmediateConnectTimeout()}. They check for + * backwards-compatibility when this property didn't existed and various cases when it's enabled (for successful + * connection and failed one). + */ +public class ImmediateConnectTests { + + @Slf4j + @SpringBootTest(properties = { + "grpc.client.GLOBAL.address=localhost:9090", + "grpc.client.GLOBAL.negotiationType=PLAINTEXT", + "grpc.client.GLOBAL.immediateConnectTimeout=10s", + }) + @SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) + @DirtiesContext + static class ImmediateConnectEnabledAndSuccessfulTest extends AbstractSimpleServerClientTest { + + ImmediateConnectEnabledAndSuccessfulTest() { + log.info("--- ImmediateConnectEnabledAnsSuccessfulTest ---"); + } + + @Test + @DirtiesContext + void immediateConnectEnabledAndSuccessful() { + assumeTrue(channel instanceof ManagedChannel, + "To run this test channel must be ManagedChannel"); + ManagedChannel managedChannel = (ManagedChannel) channel; + + ConnectivityState state = managedChannel.getState(false); + assertEquals( + "When immediateConnectTimeout property is set to positive duration channel must be in READY state if connection was successful", + ConnectivityState.READY, state); + } + } + + @Slf4j + @SpringBootTest(properties = { + "grpc.client.GLOBAL.address=localhost:9090", + "grpc.client.GLOBAL.negotiationType=PLAINTEXT", + }) + @SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) + @DirtiesContext + static class ImmediateConnectDisabledTest extends AbstractSimpleServerClientTest { + + ImmediateConnectDisabledTest() { + log.info("--- ImmediateConnectDisabledTest ---"); + } + + @Test + @DirtiesContext + void immediateConnectDisabled() { + assumeTrue(channel instanceof ManagedChannel, + "To run this test channel must be ManagedChannel"); + ManagedChannel managedChannel = (ManagedChannel) channel; + + ConnectivityState state = managedChannel.getState(false); + assertEquals( + "When immediateConnectTimeout property is set to zero or unset grpc must not attempt to connect until first request", + ConnectivityState.IDLE, state); + } + } + + @Slf4j + static class ImmediateConnectEnabledAndFailedToConnectTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withPropertyValues( + "grpc.client.GLOBAL.address=localhost:9999", + "grpc.client.GLOBAL.negotiationType=PLAINTEXT", + "grpc.client.GLOBAL.immediateConnectTimeout=1s") + .withUserConfiguration(ServiceConfiguration.class, BaseAutoConfiguration.class) + .withBean(FailedChannelHolder.class); + + public ImmediateConnectEnabledAndFailedToConnectTest() { + log.info("--- ImmediateConnectEnabledAndFailedToConnectTest ---"); + } + + @Test + void immediateConnectEnabledAndFailedToConnect() { + contextRunner.run(context -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()) + .getCause() + .isOfAnyClassIn(IllegalStateException.class); + }); + } + + private static class FailedChannelHolder { + @GrpcClient("test") + private Channel channel; + } + } +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/InProcessSetupTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/InProcessSetupTest.java new file mode 100644 index 000000000..cbaa4d4ce --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/InProcessSetupTest.java @@ -0,0 +1,44 @@ +/* + * 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.setup; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.InProcessConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; + +/** + * A test checking that the server and client can start and connect to each other in process. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest +@SpringJUnitConfig(classes = {InProcessConfiguration.class, ServiceConfiguration.class, BaseAutoConfiguration.class}) +@DirtiesContext +public class InProcessSetupTest extends AbstractSimpleServerClientTest { + + public InProcessSetupTest() { + log.info("--- InProcessSetupTest ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/InterAndInProcessSetup2Test.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/InterAndInProcessSetup2Test.java new file mode 100644 index 000000000..ee24aba73 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/InterAndInProcessSetup2Test.java @@ -0,0 +1,180 @@ +/* + * 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.setup; + +import static io.grpc.Status.Code.UNIMPLEMENTED; +import static net.devh.boot.grpc.test.proto.TestServiceGrpc.newBlockingStub; +import static net.devh.boot.grpc.test.util.GrpcAssertions.assertFutureThrowsStatus; +import static net.devh.boot.grpc.test.util.GrpcAssertions.assertThrowsStatus; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import javax.annotation.PostConstruct; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.google.protobuf.Empty; + +import io.grpc.Channel; +import io.grpc.internal.testing.StreamRecorder; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceBlockingStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceFutureStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceStub; + +/** + * Tests whether the parallel setup of inter- and in-process-server/client works. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest(properties = { + "grpc.server.in-process-name=test", + "grpc.server.port=9191", + "grpc.client.GLOBAL.negotiationType=PLAINTEXT", + "grpc.client.inProcess.address=in-process:test", + "grpc.client.interProcess.address=static://localhost:9191"}) +@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) +@DirtiesContext +public class InterAndInProcessSetup2Test { + + private static final Empty EMPTY = Empty.getDefaultInstance(); + + @GrpcClient("interProcess") + protected Channel interProcessChannel; + @GrpcClient("interProcess") + protected TestServiceStub interProcessServiceStub; + @GrpcClient("interProcess") + protected TestServiceBlockingStub interProcessServiceBlockingStub; + @GrpcClient("interProcess") + protected TestServiceFutureStub interProcessServiceFutureStub; + + @GrpcClient("inProcess") + protected Channel inProcessChannel; + @GrpcClient("inProcess") + protected TestServiceStub inProcessServiceStub; + @GrpcClient("inProcess") + protected TestServiceBlockingStub inProcessServiceBlockingStub; + @GrpcClient("inProcess") + protected TestServiceFutureStub inProcessServiceFutureStub; + + public InterAndInProcessSetup2Test() { + log.info("--- InterAndInProcessSetupTest ---"); + } + + @PostConstruct + public void init() { + // Test injection + assertNotNull(this.interProcessChannel, "interProcessChannel"); + assertNotNull(this.interProcessServiceBlockingStub, "interProcessServiceBlockingStub"); + assertNotNull(this.interProcessServiceFutureStub, "interProcessServiceFutureStub"); + assertNotNull(this.interProcessServiceStub, "interProcessServiceStub"); + + assertNotNull(this.inProcessChannel, "inProcessChannel"); + assertNotNull(this.inProcessServiceBlockingStub, "inProcessServiceBlockingStub"); + assertNotNull(this.inProcessServiceFutureStub, "inProcessServiceFutureStub"); + assertNotNull(this.inProcessServiceStub, "inProcessServiceStub"); + } + + /** + * Test successful call for inter-process server. + * + * @throws ExecutionException Should never happen. + * @throws InterruptedException Should never happen. + */ + @Test + @DirtiesContext + public void testSuccessfulInterProcessCall() throws InterruptedException, ExecutionException { + log.info("--- Starting tests with successful inter-process call ---"); + assertEquals("1.2.3", newBlockingStub(this.interProcessChannel).normal(EMPTY).getVersion()); + + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.interProcessServiceStub.normal(EMPTY, streamRecorder); + assertEquals("1.2.3", streamRecorder.firstValue().get().getVersion()); + assertEquals("1.2.3", this.interProcessServiceBlockingStub.normal(EMPTY).getVersion()); + assertEquals("1.2.3", this.interProcessServiceFutureStub.normal(EMPTY).get().getVersion()); + log.info("--- Test completed ---"); + } + + /** + * Test successful call for in-process server. + * + * @throws ExecutionException Should never happen. + * @throws InterruptedException Should never happen. + */ + @Test + @DirtiesContext + public void testSuccessfulInProcessCall() throws InterruptedException, ExecutionException { + log.info("--- Starting tests with successful in-process call ---"); + assertEquals("1.2.3", newBlockingStub(this.inProcessChannel).normal(EMPTY).getVersion()); + + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.inProcessServiceStub.normal(EMPTY, streamRecorder); + assertEquals("1.2.3", streamRecorder.firstValue().get().getVersion()); + assertEquals("1.2.3", this.inProcessServiceBlockingStub.normal(EMPTY).getVersion()); + assertEquals("1.2.3", this.inProcessServiceFutureStub.normal(EMPTY).get().getVersion()); + log.info("--- Test completed ---"); + } + + /** + * Test failing call for inter-process server. + */ + @Test + @DirtiesContext + public void testFailingInterProcessCall() { + log.info("--- Starting tests with failing inter-process call ---"); + assertThrowsStatus(UNIMPLEMENTED, () -> newBlockingStub(this.interProcessChannel).unimplemented(EMPTY)); + + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.interProcessServiceStub.unimplemented(EMPTY, streamRecorder); + assertFutureThrowsStatus(UNIMPLEMENTED, streamRecorder.firstValue(), 5, TimeUnit.SECONDS); + assertThrowsStatus(UNIMPLEMENTED, () -> this.interProcessServiceBlockingStub.unimplemented(EMPTY)); + assertFutureThrowsStatus(UNIMPLEMENTED, this.interProcessServiceFutureStub.unimplemented(EMPTY), + 5, TimeUnit.SECONDS); + log.info("--- Test completed ---"); + } + + /** + * Test failing call for in-process server. + */ + @Test + @DirtiesContext + public void testFailingInProcessCall() { + log.info("--- Starting tests with failing in-process call ---"); + assertThrowsStatus(UNIMPLEMENTED, () -> newBlockingStub(this.inProcessChannel).unimplemented(EMPTY)); + + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.inProcessServiceStub.unimplemented(EMPTY, streamRecorder); + assertFutureThrowsStatus(UNIMPLEMENTED, streamRecorder.firstValue(), 5, TimeUnit.SECONDS); + assertThrowsStatus(UNIMPLEMENTED, () -> this.inProcessServiceBlockingStub.unimplemented(EMPTY)); + assertFutureThrowsStatus(UNIMPLEMENTED, this.inProcessServiceFutureStub.unimplemented(EMPTY), + 5, TimeUnit.SECONDS); + log.info("--- Test completed ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/InterAndInProcessSetupTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/InterAndInProcessSetupTest.java new file mode 100644 index 000000000..cdb917a71 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/InterAndInProcessSetupTest.java @@ -0,0 +1,180 @@ +/* + * 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.setup; + +import static io.grpc.Status.Code.UNIMPLEMENTED; +import static net.devh.boot.grpc.test.proto.TestServiceGrpc.newBlockingStub; +import static net.devh.boot.grpc.test.util.GrpcAssertions.assertFutureThrowsStatus; +import static net.devh.boot.grpc.test.util.GrpcAssertions.assertThrowsStatus; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import javax.annotation.PostConstruct; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.google.protobuf.Empty; + +import io.grpc.Channel; +import io.grpc.internal.testing.StreamRecorder; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceBlockingStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceFutureStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceStub; + +/** + * Tests whether the parallel setup of inter- and in-process-server/client works. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest(properties = { + "grpc.server.inProcessName=test", + "grpc.server.port=9191", + "grpc.client.GLOBAL.negotiationType=PLAINTEXT", + "grpc.client.inProcess.address=in-process:test", + "grpc.client.interProcess.address=static://localhost:9191"}) +@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) +@DirtiesContext +public class InterAndInProcessSetupTest { + + private static final Empty EMPTY = Empty.getDefaultInstance(); + + @GrpcClient("interProcess") + protected Channel interProcessChannel; + @GrpcClient("interProcess") + protected TestServiceStub interProcessServiceStub; + @GrpcClient("interProcess") + protected TestServiceBlockingStub interProcessServiceBlockingStub; + @GrpcClient("interProcess") + protected TestServiceFutureStub interProcessServiceFutureStub; + + @GrpcClient("inProcess") + protected Channel inProcessChannel; + @GrpcClient("inProcess") + protected TestServiceStub inProcessServiceStub; + @GrpcClient("inProcess") + protected TestServiceBlockingStub inProcessServiceBlockingStub; + @GrpcClient("inProcess") + protected TestServiceFutureStub inProcessServiceFutureStub; + + public InterAndInProcessSetupTest() { + log.info("--- InterAndInProcessSetupTest ---"); + } + + @PostConstruct + public void init() { + // Test injection + assertNotNull(this.interProcessChannel, "interProcessChannel"); + assertNotNull(this.interProcessServiceBlockingStub, "interProcessServiceBlockingStub"); + assertNotNull(this.interProcessServiceFutureStub, "interProcessServiceFutureStub"); + assertNotNull(this.interProcessServiceStub, "interProcessServiceStub"); + + assertNotNull(this.inProcessChannel, "inProcessChannel"); + assertNotNull(this.inProcessServiceBlockingStub, "inProcessServiceBlockingStub"); + assertNotNull(this.inProcessServiceFutureStub, "inProcessServiceFutureStub"); + assertNotNull(this.inProcessServiceStub, "inProcessServiceStub"); + } + + /** + * Test successful call for inter-process server. + * + * @throws ExecutionException Should never happen. + * @throws InterruptedException Should never happen. + */ + @Test + @DirtiesContext + public void testSuccessfulInterProcessCall() throws InterruptedException, ExecutionException { + log.info("--- Starting tests with successful inter-process call ---"); + assertEquals("1.2.3", newBlockingStub(this.interProcessChannel).normal(EMPTY).getVersion()); + + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.interProcessServiceStub.normal(EMPTY, streamRecorder); + assertEquals("1.2.3", streamRecorder.firstValue().get().getVersion()); + assertEquals("1.2.3", this.interProcessServiceBlockingStub.normal(EMPTY).getVersion()); + assertEquals("1.2.3", this.interProcessServiceFutureStub.normal(EMPTY).get().getVersion()); + log.info("--- Test completed ---"); + } + + /** + * Test successful call for in-process server. + * + * @throws ExecutionException Should never happen. + * @throws InterruptedException Should never happen. + */ + @Test + @DirtiesContext + public void testSuccessfulInProcessCall() throws InterruptedException, ExecutionException { + log.info("--- Starting tests with successful in-process call ---"); + assertEquals("1.2.3", newBlockingStub(this.inProcessChannel).normal(EMPTY).getVersion()); + + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.inProcessServiceStub.normal(EMPTY, streamRecorder); + assertEquals("1.2.3", streamRecorder.firstValue().get().getVersion()); + assertEquals("1.2.3", this.inProcessServiceBlockingStub.normal(EMPTY).getVersion()); + assertEquals("1.2.3", this.inProcessServiceFutureStub.normal(EMPTY).get().getVersion()); + log.info("--- Test completed ---"); + } + + /** + * Test failing call for inter-process server. + */ + @Test + @DirtiesContext + public void testFailingInterProcessCall() { + log.info("--- Starting tests with failing inter-process call ---"); + assertThrowsStatus(UNIMPLEMENTED, () -> newBlockingStub(this.interProcessChannel).unimplemented(EMPTY)); + + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.interProcessServiceStub.unimplemented(EMPTY, streamRecorder); + assertFutureThrowsStatus(UNIMPLEMENTED, streamRecorder.firstValue(), 5, TimeUnit.SECONDS); + assertThrowsStatus(UNIMPLEMENTED, () -> this.interProcessServiceBlockingStub.unimplemented(EMPTY)); + assertFutureThrowsStatus(UNIMPLEMENTED, this.interProcessServiceFutureStub.unimplemented(EMPTY), + 5, TimeUnit.SECONDS); + log.info("--- Test completed ---"); + } + + /** + * Test failing call for in-process server. + */ + @Test + @DirtiesContext + public void testFailingInProcessCall() { + log.info("--- Starting tests with failing in-process call ---"); + assertThrowsStatus(UNIMPLEMENTED, () -> newBlockingStub(this.inProcessChannel).unimplemented(EMPTY)); + + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.inProcessServiceStub.unimplemented(EMPTY, streamRecorder); + assertFutureThrowsStatus(UNIMPLEMENTED, streamRecorder.firstValue(), 5, TimeUnit.SECONDS); + assertThrowsStatus(UNIMPLEMENTED, () -> this.inProcessServiceBlockingStub.unimplemented(EMPTY)); + assertFutureThrowsStatus(UNIMPLEMENTED, this.inProcessServiceFutureStub.unimplemented(EMPTY), + 5, TimeUnit.SECONDS); + log.info("--- Test completed ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/NameResolverConnectionTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/NameResolverConnectionTest.java new file mode 100644 index 000000000..b1b125b34 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/NameResolverConnectionTest.java @@ -0,0 +1,86 @@ +/* + * 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.setup; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.google.protobuf.Empty; + +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceBlockingStub; +import net.devh.boot.grpc.test.util.EnableOnIPv6; + +@SpringBootTest(properties = { + "grpc.client.GLOBAL.negotiationType=PLAINTEXT", + "grpc.client.GLOBAL.address=localhost:9090", + "grpc.client.dns.address=dns:///localhost:9090", + "grpc.client.localhost.address=static://localhost:9090", + "grpc.client.ipv4.address=static://127.0.0.1:9090", + "grpc.client.ipv6.address=static://[::1]:9090", +}) +@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) +@DirtiesContext +public class NameResolverConnectionTest { + + private static final Empty EMPTY = Empty.getDefaultInstance(); + + @GrpcClient("default") + private TestServiceBlockingStub defaultStub; + @GrpcClient("dns") + private TestServiceBlockingStub dnsStub; + @GrpcClient("localhost") + private TestServiceBlockingStub localhostStub; + @GrpcClient("ipv4") + private TestServiceBlockingStub ipv4Stub; + @GrpcClient("ipv6") + private TestServiceBlockingStub ipv6Stub; + + @Test + public void testDefaultConnection() { + assertEquals("1.2.3", this.defaultStub.normal(EMPTY).getVersion()); + } + + @Test + public void testDNSConnection() { + assertEquals("1.2.3", this.dnsStub.normal(EMPTY).getVersion()); + } + + @Test + public void testLocalhostConnection() { + assertEquals("1.2.3", this.localhostStub.normal(EMPTY).getVersion()); + } + + @Test + public void testIpv4Connection() { + assertEquals("1.2.3", this.ipv4Stub.normal(EMPTY).getVersion()); + } + + @Test + @EnableOnIPv6 + public void testIpv6Connection() { + assertEquals("1.2.3", this.ipv6Stub.normal(EMPTY).getVersion()); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/NameResolverIPv4ConnectionTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/NameResolverIPv4ConnectionTest.java new file mode 100644 index 000000000..61f169c88 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/NameResolverIPv4ConnectionTest.java @@ -0,0 +1,76 @@ +/* + * 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.setup; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.google.protobuf.Empty; + +import io.grpc.Status.Code; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceBlockingStub; +import net.devh.boot.grpc.test.util.GrpcAssertions; + +@SpringBootTest(properties = { + "grpc.server.address=127.0.0.1", + "grpc.client.default.negotiationType=PLAINTEXT", + "grpc.client.dns.negotiationType=PLAINTEXT", + "grpc.client.dns.address=dns:/localhost:9090/", + "grpc.client.localhost.negotiationType=PLAINTEXT", + "grpc.client.localhost.address=static://localhost:9090", + "grpc.client.ipv4.negotiationType=PLAINTEXT", + "grpc.client.ipv4.address=static://127.0.0.1:9090", + "grpc.client.ipv6.negotiationType=PLAINTEXT", + "grpc.client.ipv6.address=static://[::1]:9090", +}) +@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) +@DirtiesContext +public class NameResolverIPv4ConnectionTest { + + private static final Empty EMPTY = Empty.getDefaultInstance(); + + @GrpcClient("dns") + private TestServiceBlockingStub dnsStub; + @GrpcClient("ipv4") + private TestServiceBlockingStub ipv4Stub; + @GrpcClient("ipv6") + private TestServiceBlockingStub ipv6Stub; + + @Test + public void testDNSConnection() { + assertEquals("1.2.3", this.dnsStub.normal(EMPTY).getVersion()); + } + + @Test + public void testIpv4Connection() { + assertEquals("1.2.3", this.ipv4Stub.normal(EMPTY).getVersion()); + } + + @Test + public void testIpv6Connection() { + GrpcAssertions.assertThrowsStatus(Code.UNAVAILABLE, () -> this.ipv6Stub.normal(EMPTY)); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/NameResolverIPv6ConnectionTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/NameResolverIPv6ConnectionTest.java new file mode 100644 index 000000000..9a5b93a9e --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/NameResolverIPv6ConnectionTest.java @@ -0,0 +1,75 @@ +/* + * 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.setup; + +import static net.devh.boot.grpc.test.util.GrpcAssertions.assertThrowsStatus; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.google.protobuf.Empty; + +import io.grpc.Status.Code; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceBlockingStub; +import net.devh.boot.grpc.test.util.EnableOnIPv6; + +@SpringBootTest(properties = { + "grpc.server.address=::1", + "grpc.client.dns.negotiationType=PLAINTEXT", + "grpc.client.dns.address=dns:/localhost:9090/", + "grpc.client.ipv4.negotiationType=PLAINTEXT", + "grpc.client.ipv4.address=static://127.0.0.1:9090", + "grpc.client.ipv6.negotiationType=PLAINTEXT", + "grpc.client.ipv6.address=static://[::1]:9090", +}) +@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) +@DirtiesContext +@EnableOnIPv6 +public class NameResolverIPv6ConnectionTest { + + private static final Empty EMPTY = Empty.getDefaultInstance(); + + @GrpcClient("dns") + private TestServiceBlockingStub dnsStub; + @GrpcClient("ipv4") + private TestServiceBlockingStub ipv4Stub; + @GrpcClient("ipv6") + private TestServiceBlockingStub ipv6Stub; + + @Test + public void testDNSConnection() { + assertEquals("1.2.3", this.dnsStub.normal(EMPTY).getVersion()); + } + + @Test + public void testIpv4Connection() { + assertThrowsStatus(Code.UNAVAILABLE, () -> this.ipv4Stub.normal(EMPTY)); + } + + @Test + public void testIpv6Connection() { + assertEquals("1.2.3", this.ipv6Stub.normal(EMPTY).getVersion()); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/OnlyInProcessServerTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/OnlyInProcessServerTest.java new file mode 100644 index 000000000..bc2ce02a5 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/OnlyInProcessServerTest.java @@ -0,0 +1,179 @@ +/* + * 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.setup; + +import static io.grpc.Status.Code.UNAVAILABLE; +import static io.grpc.Status.Code.UNIMPLEMENTED; +import static net.devh.boot.grpc.test.proto.TestServiceGrpc.newBlockingStub; +import static net.devh.boot.grpc.test.util.GrpcAssertions.assertFutureThrowsStatus; +import static net.devh.boot.grpc.test.util.GrpcAssertions.assertThrowsStatus; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +import javax.annotation.PostConstruct; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.google.protobuf.Empty; + +import io.grpc.Channel; +import io.grpc.internal.testing.StreamRecorder; +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceBlockingStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceFutureStub; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceStub; + +/** + * Test that ensures that it is possible to disable the inter-process server with a property. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest(properties = { + "grpc.server.inProcessName=test", + "grpc.server.port=-1", + "grpc.client.GLOBAL.negotiationType=PLAINTEXT", + "grpc.client.inProcess.address=in-process:test", + "grpc.client.interProcess.address=static://localhost:9090"}) +@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) +@DirtiesContext +public class OnlyInProcessServerTest { + + private static final Empty EMPTY = Empty.getDefaultInstance(); + + @GrpcClient("interProcess") + protected Channel interProcessChannel; + @GrpcClient("interProcess") + protected TestServiceStub interProcessServiceStub; + @GrpcClient("interProcess") + protected TestServiceBlockingStub interProcessServiceBlockingStub; + @GrpcClient("interProcess") + protected TestServiceFutureStub interProcessServiceFutureStub; + + @GrpcClient("inProcess") + protected Channel inProcessChannel; + @GrpcClient("inProcess") + protected TestServiceStub inProcessServiceStub; + @GrpcClient("inProcess") + protected TestServiceBlockingStub inProcessServiceBlockingStub; + @GrpcClient("inProcess") + protected TestServiceFutureStub inProcessServiceFutureStub; + + public OnlyInProcessServerTest() { + log.info("--- OnlyInProcessServerTest ---"); + } + + @PostConstruct + public void init() { + // Test injection + assertNotNull(this.interProcessChannel, "interProcessChannel"); + assertNotNull(this.interProcessServiceBlockingStub, "interProcessServiceBlockingStub"); + assertNotNull(this.interProcessServiceFutureStub, "interProcessServiceFutureStub"); + assertNotNull(this.interProcessServiceStub, "interProcessServiceStub"); + + assertNotNull(this.inProcessChannel, "inProcessChannel"); + assertNotNull(this.inProcessServiceBlockingStub, "inProcessServiceBlockingStub"); + assertNotNull(this.inProcessServiceFutureStub, "inProcessServiceFutureStub"); + assertNotNull(this.inProcessServiceStub, "inProcessServiceStub"); + } + + /** + * Test successful call for inter-process server. + */ + @Test + @DirtiesContext + public void testSuccessfulInterProcessCall() { + log.info("--- Starting tests with successful but unavailable inter-process call ---"); + assertThrowsStatus(UNAVAILABLE, () -> newBlockingStub(this.interProcessChannel).unimplemented(EMPTY)); + + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.interProcessServiceStub.unimplemented(EMPTY, streamRecorder); + assertFutureThrowsStatus(UNAVAILABLE, streamRecorder.firstValue(), 5, TimeUnit.SECONDS); + assertThrowsStatus(UNAVAILABLE, () -> this.interProcessServiceBlockingStub.unimplemented(EMPTY)); + assertFutureThrowsStatus(UNAVAILABLE, this.interProcessServiceFutureStub.unimplemented(EMPTY), + 5, TimeUnit.SECONDS); + log.info("--- Test completed ---"); + } + + /** + * Test successful call for in-process server. + * + * @throws ExecutionException Should never happen. + * @throws InterruptedException Should never happen. + */ + @Test + @DirtiesContext + public void testSuccessfulInProcessCall() throws InterruptedException, ExecutionException { + log.info("--- Starting tests with successful in-process call ---"); + assertEquals("1.2.3", newBlockingStub(this.inProcessChannel).normal(EMPTY).getVersion()); + + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.inProcessServiceStub.normal(EMPTY, streamRecorder); + assertEquals("1.2.3", streamRecorder.firstValue().get().getVersion()); + assertEquals("1.2.3", this.inProcessServiceBlockingStub.normal(EMPTY).getVersion()); + assertEquals("1.2.3", this.inProcessServiceFutureStub.normal(EMPTY).get().getVersion()); + log.info("--- Test completed ---"); + } + + /** + * Test failing call for inter-process server. + */ + @Test + @DirtiesContext + public void testFailingInterProcessCall() { + log.info("--- Starting tests with failing inter-process call ---"); + assertThrowsStatus(UNAVAILABLE, () -> newBlockingStub(this.interProcessChannel).unimplemented(EMPTY)); + + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.interProcessServiceStub.unimplemented(EMPTY, streamRecorder); + assertFutureThrowsStatus(UNAVAILABLE, streamRecorder.firstValue(), 5, TimeUnit.SECONDS); + assertThrowsStatus(UNAVAILABLE, () -> this.interProcessServiceBlockingStub.unimplemented(EMPTY)); + assertFutureThrowsStatus(UNAVAILABLE, this.interProcessServiceFutureStub.unimplemented(EMPTY), + 5, TimeUnit.SECONDS); + log.info("--- Test completed ---"); + } + + /** + * Test failing call for in-process server. + */ + @Test + @DirtiesContext + public void testFailingInProcessCall() { + log.info("--- Starting tests with failing in-process call ---"); + assertThrowsStatus(UNIMPLEMENTED, () -> newBlockingStub(this.inProcessChannel).unimplemented(EMPTY)); + + final StreamRecorder streamRecorder = StreamRecorder.create(); + this.inProcessServiceStub.unimplemented(EMPTY, streamRecorder); + assertFutureThrowsStatus(UNIMPLEMENTED, streamRecorder.firstValue(), 5, TimeUnit.SECONDS); + assertThrowsStatus(UNIMPLEMENTED, () -> this.inProcessServiceBlockingStub.unimplemented(EMPTY)); + assertFutureThrowsStatus(UNIMPLEMENTED, this.inProcessServiceFutureStub.unimplemented(EMPTY), + 5, TimeUnit.SECONDS); + log.info("--- Test completed ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/PlaintextSetupTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/PlaintextSetupTest.java new file mode 100644 index 000000000..0afaf8fe6 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/PlaintextSetupTest.java @@ -0,0 +1,46 @@ +/* + * 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.setup; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; + +/** + * A test checking that the server and client can start and connect to each other with minimal config. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest(properties = { + "grpc.client.GLOBAL.address=localhost:9090", + "grpc.client.GLOBAL.negotiationType=PLAINTEXT" +}) +@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) +@DirtiesContext +public class PlaintextSetupTest extends AbstractSimpleServerClientTest { + + public PlaintextSetupTest() { + log.info("--- PlaintextSetupTest ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/SelfNameResolverConnectionTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/SelfNameResolverConnectionTest.java new file mode 100644 index 000000000..29ebee30f --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/SelfNameResolverConnectionTest.java @@ -0,0 +1,73 @@ +/* + * 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.setup; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import com.google.protobuf.Empty; + +import net.devh.boot.grpc.client.inject.GrpcClient; +import net.devh.boot.grpc.server.nameresolver.SelfNameResolverFactory; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceBlockingStub; + +/** + * Tests that the {@link SelfNameResolverFactory} works as expected. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@SpringBootTest(properties = { + "grpc.server.port=0", + "grpc.client.GLOBAL.negotiationType=PLAINTEXT", + "grpc.client.other.address=self", +}) +@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) +@DirtiesContext +public class SelfNameResolverConnectionTest { + + private static final Empty EMPTY = Empty.getDefaultInstance(); + + @GrpcClient("self") + private TestServiceBlockingStub selfStub; + + @GrpcClient("other") + private TestServiceBlockingStub otherStub; + + /** + * Tests the connection via the implicit client address. + */ + @Test + public void testSelfConnection() { + assertEquals("1.2.3", this.selfStub.normal(EMPTY).getVersion()); + } + + /** + * Tests the connection via the explicit client address. + */ + @Test + public void testOtherConnection() { + assertEquals("1.2.3", this.otherStub.normal(EMPTY).getVersion()); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/SelfSignedInProcessSetupTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/SelfSignedInProcessSetupTest.java new file mode 100644 index 000000000..d1313333e --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/SelfSignedInProcessSetupTest.java @@ -0,0 +1,57 @@ +/* + * 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.setup; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; + +/** + * A test checking that the server and client can start and connect to each other with minimal config. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest(properties = { + "grpc.server.port=-1", + "grpc.server.in-process-name=test", + "grpc.server.security.enabled=true", + "grpc.server.security.certificateChain=file:src/test/resources/certificates/server.crt", + "grpc.server.security.privateKey=file:src/test/resources/certificates/server.key", + "grpc.server.security.trustCertCollection=file:src/test/resources/certificates/trusted-clients-collection", + "grpc.server.security.clientAuth=REQUIRE", + + "grpc.client.test.address=in-process:test", + "grpc.client.test.security.authorityOverride=localhost", + "grpc.client.test.security.trustCertCollection=file:src/test/resources/certificates/trusted-servers-collection", + "grpc.client.test.security.clientAuthEnabled=true", + "grpc.client.test.security.certificateChain=file:src/test/resources/certificates/client1.crt", + "grpc.client.test.security.privateKey=file:src/test/resources/certificates/client1.key"}) +@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) +@DirtiesContext +public class SelfSignedInProcessSetupTest extends AbstractSimpleServerClientTest { + + public SelfSignedInProcessSetupTest() { + log.info("--- SelfSignedInProcessSetupTest ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/SelfSignedMutualSetupTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/SelfSignedMutualSetupTest.java new file mode 100644 index 000000000..67c78b204 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/SelfSignedMutualSetupTest.java @@ -0,0 +1,55 @@ +/* + * 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.setup; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; + +/** + * A test checking that the server and client can start and connect to each other with minimal config. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest(properties = { + "grpc.server.security.enabled=true", + "grpc.server.security.certificateChain=file:src/test/resources/certificates/server.crt", + "grpc.server.security.privateKey=file:src/test/resources/certificates/server.key", + "grpc.server.security.trustCertCollection=file:src/test/resources/certificates/trusted-clients-collection", + "grpc.server.security.clientAuth=REQUIRE", + + "grpc.client.test.address=localhost:9090", + "grpc.client.test.security.authorityOverride=localhost", + "grpc.client.test.security.trustCertCollection=file:src/test/resources/certificates/trusted-servers-collection", + "grpc.client.test.security.clientAuthEnabled=true", + "grpc.client.test.security.certificateChain=file:src/test/resources/certificates/client1.crt", + "grpc.client.test.security.privateKey=file:src/test/resources/certificates/client1.key"}) +@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) +@DirtiesContext +public class SelfSignedMutualSetupTest extends AbstractSimpleServerClientTest { + + public SelfSignedMutualSetupTest() { + log.info("--- SelfSignedMutualSetupTest ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/SelfSignedServerSetupTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/SelfSignedServerSetupTest.java new file mode 100644 index 000000000..38364755b --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/SelfSignedServerSetupTest.java @@ -0,0 +1,50 @@ +/* + * 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.setup; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; + +/** + * A test checking that the server and client can start and connect to each other with minimal config. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest(properties = { + "grpc.server.security.enabled=true", + "grpc.server.security.certificateChain=file:src/test/resources/certificates/server.crt", + "grpc.server.security.privateKey=file:src/test/resources/certificates/server.key", + "grpc.client.test.address=localhost:9090", + "grpc.client.test.security.authorityOverride=localhost", + "grpc.client.test.security.trustCertCollection=file:src/test/resources/certificates/trusted-servers-collection" +}) +@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) +@DirtiesContext +public class SelfSignedServerSetupTest extends AbstractSimpleServerClientTest { + + public SelfSignedServerSetupTest() { + log.info("--- SelfSignedServerSetupTest ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/setup/UnixSetupTest.java b/tests/src/test/java/net/devh/boot/grpc/test/setup/UnixSetupTest.java new file mode 100644 index 000000000..e55aa749e --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/setup/UnixSetupTest.java @@ -0,0 +1,50 @@ +/* + * 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.setup; + +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import lombok.extern.slf4j.Slf4j; +import net.devh.boot.grpc.test.config.BaseAutoConfiguration; +import net.devh.boot.grpc.test.config.ServiceConfiguration; + +/** + * A test checking that the server and client can start and connect to each other with minimal config. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +@SpringBootTest(properties = { + "grpc.server.address=unix:unix-test", + "grpc.client.GLOBAL.address=unix:unix-test", + "grpc.client.GLOBAL.negotiationType=PLAINTEXT", +}) +@SpringJUnitConfig(classes = {ServiceConfiguration.class, BaseAutoConfiguration.class}) +@DirtiesContext +@EnabledOnOs(OS.LINUX) +public class UnixSetupTest extends AbstractSimpleServerClientTest { + + public UnixSetupTest() { + log.info("--- UnixSetupTest ---"); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/shutdown/GrpcChannelLifecycleWithCallsTest.java b/tests/src/test/java/net/devh/boot/grpc/test/shutdown/GrpcChannelLifecycleWithCallsTest.java new file mode 100644 index 000000000..ba560e9a8 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/shutdown/GrpcChannelLifecycleWithCallsTest.java @@ -0,0 +1,199 @@ +/* + * 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.shutdown; + +import static io.grpc.Status.Code.UNAVAILABLE; +import static java.time.Duration.ZERO; +import static java.time.Duration.ofMillis; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static net.devh.boot.grpc.test.util.FutureAssertions.assertFutureEquals; +import static net.devh.boot.grpc.test.util.GrpcAssertions.assertFutureThrowsStatus; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTimeout; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + +import java.io.IOException; +import java.time.Duration; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.api.function.ThrowingConsumer; +import org.springframework.context.support.GenericApplicationContext; + +import com.google.protobuf.Empty; + +import io.grpc.Channel; +import io.grpc.Server; +import io.grpc.Status; +import io.grpc.inprocess.InProcessServerBuilder; +import net.devh.boot.grpc.client.channelfactory.GrpcChannelFactory; +import net.devh.boot.grpc.client.channelfactory.InProcessChannelFactory; +import net.devh.boot.grpc.client.config.GrpcChannelsProperties; +import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorRegistry; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceFutureStub; +import net.devh.boot.grpc.test.server.WaitingTestService; + +/** + * Tests for {@link InProcessChannelFactory}'s shutdown behavior using an in-process server/channel. + */ +class GrpcChannelLifecycleWithCallsTest { + + private static final int A_BIT_TIME = 100; + private static final Empty EMPTY = Empty.getDefaultInstance(); + static final SomeType SOME_TYPE = SomeType.getDefaultInstance(); + + private static final WaitingTestService service = new WaitingTestService(); + private static final Server server = InProcessServerBuilder.forName("test") + .addService(service) + .build(); + private static final GenericApplicationContext applicationContext = new GenericApplicationContext(); + + @BeforeAll + static void beforeAll() throws IOException { + applicationContext.refresh(); + // Init classes before the actual test, because that is somewhat slow + applicationContext.start(); + server.start(); + withStub(ZERO, stub -> { + + service.nextDelay(ofMillis(1)); + stub.normal(EMPTY); + + service.awaitAllRequestsArrived(); + + }); + } + + @AfterAll + static void afterAll() { + server.shutdownNow(); + applicationContext.close(); + } + + @Test + void testZeroShutdownGracePeriod() { + + final AtomicReference> request = new AtomicReference<>(); + + assertTimeoutPreemptively(ofMillis(A_BIT_TIME), + runWithStub(ZERO, stub -> { + + // Send request (Takes 5s) + service.nextDelay(ofMillis(5000)); + request.set(stub.normal(EMPTY)); + + service.awaitAllRequestsArrived(); + + })); + + // The request did not complete in time + assertFailedShutdown(request); + } + + @Test + void testShortShutdownGracePeriod() { + + final AtomicReference> request1 = new AtomicReference<>(); + final AtomicReference> request2 = new AtomicReference<>(); + final AtomicReference> request3 = new AtomicReference<>(); + + assertTimeout(ofMillis(2000 + A_BIT_TIME), + runWithStub(ofMillis(2000), stub -> { + + // Send first request (Takes 1s) + service.nextDelay(ofMillis(1000)); + request1.set(stub.normal(EMPTY)); + + // Send second request (Takes 5s) + service.nextDelay(ofMillis(5000)); + request2.set(stub.normal(EMPTY)); + + // Send last request (Takes 1s) + service.nextDelay(ofMillis(1000)); + request3.set(stub.normal(EMPTY)); + + service.awaitAllRequestsArrived(); + + })); + + // First one completed + assertCompleted(request1); + + // The second one did not complete in time + assertFailedShutdown(request2); + + // Last one completed + assertCompleted(request3); + } + + @Test + void testInfiniteShutdownGracePeriod() { + + final AtomicReference> request = new AtomicReference<>(); + + assertTimeoutPreemptively(ofMillis(1000 + A_BIT_TIME), + runWithStub(ofMillis(-1), stub -> { + + // Send request (Takes 1s) + service.nextDelay(ofMillis(1000)); + request.set(stub.normal(EMPTY)); + + service.awaitAllRequestsArrived(); + + })); + + // Request completed + assertCompleted(request); + } + + static Executable runWithStub(final Duration shutdownGracePeriod, + final ThrowingConsumer executuable) { + return () -> withStub(shutdownGracePeriod, executuable); + } + + static void withStub(final Duration shutdownGracePeriod, + final ThrowingConsumer executuable) { + + final GrpcChannelsProperties properties = new GrpcChannelsProperties(); + properties.getGlobalChannel().setShutdownGracePeriod(shutdownGracePeriod); + try (final GrpcChannelFactory factory = new InProcessChannelFactory(properties, + new GlobalClientInterceptorRegistry(applicationContext))) { + + final Channel channel = factory.createChannel("test"); + final TestServiceFutureStub stub = TestServiceGrpc.newFutureStub(channel); + assertDoesNotThrow(() -> executuable.accept(stub)); + } + } + + private void assertFailedShutdown(final AtomicReference> request) { + final Status status = assertFutureThrowsStatus(UNAVAILABLE, request.get(), A_BIT_TIME, MILLISECONDS); + assertThat(status.getDescription()).contains("shutdownNow"); + } + + private void assertCompleted(final AtomicReference> request) { + assertFutureEquals(SOME_TYPE, request.get(), A_BIT_TIME, MILLISECONDS); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/shutdown/GrpcServerLifecycleWithCallsTest.java b/tests/src/test/java/net/devh/boot/grpc/test/shutdown/GrpcServerLifecycleWithCallsTest.java new file mode 100644 index 000000000..71c1e6de0 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/shutdown/GrpcServerLifecycleWithCallsTest.java @@ -0,0 +1,178 @@ +/* + * 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.shutdown; + +import static io.grpc.Status.Code.UNAVAILABLE; +import static java.time.Duration.ZERO; +import static java.time.Duration.ofMillis; +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static net.devh.boot.grpc.test.util.FutureAssertions.assertFutureEquals; +import static net.devh.boot.grpc.test.util.GrpcAssertions.assertFutureThrowsStatus; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; + +import java.time.Duration; +import java.util.concurrent.Future; +import java.util.function.BiConsumer; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import com.google.protobuf.Empty; + +import io.grpc.ManagedChannel; +import io.grpc.Status; +import io.grpc.inprocess.InProcessChannelBuilder; +import net.devh.boot.grpc.server.config.GrpcServerProperties; +import net.devh.boot.grpc.server.serverfactory.GrpcServerFactory; +import net.devh.boot.grpc.server.serverfactory.GrpcServerLifecycle; +import net.devh.boot.grpc.server.serverfactory.InProcessGrpcServerFactory; +import net.devh.boot.grpc.server.service.GrpcServiceDefinition; +import net.devh.boot.grpc.test.proto.SomeType; +import net.devh.boot.grpc.test.proto.TestServiceGrpc; +import net.devh.boot.grpc.test.proto.TestServiceGrpc.TestServiceFutureStub; +import net.devh.boot.grpc.test.server.WaitingTestService; + +/** + * Tests for {@link GrpcServerLifecycle}'s shutdown behavior using an in-process server/channel. + */ +class GrpcServerLifecycleWithCallsTest { + + private static final int A_BIT_TIME = 100; + private static final Empty EMPTY = Empty.getDefaultInstance(); + private static final SomeType SOME_TYPE = SomeType.getDefaultInstance(); + + private static ManagedChannel channel; + private static TestServiceFutureStub stub; + + @BeforeAll + static void beforeAll() { + channel = InProcessChannelBuilder.forName("test").build(); + stub = TestServiceGrpc.newFutureStub(channel); + } + + @AfterAll + static void afterAll() { + channel.shutdownNow(); + } + + @Test + void testZeroShutdownGracePeriod() { + + withServer(ZERO, (service, lifecycle) -> { + // Start server + lifecycle.start(); + + // Send request (Takes 5s) + service.nextDelay(ofMillis(5000)); + final Future request = stub.normal(EMPTY); + + service.awaitAllRequestsArrived(); + + // Shutdown and don't wait for graceful shutdown (we give it a short time to finish shutting down) + assertTimeoutPreemptively(ofMillis(A_BIT_TIME), (Executable) lifecycle::stop); + + // The request did not complete in time + assertFailedShutdown(request); + }); + } + + @Test + void testShortShutdownGracePeriod() { + + withServer(ofMillis(2000), (service, lifecycle) -> { + // Start server + lifecycle.start(); + + // Send first request (Takes 1s) + service.nextDelay(ofMillis(1000)); + final Future request1 = stub.normal(EMPTY); + + // Send second request (Takes 5s) + service.nextDelay(ofMillis(5000)); + final Future request2 = stub.normal(EMPTY); + + // Send last request (Takes 1s) + service.nextDelay(ofMillis(1000)); + final Future request3 = stub.normal(EMPTY); + + service.awaitAllRequestsArrived(); + + // Shutdown and wait a short amount of time to shutdown gracefully + assertTimeoutPreemptively(ofMillis(2000 + A_BIT_TIME), (Executable) lifecycle::stop); + + // First one completed + assertCompleted(request1); + + // The second one did not complete in time + assertFailedShutdown(request2); + + // Last one completed + assertCompleted(request3); + }); + } + + @Test + void testInfiniteShutdownGracePeriod() { + + withServer(ofMillis(-1), (service, lifecycle) -> { + // Start server + lifecycle.start(); + + // Send request (Takes 1s) + service.nextDelay(ofMillis(1000)); + final Future request = stub.normal(EMPTY); + + service.awaitAllRequestsArrived(); + + // Shutdown and wait for the server to shutdown gracefully + assertTimeoutPreemptively(ofMillis(1000 + A_BIT_TIME), (Executable) lifecycle::stop); + + // Request completed + assertCompleted(request); + }); + } + + void withServer(final Duration gracefulShutdownTimeout, + final BiConsumer executuable) { + final GrpcServerFactory factory = new InProcessGrpcServerFactory("test", new GrpcServerProperties()); + final WaitingTestService service = new WaitingTestService(); + + factory.addService(new GrpcServiceDefinition("service", WaitingTestService.class, service.bindService())); + + final GrpcServerLifecycle lifecycle = new GrpcServerLifecycle(factory, gracefulShutdownTimeout); + try { + assertDoesNotThrow(() -> executuable.accept(service, lifecycle)); + } finally { + lifecycle.stop(); + } + } + + private void assertFailedShutdown(final Future request) { + final Status status = assertFutureThrowsStatus(UNAVAILABLE, request, A_BIT_TIME, MILLISECONDS); + assertThat(status.getDescription()).contains("shutdownNow"); + } + + private void assertCompleted(final Future request) { + assertFutureEquals(SOME_TYPE, request, A_BIT_TIME, MILLISECONDS); + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/util/DynamicTestCollection.java b/tests/src/test/java/net/devh/boot/grpc/test/util/DynamicTestCollection.java new file mode 100644 index 000000000..f8324ca2f --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/util/DynamicTestCollection.java @@ -0,0 +1,49 @@ +/* + * 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.util; + +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.function.Executable; + +public class DynamicTestCollection implements Iterable { + + private final List tests = new ArrayList<>(); + + public static DynamicTestCollection create() { + return new DynamicTestCollection(); + } + + private DynamicTestCollection() {} + + @Override + public Iterator iterator() { + return this.tests.iterator(); + } + + public DynamicTestCollection add(final String name, final Executable executable) { + this.tests.add(dynamicTest(name, executable)); + return this; + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/util/EnableOnIPv6.java b/tests/src/test/java/net/devh/boot/grpc/test/util/EnableOnIPv6.java new file mode 100644 index 000000000..430ecae49 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/util/EnableOnIPv6.java @@ -0,0 +1,39 @@ +/* + * 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.util; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * An annotation for JUnit tests that will enable the test if the current host has an IPv6 loopback address. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Target({METHOD, TYPE, ANNOTATION_TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@ExtendWith(RequireIPv6Condition.class) +public @interface EnableOnIPv6 { +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/util/FutureAssertions.java b/tests/src/test/java/net/devh/boot/grpc/test/util/FutureAssertions.java new file mode 100644 index 000000000..f73382810 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/util/FutureAssertions.java @@ -0,0 +1,95 @@ +/* + * 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.util; + +import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +/** + * Assertions related to {@link Future}s. + */ +public final class FutureAssertions { + + /** + * Asserts that the {@link Future} returns the expected result. + * + * @param The type of the future content. + * @param expected The expected content. + * @param future The future to check for the expected content. + * @param timeout The maximum time to wait for the result. + * @param timeoutUnit The time unit of the {@code timeout} argument. + */ + public static void assertFutureEquals(final T expected, final Future future, + final int timeout, final TimeUnit timeoutUnit) { + assertFutureEquals(expected, future, UnaryOperator.identity(), timeout, timeoutUnit); + } + + /** + * Asserts that the {@link Future} returns the expected result. + * + * @param The type of the unwrapped/expected content. + * @param The type of the future content. + * @param expected The expected content. + * @param future The future to check for the expected content. + * @param unwrapper The function used to extract the content. + * @param timeout The maximum time to wait for the result. + * @param timeoutUnit The time unit of the {@code timeout} argument. + */ + public static void assertFutureEquals(final T expected, final Future future, + final Function unwrapper, final int timeout, final TimeUnit timeoutUnit) { + try { + assertEquals(expected, unwrapper.apply(future.get(timeout, timeoutUnit))); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + fail("Unexpected error while trying to get the result", e); + } + } + + /** + * Asserts that the given {@link Future} fails with an {@link ExecutionException} caused by the given exception + * type. + * + * @param The type of the causing exception. + * @param expectedType The expected type of the causing exception. + * @param future The future expected to throw. + * @param timeout The maximum time to wait for the result. + * @param timeoutUnit The time unit of the {@code timeout} argument. + * @return The causing exception. + */ + @SuppressWarnings("unchecked") + public static T assertFutureThrows(final Class expectedType, + final Future future, final int timeout, final TimeUnit timeoutUnit) { + final Throwable cause = + assertThrows(ExecutionException.class, () -> future.get(timeout, timeoutUnit)).getCause(); + final Class causeClass = cause.getClass(); + assertTrue(expectedType.isAssignableFrom(causeClass), "The cause was of type: " + causeClass.getName() + + ", but it was expected to be a subclass of " + expectedType.getName()); + return (T) cause; + } + + private FutureAssertions() {} + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/util/GrpcAssertions.java b/tests/src/test/java/net/devh/boot/grpc/test/util/GrpcAssertions.java new file mode 100644 index 000000000..c5fce8885 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/util/GrpcAssertions.java @@ -0,0 +1,134 @@ +/* + * 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.util; + +import static net.devh.boot.grpc.test.util.FutureAssertions.assertFutureEquals; +import static net.devh.boot.grpc.test.util.FutureAssertions.assertFutureThrows; +import static org.junit.Assert.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.UnaryOperator; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.function.Executable; + +import io.grpc.Status; +import io.grpc.StatusRuntimeException; +import io.grpc.internal.testing.StreamRecorder; + +/** + * Assertions related to gRPC client calls. + */ +public final class GrpcAssertions { + + /** + * Asserts that the first value in the {@link StreamRecorder} equals the expected value. + * + * @param The type of the observer's content. + * @param expected The expected content. + * @param responseObserver The observer to check for the expected content. + * @param timeout The maximum time to wait for the result. + * @param timeoutUnit The time unit of the {@code timeout} argument. + */ + public static void assertFutureFirstEquals(final T expected, final StreamRecorder responseObserver, + final int timeout, final TimeUnit timeoutUnit) { + assertFutureFirstEquals(expected, responseObserver, UnaryOperator.identity(), timeout, timeoutUnit); + } + + /** + * Asserts that the first value in the {@link StreamRecorder} equals the expected value. + * + * @param The type of the unwrapped/expected content. + * @param The type of the observer's content. + * @param expected The expected content. + * @param responseObserver The observer to check for the expected content. + * @param unwrapper The function used to extract the content. + * @param timeout The maximum time to wait for the result. + * @param timeoutUnit The time unit of the {@code timeout} argument. + */ + public static void assertFutureFirstEquals(final T expected, final StreamRecorder responseObserver, + final Function unwrapper, final int timeout, final TimeUnit timeoutUnit) { + assertFutureEquals(expected, responseObserver.firstValue(), unwrapper, timeout, timeoutUnit); + } + + /** + * Assert that the given {@link Executable} throws a {@link StatusRuntimeException} with the expected status code. + * + * @param expectedCode The expected status code. + * @param executable The executable to run. + * @return The status contained in the exception. + * @see Assertions#assertThrows(Class, Executable) + */ + public static Status assertThrowsStatus(final Status.Code expectedCode, final Executable executable) { + final StatusRuntimeException exception = assertThrows(StatusRuntimeException.class, executable); + return assertStatus(expectedCode, exception); + } + + /** + * Asserts that the given {@link StreamRecorder} throws an {@link ExecutionException} caused by a + * {@link StatusRuntimeException} with the expected status code. + * + * @param expectedCode The expected status code. + * @param recorder The recorder expected to throw. + * @param timeout The maximum time to wait for the result. + * @param timeoutUnit The time unit of the {@code timeout} argument. + * @return The status contained in the exception. + * @see #assertFutureThrowsStatus(io.grpc.Status.Code, Future, int, TimeUnit) + */ + public static Status assertFutureThrowsStatus(final Status.Code expectedCode, final StreamRecorder recorder, + final int timeout, final TimeUnit timeoutUnit) { + return assertFutureThrowsStatus(expectedCode, recorder.firstValue(), timeout, timeoutUnit); + } + + /** + * Asserts that the given {@link Future} throws an {@link ExecutionException} caused by a + * {@link StatusRuntimeException} with the expected status code. + * + * @param expectedCode The expected status code. + * @param future The future expected to throw. + * @param timeout The maximum time to wait for the result. + * @param timeoutUnit The time unit of the {@code timeout} argument. + * @return The status contained in the exception. + */ + public static Status assertFutureThrowsStatus(final Status.Code expectedCode, final Future future, + final int timeout, final TimeUnit timeoutUnit) { + final StatusRuntimeException exception = + assertFutureThrows(StatusRuntimeException.class, future, timeout, timeoutUnit); + return assertStatus(expectedCode, exception); + } + + /** + * Asserts that the given {@link StatusRuntimeException} uses the expected status code. + * + * @param expectedCode The expected status code. + * @param exception The exception to check for the status code. + * @return The status contained in the exception. + */ + public static Status assertStatus(final Status.Code expectedCode, final StatusRuntimeException exception) { + final Status status = exception.getStatus(); + assertEquals(expectedCode, status.getCode()); + return status; + } + + private GrpcAssertions() {} + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/util/LoggerTestUtil.java b/tests/src/test/java/net/devh/boot/grpc/test/util/LoggerTestUtil.java new file mode 100644 index 000000000..3a4f46329 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/util/LoggerTestUtil.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2016-2020 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.util; + +import java.util.Arrays; + +import org.slf4j.LoggerFactory; +import org.springframework.lang.Nullable; + +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.read.ListAppender; + +/** + * Class to test proper @Slf4j logging. + * + * @author Andjelko Perisic (andjelko.perisic@gmail.com) + */ +public class LoggerTestUtil { + + private LoggerTestUtil() { + throw new UnsupportedOperationException("Util class not to be instantiated."); + } + + + public static ListAppender getListAppenderForClasses(@Nullable Class... classList) { + + ListAppender loggingEventListAppender = new ListAppender<>(); + loggingEventListAppender.start(); + + if (classList == null) { + return loggingEventListAppender; + } + + Arrays.stream(classList) + .map(clazz -> (Logger) LoggerFactory.getLogger(clazz)) + .forEach(log -> log.addAppender(loggingEventListAppender)); + + return loggingEventListAppender; + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/util/RequireIPv6Condition.java b/tests/src/test/java/net/devh/boot/grpc/test/util/RequireIPv6Condition.java new file mode 100644 index 000000000..c7e59b135 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/util/RequireIPv6Condition.java @@ -0,0 +1,75 @@ +/* + * 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.util; + +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Collections; +import java.util.Enumeration; + +import org.junit.jupiter.api.extension.ConditionEvaluationResult; +import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.extension.ExtensionContext; + +import lombok.extern.slf4j.Slf4j; + +/** + * A JUnit 5 condition that checks whether the current host has an IPv6 loopback address. + * + * @author Daniel Theuke (daniel.theuke@heuboe.de) + */ +@Slf4j +public class RequireIPv6Condition implements ExecutionCondition { + + private static volatile ConditionEvaluationResult ipv6Result; + + @Override + public ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context) { + if (ipv6Result == null) { + boolean result = false; + try { + result = hasIPv6Loopback(); + } catch (SocketException e) { + log.warn("Could not determine presence of IPv6 loopback address", e); + } + + if (result) { + ipv6Result = ConditionEvaluationResult.enabled("Found IPv6 loopback"); + } else { + ipv6Result = ConditionEvaluationResult.disabled("Could not find IPv6 loopback"); + } + } + return ipv6Result; + } + + private static boolean hasIPv6Loopback() throws SocketException { + final Enumeration ifaces = NetworkInterface.getNetworkInterfaces(); + for (NetworkInterface iface : Collections.list(ifaces)) { + final Enumeration addresses = iface.getInetAddresses(); + for (InetAddress address : Collections.list(addresses)) { + if (address instanceof Inet6Address && address.isLoopbackAddress()) { + return true; + } + } + } + return false; + } + +} diff --git a/tests/src/test/java/net/devh/boot/grpc/test/util/TriConsumer.java b/tests/src/test/java/net/devh/boot/grpc/test/util/TriConsumer.java new file mode 100644 index 000000000..5472e4a10 --- /dev/null +++ b/tests/src/test/java/net/devh/boot/grpc/test/util/TriConsumer.java @@ -0,0 +1,25 @@ +/* + * 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.util; + +@FunctionalInterface +public interface TriConsumer { + + void accept(S s, T t, U u); + +} diff --git a/tests/src/test/proto/TestService.proto b/tests/src/test/proto/TestService.proto new file mode 100644 index 000000000..c2c6d3e56 --- /dev/null +++ b/tests/src/test/proto/TestService.proto @@ -0,0 +1,39 @@ +syntax = "proto3"; + +import "google/protobuf/empty.proto"; + +option java_multiple_files = true; +option java_package = "net.devh.boot.grpc.test.proto"; +option java_outer_classname = "TestServiceProto"; + +service TestService { + + // Returns version information + rpc normal(google.protobuf.Empty) returns (SomeType) {} + + // Fails with an error + rpc error(google.protobuf.Empty) returns (google.protobuf.Empty) {} + + // Unimplemented method + rpc unimplemented(google.protobuf.Empty) returns (SomeType) {} + + // Returns the incoming requests as is. + rpc echo(stream SomeType) returns (stream SomeType) {} + + // Secured method + rpc secure(google.protobuf.Empty) returns (SomeType) {} + + // Secured method with only multiple requests + rpc secureDrain(stream SomeType) returns (google.protobuf.Empty) {} + + // Secured method with only multiple answers + rpc secureSupply(google.protobuf.Empty) returns (stream SomeType) {} + + // Secured method with both multiple requests and answers + rpc secureBidi(stream SomeType) returns (stream SomeType) {} + +} + +message SomeType { + string version = 1; +} diff --git a/tests/src/test/resources/certificates/client1.crt b/tests/src/test/resources/certificates/client1.crt new file mode 100644 index 000000000..c06679b43 --- /dev/null +++ b/tests/src/test/resources/certificates/client1.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE9zCCAt+gAwIBAgIJAPnoUKl4HMFdMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV +BAMMB2NsaWVudDEwHhcNMTgxMDMwMTYwOTM2WhcNMjgxMDI3MTYwOTM2WjASMRAw +DgYDVQQDDAdjbGllbnQxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +1RY1x0KZuWPAKCSkdLSfBMK3SWiqt3JZQEL50SyjbHRxPza+igfxvXCFW7jxRX2b +DRSN7m9RYB5HYz55hHUxNDq4n4O3ciB9NdNrt4LCgpRu9gy8Sj4J16H1zssJU0D2 +APtNaLKbZOcfyAev4dPBVObmis/TAhSF/QTSNnWBdVgA0+p0DMZ6M+F5qNpZKltn +VEu8j19BfFckRMZglkhhTMiI/0kzbr9Yki+OZCCLZBARFrFw3ZELTbGL/ezpPqjq +hpYkGFB39yyQmfeXKLMEOXPiyCCvXk6YZrwfItrXzVDh4HJq/SAQaoMs0t2csj5C +O4bixf9MD4o/jqvlLwQX0XjbbDLtNgUVeIhNEtF91NXcuYO8sCXLY12roy1RWR5f +9/Q7a6i5YNTBR0BZHLBNas34PziefAQrwU50djQ7lecm3RDKWHrz0WtVNH/Fugb7 +KLh+i47dh/mpJ/0wC9Xdb17E8wFLCF7viDT+Uh4UC/Rpr/3v4zGjBv6Cs/9oIaza +W4BxfoSsEy+ZiFWl0Ud07wl23sqUGgM4ZzcyHYv57zG3n/SFzMJ0UDF4FbGF2U/9 +PIbBy6QbAZbI6/WQXuWBFleyE7BDwQS/YF4WjIOGY7llhwyLSQi6EXurAR2ECYDE +njqvTfcLvphoM5Jn0c97Mi7PYr/mmKqX2Jv+SbW4Eq8CAwEAAaNQME4wHQYDVR0O +BBYEFDlQ8/CY8881ZjnMq9qBaOzmjmC2MB8GA1UdIwQYMBaAFDlQ8/CY8881ZjnM +q9qBaOzmjmC2MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAH4iOBLQ +oi2C2HPMqE8314Fv0swr8yjwHNf9vDvAkL939E/PUAxwSpaR7Gcxik8jauzbqqQM +gfZyGVXO3EJEXL5+XgkrcSeoWHfgGLa4saqsb+kIgStrI4iP5OFVrAgDuUh6WPYI +b3qQeQYblyLwuc25sboTJDqen9xMoEYXa4wWgNSLFdvupUJkKCZ6LBAT579lAZeV +Bb8+4JH/JmXUkPzfN7lJXlAelwqNSs2cQVx+RIrYp+Dz9BafYZMgS1MsqHR3XltK +5rsqdFp6WQt5fy0M8ZXm6jkv7szJtvFVBJB036pk3RUVJv8hUI6NBqipFM8vzXb8 +eWFp9AmXfjUBAPhSrfhNCORTfzDJswOTmWq1I73m43Pnwe6hx6QILjroIZNSA5Am +Peu1+hv/Gkj5EdfZdlIgf6I8BuEjlMpffhKt9Yn4tYV3o9yWo8peeLawT4avqBVd +02K9g7i5+YpMHZ5s+enWgy+/YVWNvH/3coNHTHDMNvmEy3EHfzswgnB9qkYUsj1W +kujB0PKBYvTRnIxry6UPGXh7i8WSOk67160lwTZMCxIZGuJAPu47n8ADCObOA8/T +3OE5kKWX7Gw3Hi2Nu3IWF7DAeYN2oMurqmigN9nl/dZKhJThZUZ1G3SBwXtT7mj1 +UOP7pQbCYj/fdqoMpwFPak+fQ+qHU3ostbsV +-----END CERTIFICATE----- diff --git a/tests/src/test/resources/certificates/client1.key b/tests/src/test/resources/certificates/client1.key new file mode 100644 index 000000000..0bf288a61 --- /dev/null +++ b/tests/src/test/resources/certificates/client1.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDVFjXHQpm5Y8Ao +JKR0tJ8EwrdJaKq3cllAQvnRLKNsdHE/Nr6KB/G9cIVbuPFFfZsNFI3ub1FgHkdj +PnmEdTE0Orifg7dyIH0102u3gsKClG72DLxKPgnXofXOywlTQPYA+01osptk5x/I +B6/h08FU5uaKz9MCFIX9BNI2dYF1WADT6nQMxnoz4Xmo2lkqW2dUS7yPX0F8VyRE +xmCWSGFMyIj/STNuv1iSL45kIItkEBEWsXDdkQtNsYv97Ok+qOqGliQYUHf3LJCZ +95coswQ5c+LIIK9eTphmvB8i2tfNUOHgcmr9IBBqgyzS3ZyyPkI7huLF/0wPij+O +q+UvBBfReNtsMu02BRV4iE0S0X3U1dy5g7ywJctjXaujLVFZHl/39DtrqLlg1MFH +QFkcsE1qzfg/OJ58BCvBTnR2NDuV5ybdEMpYevPRa1U0f8W6BvsouH6Ljt2H+akn +/TAL1d1vXsTzAUsIXu+INP5SHhQL9Gmv/e/jMaMG/oKz/2ghrNpbgHF+hKwTL5mI +VaXRR3TvCXbeypQaAzhnNzIdi/nvMbef9IXMwnRQMXgVsYXZT/08hsHLpBsBlsjr +9ZBe5YEWV7ITsEPBBL9gXhaMg4ZjuWWHDItJCLoRe6sBHYQJgMSeOq9N9wu+mGgz +kmfRz3syLs9iv+aYqpfYm/5JtbgSrwIDAQABAoICAQDGTyU9jH4ESxFKSs/CzVYw +2AY8C2zVzVXCuJJGWYS+KqA6dHhffYU9CLsT4cqpIXxrb/WnMXJKQeOuOTZeT7K7 +KOspiSvwysu8jEZzQv5DCGE8HB4f+hyu0cwx6EOFd696umYQSijUf0TUpFLLmIcU +vM9R4JhAF1mbYCFDDZit3HjowAnA2fZg0janLo5NHUPM769Mvxny3iai2vyI4FOt +YfQdocJ8Dxge9KNQC/mt5kYBGKIxkgszA0mFyDZKWmnpV1HATyNWpeLtZ78F4ZTM +40zrc3/CBONQuUNA7Opp6hxNBi8L5AtjFTFiDV0HhWmefKoQGshurfr4jIF+gBCo +7mXJketV+txYlKwLrZ5yHlqJ35b+jZ8eyIu3eg/oJfk0OkkmWBqJNKe/0KGL9iAA +2QduRU8KCmTLEE9ipv6frvwtUzWgnh/tx/vr6SEmbwnth/cNPtPtDl4sI7YDALoW +mCt9ziA/LmhQ7NiD2ufSdIwxrEkftGfvmEwtFtaaNr30HkXgdY3334FFqQNy6e3m +F8IP4ihuP4V8fUD31Gqu+QRHX9c2v5yTRwhFsep3Q+5yPSZbHthU8tXMh4xmDhG7 +VXR6EZh6kBBp/uW8Hv6oPyX8zuLQ0mXT8/oCfGJD1KslM7bBCsqCtNVPTTBhRhl4 +D8iwmZfortduEVIdoO3OAQKCAQEA+Wr2fzM2XUKOYnVp7Bo6lULgeESInaU7rbt/ +wXWLAziHfmASuEAX3kRrU7/zR6r5VxVkbkNjGKQS1ed08aQmoTfgzdpkkoVCBMYd +vtBfQxQE+JcF9QQzPQhGUsj5lAK1t0/B63sowq6908uYEUJka1283qisanj9bNDJ +YC7mqinkZvtLRdZLQy018+vyC0Llp6ui46XuGX4CB892L1orZEnOn7bhLNDXDfyL +Y1a4xp0pu8Gz4a6IMwHlELQVM+ZxvJkdKQav19rHSDN6YSnlyN7Fzdivdc5iPvj1 +q+lQfqjhyecKuqjMv2nm8CaKv8sUMvSYQlQJdUC4KH7R5kgxjQKCAQEA2rXMgLv6 +7blQSdGdPMi9uW3kns6GeLZ9NldKWQYatO+5T3RTsiYj1Cq1Ri3cNsEaukbArilW +VBjq9RT77ZD1qb7IWik4l9eK72ICuE690e/iZgeq+31G5JtDb/eyqlykWyjPVRTa +tq5ke1l5GHSFCD8YwuSUAFlu1wShaK1W5DuzsLkX3WXXlOFJVvFcOV8ZNQiVM8ob +W/IEd+zF5TWkwrzKu5A8WE+VMd9qXQZkTdnNTfx/shcaP97v8AnDRR0nzJewZD1I +4Rgy8Y55iP+EZVGVgXPgiUoUZordjJTP4SONTTTVoVdWUPfVwa9804m2HwqazMgB +nUxCqgcyBabAKwKCAQEA6MGX7RZ2qktdIqbdxTao6vfxEGssbqlu6u8qBvjRVJ0h +XJ79eTJe085Xtl1QA9abP1g4U+nRBYLADRwldwcwAY1q3c/rwQxYkqnrwlEkRS3e +gbo2FfPoeRFKxyDEQArTV5E3r7BMVnnJrme1ro2dttBQXB1eYTItlam182txWOUl +P7FNCowzyinbypiqVTYFqRY0OWt0qhd8f5tseD4Wdk2mZEe/FRqDRosh7P9WCgtK +kevvGTHN6oPfJ1pW2ws3or/khY+286I4DLn1f80gofa68yE+hJqn9opQgTCHLNwf +cVKKIa06/XovyHa/TsKORAscN/HCrchK04eeC9/dVQKCAQAOu9aWCZPi9ev5vRxJ +nwBhAI05QZJ/iVOwGHtSuf2MmOWz+5Mz2ivpvCLQIoDGU4X4bJHIadJ+Adu6PPqn +y28xSz/2CvbcC9I5RDIIto1FGlL3KqcTICJpfigx387yeSE9XudwxOHjEqaERMfK +pPbdUlrZrkpmJ6A27gHtqlfDMl5tllqMOLMoPXESYVokJ0rsbrKWdZQYQpqYdaPz +SATcdZl2v0XBcUMMxA9HSwnw0K5rBYqYtcO3783cLtwvyMIIn2NrrVE+kMHF0iJP +317M8I0Q1nyW4x2ytsIsGU5TzXBUdi26G8cd10RYMvlGyu0w9Cbvir3JGf0XtTpi +dRRXAoIBAQD4cWNspA/QQzvWZKFQou3GVlZ0Zqv+KLm5+IMGFELbng6ZS4T+atdo +GFuBJVd39G1ZUxzBFMCjefbAVv+xPt/Czqg1JFvDUmNDs9b+8ijLxXImxM7GS+Py +R9GbKPaeUm7LQjjfl3zynld84jhkYsHtwdA0NsgJ1LB6dBcX/2ARzH+vK4rUwVcT +eCz1cJutHyaYKG6mOirCdzX485Boi43zCCSTJIrS9kfWBQPxpZ8XTiY8n0ezcqDh +qIaNF0bT5TYWOdDClrProxkFe+CCzgNqKuxz4aE/37rBXex1dK5GrsoABI4bYKJ1 +XVbjK3pf28R8H0CaxBOgU04cZWP0B6cf +-----END PRIVATE KEY----- diff --git a/tests/src/test/resources/certificates/client2.crt b/tests/src/test/resources/certificates/client2.crt new file mode 100644 index 000000000..dcc864db6 --- /dev/null +++ b/tests/src/test/resources/certificates/client2.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE9zCCAt+gAwIBAgIJAITU3DA0N61EMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV +BAMMB2NsaWVudDIwHhcNMTgxMDMwMTYwOTM2WhcNMjgxMDI3MTYwOTM2WjASMRAw +DgYDVQQDDAdjbGllbnQyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +wnfZ3NfEJEyyuKJ+v6R/qIBjHvdwwy1CPLquGI08YnOpW/MEIYHUJZ7C3JNFKkCI +g8VqFvsI9cho2lpSpucAly4T40js/2gRuUVKrCeOQDy22ihDVpmOCGINEqnRzSm8 +nEXgmPnuBssZ3zvHk3xA1W4A/JzjFM2elZiXozoxj77wNSdxGS1nWsCDSkjXsxxA +h3I2KW/N+SJmH7r61uL8/zyHsrYX664OpdRXkq16iRbT0JGLWXTWr2CqtwCEtpQi +AAFUo9CJTYtxL3viSeDqQDcHmTCzIsWuwmoc0VIpkKYG7877vn91BUG9qy75UZLW +n0w5N1yPaYjsoUbTEXXv8KSUP+iwdzcB5NiRcygXzOmu3/+zG6wC9OjAIjfD1qm8 +d1uWojKqdt1X1XWqCTXJJZLn1djU2L0vPxmdcFjOHYufc0VwZ/N1I/K7/wOofE1L +oYWd9Pn0Hi7Wpu6JnWpzLVp/hfRLqwzdPXRwH6aiQIb5wSC2wHtp+lZE2Sx5ZnjW +0e+ktMUyF4YMfAHFNCpwZVMdhA3FSGnSlfZn1HLpWqTTxhofgMvbnJEZ2EIyJsTJ +FtRbfDzcjRDeMHvnnYAciTDQd1uB32qeLM16BsT9xeSIviZiDuz9RJixPn9NDG/i +dazyRkq70im1Il1DRY+yShe0IZGat/HlbmZHmB97fZsCAwEAAaNQME4wHQYDVR0O +BBYEFIgQJ961yXwGcoqPBUcUx7m95DXfMB8GA1UdIwQYMBaAFIgQJ961yXwGcoqP +BUcUx7m95DXfMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBABW12bdx +G1zVT8rPL8afZ6zdLmGPlIdF+ARqh2nxpD2W/8Z6fulJ+/SsJJcZTFApz4337adJ +85i41z0SDzS1Zcf0blgnNPDBZF0U1odVPVqBYgVQbzNHosty9j3pPZi17QHzz7EM +TXXkvoZ2a/agotAmmhn2Zbg4G/6CYsfAy8FM/KG4uFgOIVBdlT8ELXm52M5JNVzr +0TncHz6FauRNw5mXicvbwZnkjyOUQzx+0fKO5ojPoeqXGzx0QndZkulx+OLI/7JD +CybbAfgM83ll1ix7DjVGUiySo5UEJZw/pPdKcrnmvgoN0Wmbm/MXfmwD9eYPIyoZ +rhxxdg/h/SYlu6Pc/qinx8EARmVa2FVbd0Ajo6yLAsQyAD4P9qc0+gDkQQfm4UE+ +XK5/jIwJ6NxAawPFVM4HIw0kTVfCDaiZ1f06enic0wKmtIE9h2gPpG9dhAEnpzv7 ++a50NNC1B94SNcJHktB599mdEuFxr7Vtfw7GR1vJieHAYcFXSbkTtmeoOMVbDx3H +RqR4V+AfMTrEBj5fh7TC/2KT3nXHy4JxOM9364APiOVe8Xqlo1w7RDokzwD2IkFv +gHCujHb9ijxjyF2OYb0fJxpBSOqrKqCjifDnUPEzFWPCj1YnMDtKIv/ykqMn8Ggj +P9V4+P7NgLkkZTOz6Qo/bLe/75xEQLX2TsV3 +-----END CERTIFICATE----- diff --git a/tests/src/test/resources/certificates/client2.key b/tests/src/test/resources/certificates/client2.key new file mode 100644 index 000000000..e9afc6d4b --- /dev/null +++ b/tests/src/test/resources/certificates/client2.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDCd9nc18QkTLK4 +on6/pH+ogGMe93DDLUI8uq4YjTxic6lb8wQhgdQlnsLck0UqQIiDxWoW+wj1yGja +WlKm5wCXLhPjSOz/aBG5RUqsJ45APLbaKENWmY4IYg0SqdHNKbycReCY+e4Gyxnf +O8eTfEDVbgD8nOMUzZ6VmJejOjGPvvA1J3EZLWdawINKSNezHECHcjYpb835ImYf +uvrW4vz/PIeythfrrg6l1FeSrXqJFtPQkYtZdNavYKq3AIS2lCIAAVSj0IlNi3Ev +e+JJ4OpANweZMLMixa7CahzRUimQpgbvzvu+f3UFQb2rLvlRktafTDk3XI9piOyh +RtMRde/wpJQ/6LB3NwHk2JFzKBfM6a7f/7MbrAL06MAiN8PWqbx3W5aiMqp23VfV +daoJNcklkufV2NTYvS8/GZ1wWM4di59zRXBn83Uj8rv/A6h8TUuhhZ30+fQeLtam +7omdanMtWn+F9EurDN09dHAfpqJAhvnBILbAe2n6VkTZLHlmeNbR76S0xTIXhgx8 +AcU0KnBlUx2EDcVIadKV9mfUculapNPGGh+Ay9uckRnYQjImxMkW1Ft8PNyNEN4w +e+edgByJMNB3W4Hfap4szXoGxP3F5Ii+JmIO7P1EmLE+f00Mb+J1rPJGSrvSKbUi +XUNFj7JKF7QhkZq38eVuZkeYH3t9mwIDAQABAoICABwY7vxunGNWRZhRuhh8bwls +i2XFAKDioChgHJllhqz+4yBq61TgCkPpPWRbTun86vyHAH4ThUblzuiFll2REM/H +E46KUdvRMIZhUZmwGWiEnTLGEHypsRDbAeKJcMvA1QSLQBk6Oz72B2XuoDZaBJAR +1cip7lFqKBibNIWb+d84CkLT0Q5/Neix2gZYofm6AkTPpq2z/sGZS8IX5Pg5Ua6b +E0wp7SjbPDiPhpulvwehqbb/4G9rZz1trRzF0WcD3im8B3unvL7cf90JfDeKbK4c +hVeH5soGX+qtJD7GLUU+B9CMSmlKxyCcUAg0wEcd/S6E99tc30ezReVDc2mZbbJJ +v9T8juRsiZjbNnNYdzsa4sz9BbZxk9PLI4ynhIkxMpWysZYzoUt6n2Mdvqe1H21r +DqPTt7NyVO7IR5SBASNYIWwQ9wuefGwUWYer+ejFfbcmCAOtc9IjXCWTr17AqhFt +aOJ0/CHrVYX2aw2vivPjCpMsWq7jkJJ5iExlUVT7VMfgJXppJKGo/bGT2wFzK+a8 +Yz/ASF2gH1V3SVEqJvFIaTgL5r8K7mA1EY5VRA5S+Jom5SzCqKshWsZ4YMy3O7m6 +wjntldTTFLw/7K33Elei0dmLX/pdYf6zvsN2+uCC519tW4Lz8uCc8UUJcJcJCpHX +7sY19yEG9fWBdOv7s1KJAoIBAQDnnLj7XOksV1h4ArxvZQ0Xfb1s/KnojPkdfUum +G5TvFGfSFN/sUZiavTKGsOok/0rlTYTG0L2g+P08Y5f+MKU1EI4BbBrBzbROSmW1 +yuLO20DCIhv3ANyiOCVL3GwL4exvn8X/XKxb1owHCxrwtZK7SU/CYSV5Mudo2x0s +HfZ80OcCGw02SOZjMLeP16pEBO564C2utml7VTcGnZ032Nl+sW/q1cLVwFGeLKbE +en+mJ2UshByE7xNmsMxChotrm9Dffc3EnYfXL0uIH807zTF2WcHTIdcL3uIYoJ73 +/7JU69KxqySykfdx+ebotccj5il4/YeTDS97kx4ISSLAz+v3AoIBAQDW8eI+I5gd +RCB7uE5yjWXRAUIny4UBSLBdmHA8yZKLs6J+vS+Z0dKOVZg5TNadv5fVQy8hjBcp +gNF/nnNOYK9EZYaahxV8fMki5laDN9norPl3q+zDDTimUoLUXo+/7WpZoWHgXt7p +fdS12XwjRCeJfl38Y2oDckD3V1okl5xODjZHLZw9ljKv+YSnOe7Yn0RVmqIqLF5/ +1YuHVItvE/r2Nqexmu5x5CnYaTjEurgLD1HovrMgtLu2SrPrwLFmYbYPhPvx5neB +3zQywP1xSDCVZifK15j7QJwJ/OiV607BnSq8CxOomxBxOclRogBBhj5YdUeYzwpd +o29kjVRlh2p9AoIBAQC2rtsJCKOyIcLaBe5zPpUw7jC3AiNSFb94DxfYEPFMEiBB +h82HLGTdyFVN/8TvIZ4Fdzs/Re4MRdgYBcYg7GWikUgwvv/r1UBecDgBR+HVnwJZ +HWZJURi1qutgBqACT1SaRr95R7N7TKJt+8hoDA+MQarzeoSAMDJudkVwQsHkeTF8 +a7HkG2P84LQodMcLl4gyyxe9MovIh9I7GZ6kKhqC35mS9MAUsPivdjCj0KtWdsRK +dHm6MIhw+wphfpYBbok1fpkKd+ZpSBifadYLUTGuU+WZjpt79XUIT8iTe89BRDCP +ipoLby5pa99Btf84xZX4pKCG0GOfaM7LYkRTKKzJAoIBAQDLRbEqBstiUf1OQ3yc +xK/XOpNvwv6jujRgLztloVNfnqA7r3qGw3GWfcgZ2FrWkExsuL6nwS5eZ564TAbL +xo+55TGojbt6ISCuSpriOT1w17SMwiCETcqXMEzdvhe/8Cy1WvOPFulEc2VoHKdT +Sq8BTmpftzyYycI6p3duR5rgDnyyT6YEcDi7RwN2ikkgv24GbtiRfahYyIDNmNzM +TV723bU1N0nsl1Qjf07abaKDgxd5Pm90rLcgrAD/IRojsqBUiPUjTUsTnRxmKzED +orufbh6Pq9jXM8DdiToEHaY7YAD8GJWczBh+m6GR+9y2Gth3G2J8VsB80YUU2LtJ +0QiZAoIBAQCefxscGpIPML+vRGMDc59GQ+7KpOjmrAHhYTRTILH6AD7QehH3qDqJ +u3ctLtCLadtHjlIjWGSAZZtcGGBvlFDkC1c4qKsiRcYGZq89XrBDO10H42stTFBQ +C9tnJge5cbQzxd1Ss10+cDcOmeP74Xenu7OACyF3E+CYpM5b74JC7sVvlFG3/H7o +3hdkIucL7pmSsuYTMkJDR/6nSx2feydCqpPHS2gEpT93aK0uhvWnqgZn83TatkMU +KwedoztWRFt0tvz7nsSOLm0cZOQcS1JYKyP9vwypF6AxNgGdsxVbCvQTkNBJd5I9 +JymmjVY27rOAXIK2j6SxPtFWCFSV7xpm +-----END PRIVATE KEY----- diff --git a/tests/src/test/resources/certificates/generateCertificates.sh b/tests/src/test/resources/certificates/generateCertificates.sh new file mode 100644 index 000000000..688bcc662 --- /dev/null +++ b/tests/src/test/resources/certificates/generateCertificates.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +echo "Generating certificates" +openssl req -x509 -nodes -subj "//CN=localhost" -newkey rsa:4096 -sha256 -keyout server.key -out server.crt -days 3650 +openssl req -x509 -nodes -subj "//CN=client1" -newkey rsa:4096 -sha256 -keyout client1.key -out client1.crt -days 3650 +openssl req -x509 -nodes -subj "//CN=client2" -newkey rsa:4096 -sha256 -keyout client2.key -out client2.crt -days 3650 + +echo "Generating trustCertCollection" +rm trusted-servers-collection +rm trusted-clients-collection +cat server.crt >> trusted-servers-collection +cat client1.crt >> trusted-clients-collection +cat client2.crt >> trusted-clients-collection + +echo "--- DONE ---" diff --git a/tests/src/test/resources/certificates/server.crt b/tests/src/test/resources/certificates/server.crt new file mode 100644 index 000000000..b7ba4c3b3 --- /dev/null +++ b/tests/src/test/resources/certificates/server.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE+zCCAuOgAwIBAgIJAPaN4CpLQzLtMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0xODEwMjYxNDI2MDVaFw0yODEwMjMxNDI2MDVaMBQx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBALYsfU9sSTCsO5+ksdLi2CwPAl2EFuVL7KbwCDxkeOh733ckWbdLE4guTvtj +kGff/dOaGhdUnqSr4CTZRDJKmkzdHeTZToChhOu4cfr//uzk90yF4j0XzUB0Aye0 +F/fHaY4XjPAnwfZf8Xuy7JCJ8WCBvK9SrxMxpwTEpCL3QIjHYeA/UTBjI/xo0LF7 +VIncRiJQnpmS9TuM6dnvJ38ezfP+fzF+8izjvX7J5rooTbgDxXNSvdoHv76C/TQR +ANTV+M+zLuDb/sl5gMWOHRpkzj4BFDUScdHU4uKT7XXkqdfNxg8rIzxrUGNXoBvL +4ETLbegkhqIY3oCtR+tbKE1145mRFjcOzvkHmhGqTkCdCIKcTn5ReXjgeSQDjlRX +VEwvBQlCSfyDlnaKy2adWAC7H1SW2PVwCALCgA88kmJEmqbOJcOiNiKFDzHaBhVw +7hSGrXa3Wfj7wdWrdtoQLtSc2MkR0qNcfnEPKGz+sU/cLtdbB19ut36UbfRJAyC7 +galsHtvLDVRWnm/z1uWUStNye7S9EDCEbiG802jwnnlfzLdpkmuTnliNngg2jJO2 +EX1HYXzT4ezsMJLZV20uAm2VUt9fXDnyofEpLRIxTcQVD9mzIYh92EK6yCcXf2ms +golS3pLAYWMy3OQ5Xfb+aZoYdnZVWFl0B23LlkniPE011HGlAgMBAAGjUDBOMB0G +A1UdDgQWBBTlnzunEY+CaZH5qymJRwDBqz14RDAfBgNVHSMEGDAWgBTlnzunEY+C +aZH5qymJRwDBqz14RDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQBs +we7UZl/BDKKjq+CFycF8UKORoHx0UVBZh7WQxzL4o89lTAbBs4drVLS6Q/YBoFLp +WhCdtPeIW+Snm2YIAFQXvgCoZZ0vDQwmsjBCXI7/K9WvPvylYF9sV53foyppEZAy +ZRosESq0407I0bTx3F5Fs6OxI6cc9DhMHmdj/YZ1/BcHZZQPSmbBW+82Dh3qB1ez +AQpEwi+9kW0nAbxK5SIBzfH3EW/XNeIZF3AAp+4JQdXi64utnrq2PByNvy2ac6xo +rP4hQIPRdnzotDPtK/81BA4ZuHHh2bkvEkEq3m/3KRQG3qIigYOwHWPcFFHef2Pj +vDJ/ah+/oAYTiH52H+zThUn9FfjeBfSJDpwsbAz34PQVhdFvahcuKEEeRolsUWTS +pXS0F0aecae+2uC0UUPSXSmQ11tgddBwmpTb3VEjXGdw0q+4ZCN25L6Oir/fmmSs +7Vqw9lkWKsSSpKPAsydHUJHZFAWdjEEQcGq4wb9sY8hWk8sQAvrISNZtNTikjzZr +hf42E+j5DTaAjNu2oMStzydVzCE5bhpZ+0rPcKdghA+6etNPtt+LwxzYpSrrlo7E +nCW2u6ZUm6JXON2u/INhoGQIXX1XWuDObfn2/5XwcC1lcYvgXiUJhtSzMQEwPi6H +o10MR/UJTafCwx5Fj5bkecYvbYkcwRNXi3W5cpSCLg== +-----END CERTIFICATE----- diff --git a/tests/src/test/resources/certificates/server.key b/tests/src/test/resources/certificates/server.key new file mode 100644 index 000000000..df3415e30 --- /dev/null +++ b/tests/src/test/resources/certificates/server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQC2LH1PbEkwrDuf +pLHS4tgsDwJdhBblS+ym8Ag8ZHjoe993JFm3SxOILk77Y5Bn3/3TmhoXVJ6kq+Ak +2UQySppM3R3k2U6AoYTruHH6//7s5PdMheI9F81AdAMntBf3x2mOF4zwJ8H2X/F7 +suyQifFggbyvUq8TMacExKQi90CIx2HgP1EwYyP8aNCxe1SJ3EYiUJ6ZkvU7jOnZ +7yd/Hs3z/n8xfvIs471+yea6KE24A8VzUr3aB7++gv00EQDU1fjPsy7g2/7JeYDF +jh0aZM4+ARQ1EnHR1OLik+115KnXzcYPKyM8a1BjV6Aby+BEy23oJIaiGN6ArUfr +WyhNdeOZkRY3Ds75B5oRqk5AnQiCnE5+UXl44HkkA45UV1RMLwUJQkn8g5Z2istm +nVgAux9Ultj1cAgCwoAPPJJiRJqmziXDojYihQ8x2gYVcO4Uhq12t1n4+8HVq3ba +EC7UnNjJEdKjXH5xDyhs/rFP3C7XWwdfbrd+lG30SQMgu4GpbB7byw1UVp5v89bl +lErTcnu0vRAwhG4hvNNo8J55X8y3aZJrk55YjZ4INoyTthF9R2F80+Hs7DCS2Vdt +LgJtlVLfX1w58qHxKS0SMU3EFQ/ZsyGIfdhCusgnF39prIKJUt6SwGFjMtzkOV32 +/mmaGHZ2VVhZdAdty5ZJ4jxNNdRxpQIDAQABAoICAAPejgrR0fwmrF2Hsf84sj69 +Ut+JnrLgGfaxwnsF50uI1gZBz2YFQjTyRtswEjEioNGNxGAafAQjYm9l7fzjeMOJ +LsldlD1HMPuQv1sFdPSzKE2HynZhuwpTbH27ZYbtxWAvP98Z+RSylP57nmqzg1z5 +ZHiU2QMfWtzPEG1tswE0uu94aNEOSgVidIxQgksrUpIIBFZeuLa7F3x5h+07SkSU +cz8p4xrw+2VmBHCPypbN/bOlXkLzRy9UuOwSMGJU6SQAOk6sXY0J96I6koOtAHRw +sW0vJxu92dPWMIUAhlr/E+XelLED1oFFqzDr86eCaqMzo2ht/svc4lotZX2/s0XW +ko59BcD02ZnVI9O5pAQucC29IUg4D9W+v/HsyvJkFUQHWYapH3MIO3wCRUkHKL7P +ICKc0x1+EkNP4iOXONH1dUSjWTIJpfgcCBgqpXw8AmwgS4QHVcFBFMuxLZcp/esd +Nqx7s4iNFXjIIFhmEc3U62miInoqVzUdmAvQjaqX+0mewlIl00uG9zE9wODXDEo9 +tPJJmux0vqZZyOWbLsrKp/3oOK/8beJHlDsXPIu3QYqAvof24tD2lb7WrsHmrhW2 +RKbamd2IP/7rC45tVFOaZ5ERUeWZCIzdKyjLqRFW6YlsZVnjQFRvQ86oEqnn3dcR +gCl4iZ0Kvl0tSi0P579BAoIBAQDYb0mmkDoiNlvww5YWKHlurMb3NjUDmIs5LshV +BlpyYgqvLlOBjzAup4DAIWKxWsGzg+iOuqck7fLMEUfVhP0KoXkJXiark6ldRqGF +7umO7gOe4AwhkmKmwPExjRcFG0+x/kOVfTpLANtLKh5HCNKquUpGR3CG6ixtkBHZ +rGqUEYc5WAbu3dKVKTydTcHkqyx0Yv4Wg7j2Ked1Nw7yvn3fJ/Y13ZrOQelyqjvM +YJ5Lh/qwu9QG8yyTiiItRHpoPq1oiQ361cY/uch9sEeh8eu79VwIwJ5YuJ8PSb++ ++3fdqyXiR8kSk2jTX0GYYqnzhO2mtxs6hQ5i2+FiOPr5m5hnAoIBAQDXedvUVqhZ +Jii3B5CCgkaFGxAfl51c3g7Af8pm7P2vdV1bplMv/DY8C5Pi3EN5QgFBr/L02LOs +VBBGTnD8/G861NNPrcw+/5+o2SnZxf6OEFgc9LdczMQm6Iaq5tO9Du0HOZROW6Z4 +Ay4Y1oufCtFt3oheHlgzg+F0+6BtVKqtxmpDAaQu1+f9EyOkNTw785sLCGqakQTK +gTZeHfbqxoFdxBobzfHFovpzPRIgMcyhIr+Cl0EYOQNwtMFcZwm3YV1NMdyoN4ug +V3yi+eewZ1I+rnpFd3ktb4zdieZ3e9mNrvMiAOf+CkaB54z199yNczN4Val/kA8o +1mB4xpEd6Y4TAoIBAQC90DwDfBG/13SinqWOIUj+K0EDpeKwmKPhLoo7JytDjYBZ +SoOp6G4VPInJ9n2blUCzs0fNhRz5YkXBepZJSCyzmhGQiaXYa9PpHfyifXkQBOXf +/BYniz5BiIz/LAG4VM98BsY24HCzPrkUHogXPEGlwILHR/gEGnOEUwmUoYWG/ihd +vjm2W4xHjLbALmWRqh1+pSK5lCQun2mCfxr5AN9bSqy/aO5PXbbi/TEceM/a5hKR +1OiKf9HkQwzeLmQ78FwchbZg+gK3+LNAp8zq1kQrv29LrcIxhRjaS4+CawAgw+yh +mttZEzanya65ei4ah8X0pDmZBQAs6zGq9tYE+tSZAoIBAQCKaXqucWP+sCZjO/a1 +/t0xz2qSAKBS8UlkmjH337AtryRjJPo2zro/4+gBSCAHmkCYY1+brD/uKKmzn9uw +hq8kiWWbvzZ+GVID8kuR2j9kHlebcg7/C4HMxH0M9u14ekgD5hbAJttOKCzKQ7a4 +WQiinNnYK4HYxZRjwucQk9x3eAb2N+2xMXuR82NnrdKdaCRl9+gSlakQM+QqnPDp +as3a34ct6SKvI7vhno8wIw0hTOLcDjEAUE9HbLTwmDeDloWKescVP2jvfINRZVpQ +1G3eWdGtIcuzRf6kqFpk0iezfXgslYxwgUU9WPIRZkBElIWxfMU+bdlvEBY0Pskk +/8wFAoIBAQCTrD8x2Do3uXrALyrw5YoAmXLBkk0FGobJoogujNxtvC0/5Uh60ppG +RTdemOGLAoqo7HXv2ctcr200XO4OnseIL+EWc/nNfDA1XWXN8PJ1PfRmNvhp9UnG +TvutfnmtekYeVNdzPZqEZL9ihx9z2k06U9PIUTT5gF4jBQ32h+RtE/fv3hN/ViwT +8jZzModexw/H3X5VQS0A7yhsO4Q9qpDBwSZvn8hnqcvqLK4SX/fYdgTkwEHJY3rS +U+Npw+PwF8Do2zYNHPrfu2TJtqaSlqg3OowhgnwXIKcOiFHiZR7Dc8VEpzip//fW +ZUVsm+rz0U/1q8OmtVPDpXJ+kdIcIJzI +-----END PRIVATE KEY----- diff --git a/tests/src/test/resources/certificates/trusted-clients-collection b/tests/src/test/resources/certificates/trusted-clients-collection new file mode 100644 index 000000000..a4ffda040 --- /dev/null +++ b/tests/src/test/resources/certificates/trusted-clients-collection @@ -0,0 +1,58 @@ +-----BEGIN CERTIFICATE----- +MIIE9zCCAt+gAwIBAgIJAPnoUKl4HMFdMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV +BAMMB2NsaWVudDEwHhcNMTgxMDMwMTYwOTM2WhcNMjgxMDI3MTYwOTM2WjASMRAw +DgYDVQQDDAdjbGllbnQxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +1RY1x0KZuWPAKCSkdLSfBMK3SWiqt3JZQEL50SyjbHRxPza+igfxvXCFW7jxRX2b +DRSN7m9RYB5HYz55hHUxNDq4n4O3ciB9NdNrt4LCgpRu9gy8Sj4J16H1zssJU0D2 +APtNaLKbZOcfyAev4dPBVObmis/TAhSF/QTSNnWBdVgA0+p0DMZ6M+F5qNpZKltn +VEu8j19BfFckRMZglkhhTMiI/0kzbr9Yki+OZCCLZBARFrFw3ZELTbGL/ezpPqjq +hpYkGFB39yyQmfeXKLMEOXPiyCCvXk6YZrwfItrXzVDh4HJq/SAQaoMs0t2csj5C +O4bixf9MD4o/jqvlLwQX0XjbbDLtNgUVeIhNEtF91NXcuYO8sCXLY12roy1RWR5f +9/Q7a6i5YNTBR0BZHLBNas34PziefAQrwU50djQ7lecm3RDKWHrz0WtVNH/Fugb7 +KLh+i47dh/mpJ/0wC9Xdb17E8wFLCF7viDT+Uh4UC/Rpr/3v4zGjBv6Cs/9oIaza +W4BxfoSsEy+ZiFWl0Ud07wl23sqUGgM4ZzcyHYv57zG3n/SFzMJ0UDF4FbGF2U/9 +PIbBy6QbAZbI6/WQXuWBFleyE7BDwQS/YF4WjIOGY7llhwyLSQi6EXurAR2ECYDE +njqvTfcLvphoM5Jn0c97Mi7PYr/mmKqX2Jv+SbW4Eq8CAwEAAaNQME4wHQYDVR0O +BBYEFDlQ8/CY8881ZjnMq9qBaOzmjmC2MB8GA1UdIwQYMBaAFDlQ8/CY8881ZjnM +q9qBaOzmjmC2MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBAH4iOBLQ +oi2C2HPMqE8314Fv0swr8yjwHNf9vDvAkL939E/PUAxwSpaR7Gcxik8jauzbqqQM +gfZyGVXO3EJEXL5+XgkrcSeoWHfgGLa4saqsb+kIgStrI4iP5OFVrAgDuUh6WPYI +b3qQeQYblyLwuc25sboTJDqen9xMoEYXa4wWgNSLFdvupUJkKCZ6LBAT579lAZeV +Bb8+4JH/JmXUkPzfN7lJXlAelwqNSs2cQVx+RIrYp+Dz9BafYZMgS1MsqHR3XltK +5rsqdFp6WQt5fy0M8ZXm6jkv7szJtvFVBJB036pk3RUVJv8hUI6NBqipFM8vzXb8 +eWFp9AmXfjUBAPhSrfhNCORTfzDJswOTmWq1I73m43Pnwe6hx6QILjroIZNSA5Am +Peu1+hv/Gkj5EdfZdlIgf6I8BuEjlMpffhKt9Yn4tYV3o9yWo8peeLawT4avqBVd +02K9g7i5+YpMHZ5s+enWgy+/YVWNvH/3coNHTHDMNvmEy3EHfzswgnB9qkYUsj1W +kujB0PKBYvTRnIxry6UPGXh7i8WSOk67160lwTZMCxIZGuJAPu47n8ADCObOA8/T +3OE5kKWX7Gw3Hi2Nu3IWF7DAeYN2oMurqmigN9nl/dZKhJThZUZ1G3SBwXtT7mj1 +UOP7pQbCYj/fdqoMpwFPak+fQ+qHU3ostbsV +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIE9zCCAt+gAwIBAgIJAITU3DA0N61EMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV +BAMMB2NsaWVudDIwHhcNMTgxMDMwMTYwOTM2WhcNMjgxMDI3MTYwOTM2WjASMRAw +DgYDVQQDDAdjbGllbnQyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA +wnfZ3NfEJEyyuKJ+v6R/qIBjHvdwwy1CPLquGI08YnOpW/MEIYHUJZ7C3JNFKkCI +g8VqFvsI9cho2lpSpucAly4T40js/2gRuUVKrCeOQDy22ihDVpmOCGINEqnRzSm8 +nEXgmPnuBssZ3zvHk3xA1W4A/JzjFM2elZiXozoxj77wNSdxGS1nWsCDSkjXsxxA +h3I2KW/N+SJmH7r61uL8/zyHsrYX664OpdRXkq16iRbT0JGLWXTWr2CqtwCEtpQi +AAFUo9CJTYtxL3viSeDqQDcHmTCzIsWuwmoc0VIpkKYG7877vn91BUG9qy75UZLW +n0w5N1yPaYjsoUbTEXXv8KSUP+iwdzcB5NiRcygXzOmu3/+zG6wC9OjAIjfD1qm8 +d1uWojKqdt1X1XWqCTXJJZLn1djU2L0vPxmdcFjOHYufc0VwZ/N1I/K7/wOofE1L +oYWd9Pn0Hi7Wpu6JnWpzLVp/hfRLqwzdPXRwH6aiQIb5wSC2wHtp+lZE2Sx5ZnjW +0e+ktMUyF4YMfAHFNCpwZVMdhA3FSGnSlfZn1HLpWqTTxhofgMvbnJEZ2EIyJsTJ +FtRbfDzcjRDeMHvnnYAciTDQd1uB32qeLM16BsT9xeSIviZiDuz9RJixPn9NDG/i +dazyRkq70im1Il1DRY+yShe0IZGat/HlbmZHmB97fZsCAwEAAaNQME4wHQYDVR0O +BBYEFIgQJ961yXwGcoqPBUcUx7m95DXfMB8GA1UdIwQYMBaAFIgQJ961yXwGcoqP +BUcUx7m95DXfMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggIBABW12bdx +G1zVT8rPL8afZ6zdLmGPlIdF+ARqh2nxpD2W/8Z6fulJ+/SsJJcZTFApz4337adJ +85i41z0SDzS1Zcf0blgnNPDBZF0U1odVPVqBYgVQbzNHosty9j3pPZi17QHzz7EM +TXXkvoZ2a/agotAmmhn2Zbg4G/6CYsfAy8FM/KG4uFgOIVBdlT8ELXm52M5JNVzr +0TncHz6FauRNw5mXicvbwZnkjyOUQzx+0fKO5ojPoeqXGzx0QndZkulx+OLI/7JD +CybbAfgM83ll1ix7DjVGUiySo5UEJZw/pPdKcrnmvgoN0Wmbm/MXfmwD9eYPIyoZ +rhxxdg/h/SYlu6Pc/qinx8EARmVa2FVbd0Ajo6yLAsQyAD4P9qc0+gDkQQfm4UE+ +XK5/jIwJ6NxAawPFVM4HIw0kTVfCDaiZ1f06enic0wKmtIE9h2gPpG9dhAEnpzv7 ++a50NNC1B94SNcJHktB599mdEuFxr7Vtfw7GR1vJieHAYcFXSbkTtmeoOMVbDx3H +RqR4V+AfMTrEBj5fh7TC/2KT3nXHy4JxOM9364APiOVe8Xqlo1w7RDokzwD2IkFv +gHCujHb9ijxjyF2OYb0fJxpBSOqrKqCjifDnUPEzFWPCj1YnMDtKIv/ykqMn8Ggj +P9V4+P7NgLkkZTOz6Qo/bLe/75xEQLX2TsV3 +-----END CERTIFICATE----- diff --git a/tests/src/test/resources/certificates/trusted-servers-collection b/tests/src/test/resources/certificates/trusted-servers-collection new file mode 100644 index 000000000..b7ba4c3b3 --- /dev/null +++ b/tests/src/test/resources/certificates/trusted-servers-collection @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE+zCCAuOgAwIBAgIJAPaN4CpLQzLtMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV +BAMMCWxvY2FsaG9zdDAeFw0xODEwMjYxNDI2MDVaFw0yODEwMjMxNDI2MDVaMBQx +EjAQBgNVBAMMCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBALYsfU9sSTCsO5+ksdLi2CwPAl2EFuVL7KbwCDxkeOh733ckWbdLE4guTvtj +kGff/dOaGhdUnqSr4CTZRDJKmkzdHeTZToChhOu4cfr//uzk90yF4j0XzUB0Aye0 +F/fHaY4XjPAnwfZf8Xuy7JCJ8WCBvK9SrxMxpwTEpCL3QIjHYeA/UTBjI/xo0LF7 +VIncRiJQnpmS9TuM6dnvJ38ezfP+fzF+8izjvX7J5rooTbgDxXNSvdoHv76C/TQR +ANTV+M+zLuDb/sl5gMWOHRpkzj4BFDUScdHU4uKT7XXkqdfNxg8rIzxrUGNXoBvL +4ETLbegkhqIY3oCtR+tbKE1145mRFjcOzvkHmhGqTkCdCIKcTn5ReXjgeSQDjlRX +VEwvBQlCSfyDlnaKy2adWAC7H1SW2PVwCALCgA88kmJEmqbOJcOiNiKFDzHaBhVw +7hSGrXa3Wfj7wdWrdtoQLtSc2MkR0qNcfnEPKGz+sU/cLtdbB19ut36UbfRJAyC7 +galsHtvLDVRWnm/z1uWUStNye7S9EDCEbiG802jwnnlfzLdpkmuTnliNngg2jJO2 +EX1HYXzT4ezsMJLZV20uAm2VUt9fXDnyofEpLRIxTcQVD9mzIYh92EK6yCcXf2ms +golS3pLAYWMy3OQ5Xfb+aZoYdnZVWFl0B23LlkniPE011HGlAgMBAAGjUDBOMB0G +A1UdDgQWBBTlnzunEY+CaZH5qymJRwDBqz14RDAfBgNVHSMEGDAWgBTlnzunEY+C +aZH5qymJRwDBqz14RDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQBs +we7UZl/BDKKjq+CFycF8UKORoHx0UVBZh7WQxzL4o89lTAbBs4drVLS6Q/YBoFLp +WhCdtPeIW+Snm2YIAFQXvgCoZZ0vDQwmsjBCXI7/K9WvPvylYF9sV53foyppEZAy +ZRosESq0407I0bTx3F5Fs6OxI6cc9DhMHmdj/YZ1/BcHZZQPSmbBW+82Dh3qB1ez +AQpEwi+9kW0nAbxK5SIBzfH3EW/XNeIZF3AAp+4JQdXi64utnrq2PByNvy2ac6xo +rP4hQIPRdnzotDPtK/81BA4ZuHHh2bkvEkEq3m/3KRQG3qIigYOwHWPcFFHef2Pj +vDJ/ah+/oAYTiH52H+zThUn9FfjeBfSJDpwsbAz34PQVhdFvahcuKEEeRolsUWTS +pXS0F0aecae+2uC0UUPSXSmQ11tgddBwmpTb3VEjXGdw0q+4ZCN25L6Oir/fmmSs +7Vqw9lkWKsSSpKPAsydHUJHZFAWdjEEQcGq4wb9sY8hWk8sQAvrISNZtNTikjzZr +hf42E+j5DTaAjNu2oMStzydVzCE5bhpZ+0rPcKdghA+6etNPtt+LwxzYpSrrlo7E +nCW2u6ZUm6JXON2u/INhoGQIXX1XWuDObfn2/5XwcC1lcYvgXiUJhtSzMQEwPi6H +o10MR/UJTafCwx5Fj5bkecYvbYkcwRNXi3W5cpSCLg== +-----END CERTIFICATE----- diff --git a/tests/src/test/resources/logback-test.xml b/tests/src/test/resources/logback-test.xml new file mode 100644 index 000000000..766b9b209 --- /dev/null +++ b/tests/src/test/resources/logback-test.xml @@ -0,0 +1,19 @@ + + + + + + + %highlight(%d{HH:mm:ss.SSS} [%10thread] %-5level %logger{36} - %msg%n) + + + + + + + + + + + + From f2a44bc2a83e5aacee06a7ef5fb34ffdc87c9aa7 Mon Sep 17 00:00:00 2001 From: haochencheng Date: Sun, 17 Oct 2021 14:00:22 +0800 Subject: [PATCH 2/2] fix:remove GrpcSleuthClientConfig GrpcSleuthServerConfig --- .../cloud/client/GrpcSleuthClientConfig.java | 84 ------------------- .../cloud/server/GrpcSleuthServerConfig.java | 70 ---------------- 2 files changed, 154 deletions(-) delete mode 100644 examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcSleuthClientConfig.java delete mode 100644 examples/cloud-sleuth-zipkin-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GrpcSleuthServerConfig.java diff --git a/examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcSleuthClientConfig.java b/examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcSleuthClientConfig.java deleted file mode 100644 index 30c2a567d..000000000 --- a/examples/cloud-sleuth-zipkin-grpc-client/src/main/java/net/devh/boot/grpc/examples/cloud/client/GrpcSleuthClientConfig.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * 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.examples.cloud.client; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import brave.Span; -import brave.Tracing; -import brave.grpc.GrpcTracing; -import io.grpc.ClientInterceptor; -import io.grpc.ServerInterceptor; -import lombok.extern.slf4j.Slf4j; -import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorConfigurer; -import zipkin2.reporter.Reporter; - -/** - * Example configuration class that add grpc client sleuth/brave and zipkin integration - */ -@ConditionalOnProperty(value = {"spring.sleuth.enabled", "spring.zipkin.enabled"}, havingValue = "true") -@Configuration -@Slf4j -public class GrpcSleuthClientConfig { - - @Bean - public GrpcTracing grpcTracing(Tracing tracing) { - return GrpcTracing.create(tracing); - } - - /** - * We also create a client-side interceptor and put that in the context, this interceptor can then be injected into - * gRPC clients and then applied to the managed channel. - * - * @param grpcTracing - * @return - */ - @Bean - ClientInterceptor grpcClientSleuthInterceptor(GrpcTracing grpcTracing) { - return grpcTracing.newClientInterceptor(); - } - - @Bean - ServerInterceptor grpcServerSleuthInterceptor(GrpcTracing grpcTracing) { - return grpcTracing.newServerInterceptor(); - } - - /** - * Use this for debugging (or if there is no Zipkin server running on port 9411) - * - * @return - */ - @Bean - public Reporter spanReporter() { - return span -> { - if (log.isDebugEnabled()) { - log.debug("{}", span); - } - }; - } - - @Bean - public GlobalClientInterceptorConfigurer globalInterceptorConfigurerAdapter( - ClientInterceptor grpcClientSleuthInterceptor) { - return registry -> registry.add(grpcClientSleuthInterceptor); - } - - -} diff --git a/examples/cloud-sleuth-zipkin-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GrpcSleuthServerConfig.java b/examples/cloud-sleuth-zipkin-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GrpcSleuthServerConfig.java deleted file mode 100644 index ffe686eed..000000000 --- a/examples/cloud-sleuth-zipkin-grpc-server/src/main/java/net/devh/boot/grpc/examples/cloud/server/GrpcSleuthServerConfig.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.examples.cloud.server; - -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -import brave.Span; -import brave.Tracing; -import brave.grpc.GrpcTracing; -import io.grpc.ServerInterceptor; -import lombok.extern.slf4j.Slf4j; -import net.devh.boot.grpc.server.interceptor.GlobalServerInterceptorConfigurer; -import zipkin2.reporter.Reporter; - -/** - * Example configuration class that add grpc server sleuth/brave and zipkin integration - */ -@ConditionalOnProperty(value = {"spring.sleuth.enabled", "spring.zipkin.enabled"}, havingValue = "true") -@Configuration -@Slf4j -public class GrpcSleuthServerConfig { - - @Bean - public GrpcTracing grpcTracing(Tracing tracing) { - return GrpcTracing.create(tracing); - } - - @Bean - ServerInterceptor grpcServerSleuthInterceptor(GrpcTracing grpcTracing) { - return grpcTracing.newServerInterceptor(); - } - - /** - * Use this for debugging (or if there is no Zipkin server running on port 9411) - * - * @return - */ - @Bean - public Reporter spanReporter() { - return span -> { - if (log.isDebugEnabled()) { - log.debug("{}", span); - } - }; - } - - @Bean - public GlobalServerInterceptorConfigurer globalInterceptorConfigurerAdapter( - ServerInterceptor grpcServerSleuthInterceptor) { - return registry -> registry.add(grpcServerSleuthInterceptor); - } - -}

+ * It is not necessary to add Spring-Security as a + * dependency in order to use this package, however, some non-trivial security setups might require it nevertheless. + *