├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── NOTICE ├── README.adoc ├── build.gradle ├── docs └── img │ └── server-vs-client.png ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── grpc-client-spring-boot.iml ├── grpc-client-spring-boot.ipr ├── grpc-client-spring-boot_main.iml ├── grpc-client-spring-boot_test.iml ├── settings.gradle └── src ├── main ├── java │ └── codes │ │ └── sf │ │ └── springboot │ │ └── grpc │ │ └── client │ │ ├── GrpcChannelSource.java │ │ ├── GrpcStubPostProcessor.java │ │ ├── GrpcStubScan.java │ │ ├── autoconfigure │ │ ├── GrpcClientAutoConfiguration.java │ │ ├── GrpcClientProperties.java │ │ ├── GrpcScanAnnotationParser.java │ │ └── GrpcStubScannerConfiguration.java │ │ ├── context │ │ ├── GrpcStubFactoryBean.java │ │ └── GrpcStubScanner.java │ │ └── stubpostprocess │ │ ├── GenericGrpcStubPostProcessor.java │ │ └── GenericGrpcStubPostProcessorAdapter.java └── resources │ └── META-INF │ └── spring.factories └── test ├── java ├── codes │ └── sf │ │ └── springboot │ │ └── grpc │ │ └── client │ │ └── test │ │ ├── CallCredentialsTests.java │ │ ├── CallOptionsTests.java │ │ ├── ChannelTests.java │ │ ├── ClientInterceptorTests.java │ │ ├── GreeterService.java │ │ ├── GrpcClientPropertiesTests.java │ │ ├── GrpcScannerTests.java │ │ ├── GrpcStubPostProcessorTests.java │ │ └── GrpcTestUtils.java └── io │ └── grpc │ └── examples │ ├── ComponentScanAnnotationConfiguration.java │ ├── GrpcStubScanAnnotationConfiguration.java │ └── SpringBootApplicationAnnotationConfiguration.java └── proto └── greeter.proto /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Java Gradle CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-java/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | - image: circleci/openjdk:8-jdk 10 | 11 | working_directory: ~/grpc-client-spring-boot 12 | 13 | steps: 14 | - checkout 15 | 16 | # Download and cache dependencies 17 | - restore_cache: 18 | keys: 19 | - v1-dependencies-{{ checksum "build.gradle" }} 20 | # fallback to using the latest cache if no exact match is found 21 | - v1-dependencies- 22 | 23 | - run: ./gradlew dependencies 24 | 25 | - save_cache: 26 | paths: 27 | - ~/.gradle 28 | key: v1-dependencies-{{ checksum "build.gradle" }} 29 | 30 | # run tests! 31 | - run: ./gradlew test 32 | 33 | # Upload test reports to Codecov 34 | - run: bash <(curl -s https://codecov.io/bash) 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Gradle 2 | .gradle 3 | /build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | 6 | # Ignore Gradle GUI config 7 | gradle-app.setting 8 | 9 | # Cache of project 10 | .gradletasknamecache 11 | 12 | ### JetBrains 13 | 14 | # User-specific stuff 15 | .idea/**/workspace.xml 16 | .idea/**/tasks.xml 17 | .idea/**/usage.statistics.xml 18 | .idea/**/dictionaries 19 | .idea/**/shelf 20 | 21 | # Sensitive or high-churn files 22 | .idea/**/dataSources/ 23 | .idea/**/dataSources.ids 24 | .idea/**/dataSources.local.xml 25 | .idea/**/sqlDataSources.xml 26 | .idea/**/dynamic.xml 27 | .idea/**/uiDesigner.xml 28 | .idea/**/dbnavigator.xml 29 | 30 | # Gradle 31 | .idea/**/gradle.xml 32 | .idea/**/libraries 33 | 34 | # Gradle and Maven with auto-import 35 | .idea/modules.xml 36 | .idea/*.iml 37 | .idea/modules 38 | 39 | # File-based project format 40 | *.iws 41 | 42 | # IntelliJ 43 | out/ 44 | 45 | # Crashlytics plugin 46 | com_crashlytics_export_strings.xml 47 | crashlytics.properties 48 | crashlytics-build.properties 49 | fabric.properties 50 | 51 | ### Java template 52 | # Compiled class file 53 | *.class 54 | 55 | # Log file 56 | *.log 57 | 58 | # Package Files # 59 | *.jar 60 | *.war 61 | *.nar 62 | *.ear 63 | *.zip 64 | *.tar.gz 65 | *.rar 66 | 67 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 68 | hs_err_pid* 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | ==== 2 | Copyright 2018 Semyon Fishman . 3 | 4 | This project is grpc-client-spring-boot. 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this project except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | ==== 18 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | image:https://circleci.com/gh/sfcodes/grpc-client-spring-boot/tree/develop.svg?style=shield["CircleCI", link="https://circleci.com/gh/sfcodes/grpc-client-spring-boot/tree/develop"] 2 | image:https://codecov.io/gh/sfcodes/grpc-client-spring-boot/branch/develop/graph/badge.svg["Codecov", link="https://codecov.io/gh/sfcodes/grpc-client-spring-boot"] 3 | image:https://img.shields.io/maven-central/v/codes.sf/grpc-client-spring-boot.svg?label=Maven%20Central["Maven Central", link="https://search.maven.org/search?q=g:%22codes.sf%22%20AND%20a:%22grpc-client-spring-boot%22"] 4 | 5 | = gRPC Client for Spring Boot 6 | 7 | Spring Boot library for auto-configuring https://github.com/grpc/grpc-java[gRPC Java stubs]. 8 | 9 | This library will automatically scan the classpath, find all gRPC stub classes, instantiate them, and register them as 10 | beans with the ApplicationContext; allowing for easy `@Autowire` and injecting them just like you would any other Spring 11 | bean. For example: 12 | 13 | ```java 14 | @RestController 15 | public class GreeterController { 16 | 17 | @Autowired // <===== gRPC stub is autowired! 18 | private GreeterGrpc.GreeterBlockingStub greeterStub; 19 | 20 | @RequestMapping(value = "/sayhello") 21 | public String sayHello(@RequestParam String name) { 22 | HelloRequest request = HelloRequest.newBuilder().setName(name).build(); 23 | HelloReply reply = greeterStub.sayHello(request); 24 | return reply.getMessage(); 25 | } 26 | } 27 | 28 | ``` 29 | 30 | If you're completely new to gRPC, start by going through the 31 | https://grpc.io/docs/tutorials/basic/java.html[gRPC Basics for Java] tutorial first. Then come back here to learn how to 32 | integrate gRPC stubs with Spring. 33 | 34 | ==== 35 | This library requires *Spring Framework 5.x* and *Spring Boot 2.x*. If you would like to see this backported to Spring 36 | Framework 4.x and Spring Boot 1.x, please upvote 37 | https://github.com/sfcodes/grpc-client-spring-boot/issues/1[this issue]. 38 | ==== 39 | 40 | ==== Maven 41 | 42 | For Maven users, in your pom.xml file, add between ` ... ` 43 | ```xml 44 | 45 | codes.sf 46 | grpc-client-spring-boot 47 | 0.0.4 48 | 49 | ``` 50 | 51 | ==== Gradle 52 | 53 | For Gradle users, add the following in your build.gradle file: 54 | ```groovy 55 | repositories { 56 | mavenCentral() 57 | } 58 | 59 | dependencies { 60 | compile group: 'codes.sf', name: 'grpc-client-spring-boot', version: '0.0.4' 61 | } 62 | ``` 63 | 64 | == Usage 65 | 66 | By default, without any user configuration, this library will scan for gRPC stubs in the same packages already 67 | specified in your `@SpringBootApplication` and `@ComponentScan` annotations. Instantiated stubs will use a default 68 | plaintext _channel_ with target `localhost:6565`. 69 | 70 | 71 | ==== Scan Packages 72 | 73 | If your stubs are not in the same package as your Spring Boot app, you can configure alternative packages to scan with 74 | property: 75 | ```yaml 76 | grpc: 77 | client: 78 | scanPackages: io.grpc.examples 79 | ``` 80 | 81 | Or you can use the more sophisticated annotation form: 82 | ```java 83 | @GrpcStubScan(basePackages = "io.grpc.examples") 84 | public class MyGrpcConfiguration { 85 | } 86 | ``` 87 | 88 | If both are present, the annotation will override the property. 89 | 90 | ==== Channel 91 | 92 | By default, a plaintext `localhost:6565` channel is used for all stub instances. To configure a different target, 93 | use property: 94 | 95 | ```yaml 96 | grpc: 97 | client: 98 | target: example.com:8080 99 | ``` 100 | 101 | For a more sophisticated channel configuration, you can declare your own channel bean: 102 | ```java 103 | @Configuration 104 | public class MyGrpcConfiguration { 105 | @Bean 106 | public Channel channel() { 107 | return ManagedChannelBuilder 108 | .forAddress("grpc.example.com", 443) 109 | .useTransportSecurity() 110 | .enableRetry() 111 | .build(); 112 | } 113 | } 114 | ``` 115 | 116 | If you require different channels for different stubs, you can declare a `GrpcChannelSource` bean: 117 | ```java 118 | @Configuration 119 | public class MyGrpcConfiguration { 120 | @Bean 121 | public GrpcChannelSource channelSource() { 122 | return stubClass -> { 123 | String serviceName = stubClass.getCanonicalName().toLowerCase(); 124 | return ManagedChannelBuilder 125 | .forAddress(serviceName + ".local", 8080) 126 | .usePlaintext() 127 | .build(); 128 | }; 129 | } 130 | } 131 | ``` 132 | 133 | GrpcChannelSource is where your _service discovery_ logic should go, routing stubs to their implementations. 134 | 135 | ==== Executor 136 | 137 | By default gRPC uses it's own `Executor` instance for asynchronous operations. You can however switch to using the 138 | _Spring_ executor with this property: 139 | ```yaml 140 | grpc: 141 | client: 142 | springexecutor: true 143 | ``` 144 | 145 | ==== Compression 146 | 147 | You may set the compression to use for calls: 148 | ```yaml 149 | grpc: 150 | client: 151 | compression: gzip 152 | ``` 153 | 154 | Note however that the compression set here is used by the stub to compress messages _to_ the server. To get compressed 155 | responses _from_ the server, you will need to set the appropriate decompressor registry on the channel. 156 | 157 | ==== Max Message Sizes 158 | 159 | You can set the maximum allowed _inbound_ (from the server) and _outbound_ (to the server) message sizes in bytes, 160 | with properties: 161 | ```yaml 162 | grpc: 163 | client: 164 | maxInboundMessageSize: 2048 165 | maxOutboundMessageSize: 1024 166 | ``` 167 | 168 | ===== Client Interceptors 169 | 170 | You may register https://grpc.io/grpc-java/javadoc/io/grpc/ClientInterceptor.html[client interceptors] as Spring beans 171 | and they will automatically be applied to all stub calls. You may order these interceptors using Spring's order 172 | https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/annotation/Order.html[annotation] 173 | or https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/Ordered.html[interface]. 174 | 175 | ```java 176 | @Configuration 177 | public class MyGrpcConfiguration { 178 | 179 | @Order(Ordered.HIGHEST_PRECEDENCE) 180 | @Bean 181 | public ClientInterceptor clientInterceptorB() { 182 | return new ClientInterceptor() { 183 | @Override 184 | public ClientCall interceptCall( 185 | MethodDescriptor method, 186 | CallOptions callOptions, 187 | Channel next) { 188 | 189 | // For example add credentials to the call 190 | callOptions = callOptions.withCallCredentials(myCallCredentials); 191 | 192 | return next.newCall(method, callOptions)); 193 | } 194 | }; 195 | } 196 | } 197 | ``` 198 | 199 | ==== Stub Post Processors 200 | 201 | gRPC _stub post processors_ are factory hooks for custom modification of the new gRPC stub instances before they are 202 | ready for use. Use post processors if you require further fine tuning not already exposed by the properties documented 203 | above. 204 | 205 | Stub post processors are registered as Spring beans, can be ordered, and will automatically be detected and applied to 206 | the newly created stub instances. Example registration: 207 | 208 | ```java 209 | @Configuration 210 | public class MyGrpcConfiguration { 211 | @Bean 212 | public GrpcStubPostProcessor postProcessor() { 213 | return stub -> { 214 | return stub.withOption(myCustomOptionKey, myCustomOption); 215 | }; 216 | } 217 | } 218 | ``` 219 | 220 | `GrpcStubPostProcessor` can generically declare the stub type that it is interested in, in which case the processor will 221 | only be invoked on the matching stub instances; for example the following post processor will only apply to 222 | `GreeterBlockingStub`, it will _not_ apply to `GreeterStub`, `GreeterFutureStub`, or any other stub: 223 | 224 | ```java 225 | @Configuration 226 | public class MyGrpcConfiguration { 227 | 228 | @Bean 229 | public GrpcStubPostProcessor postProcessor() { 230 | return new GrpcStubPostProcessor() { 231 | @Override 232 | public GreeterBlockingStub postProcess(GreeterBlockingStub stub) { 233 | return stub.withDeadlineAfter(1, TimeUnit.DAYS); 234 | } 235 | }; 236 | } 237 | } 238 | ``` 239 | _Warning, due to Spring issue https://jira.spring.io/browse/SPR-13698[SPR-13698], you can not use lambda's to 240 | define generically-declared post processors, they will not work._ 241 | 242 | If you require more control over deciding which stubs to process, consider using interface 243 | `GenericGrpcStubPostProcessor` instead. 244 | 245 | == What about _LogNet/grpc-spring-boot-starter_ ? 246 | 247 | If you Googled _"spring grpc"_, you probably found this library and 248 | https://github.com/LogNet/grpc-spring-boot-starter[LogNet/grpc-spring-boot-starter], and now wondering why are there 249 | _two_ different libraries for integrating gRPC with Spring? 250 | 251 | Answer is, the two library actually do two different things: 252 | 253 | - This library integrates gRPC *stubs* with 254 | Spring Boot, which means it should be used by Spring Boot apps that are gRPC *clients*. 255 | - https://github.com/LogNet/grpc-spring-boot-starter[LogNet/grpc-spring-boot-starter] integrates gRPC service 256 | *implementations* with Spring Boot, which means it should be used by Spring Boot apps that are gRPC *servers*. 257 | 258 | It might be the case that your app both consumes gRPC stubs, and implements gRPC services, in which case you should use 259 | both of these libraries in your app; they're totally compatible with each other. 260 | 261 | image::docs/img/server-vs-client.png[Server vs. Client] 262 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | jcenter() 4 | } 5 | dependencies { 6 | classpath 'com.github.amkay:gradle-gitflow:0.2.0' 7 | } 8 | } 9 | 10 | plugins { 11 | id 'java-library' 12 | id 'jacoco' 13 | id 'idea' 14 | id 'com.google.protobuf' version '0.8.6' 15 | id 'maven-publish' 16 | id 'signing' 17 | } 18 | 19 | apply plugin: 'com.github.amkay.gitflow' 20 | 21 | ext { 22 | springBootVersion = '2.0.6.RELEASE' 23 | grpcVersion = '1.15.0' 24 | junitVersion = '4.12' 25 | javafakerVersion = '0.16' 26 | openpojoVersion = '0.8.10' 27 | } 28 | 29 | group 'codes.sf' 30 | 31 | sourceCompatibility = 1.8 32 | targetCompatibility = 1.8 33 | 34 | repositories { 35 | mavenCentral() 36 | } 37 | 38 | dependencies { 39 | api group: 'org.springframework.boot', name: 'spring-boot', version: "${springBootVersion}" 40 | api group: 'org.springframework.boot', name: 'spring-boot-autoconfigure', version: "${springBootVersion}" 41 | api group: 'io.grpc', name: 'grpc-services', version: "${grpcVersion}" 42 | api group: 'io.grpc', name: 'grpc-netty', version: "${grpcVersion}" 43 | 44 | annotationProcessor group: 'org.springframework.boot', name: 'spring-boot-configuration-processor', version: "${springBootVersion}" 45 | 46 | testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: "${springBootVersion}" 47 | testCompile group: 'junit', name: 'junit', version: "${junitVersion}" 48 | testCompile group: 'com.github.javafaker', name: 'javafaker', version: "${javafakerVersion}" 49 | testCompile group: 'com.openpojo', name: 'openpojo', version: "${openpojoVersion}" 50 | } 51 | 52 | sourceSets { 53 | test { 54 | proto { 55 | srcDir 'src/test/proto' 56 | } 57 | } 58 | } 59 | 60 | compileJava { 61 | options.encoding = 'UTF-8' 62 | options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" 63 | } 64 | 65 | compileTestJava { 66 | options.encoding = 'UTF-8' 67 | options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" 68 | } 69 | 70 | protobuf { 71 | protoc { 72 | artifact = 'com.google.protobuf:protoc:3.6.1' 73 | } 74 | plugins { 75 | grpc { 76 | artifact = 'io.grpc:protoc-gen-grpc-java:1.15.1' 77 | } 78 | } 79 | generateProtoTasks { 80 | all().each { task -> 81 | task.plugins { 82 | grpc {} 83 | } 84 | } 85 | } 86 | } 87 | 88 | test { 89 | finalizedBy jacocoTestReport 90 | } 91 | 92 | jacocoTestReport { 93 | reports { 94 | xml.enabled true 95 | html.enabled true 96 | } 97 | } 98 | 99 | jacocoTestCoverageVerification { 100 | violationRules { 101 | rule { 102 | limit { 103 | minimum = 1.0 // Require 100% code coverage 104 | } 105 | } 106 | } 107 | } 108 | 109 | task sourcesJar(type: Jar) { 110 | classifier = 'sources' 111 | from sourceSets.main.allJava 112 | } 113 | 114 | task javadocJar(type: Jar, dependsOn: javadoc) { 115 | classifier = 'javadoc' 116 | from javadoc.destinationDir 117 | } 118 | 119 | if (project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword')) { 120 | publishing { 121 | publications { 122 | maven(MavenPublication) { 123 | from components.java 124 | artifact(javadocJar) { 125 | classifier = 'javadoc' 126 | } 127 | artifact(sourcesJar) { 128 | classifier = 'sources' 129 | } 130 | pom { 131 | name = 'Spring Boot module for gRPC client apps' 132 | description = 'Allows Spring Boot-style configuration of gRPC stubs ' + 133 | 'using properties and JavaConfig. Automatically registers ' + 134 | 'gRPC stubs found on the classpath as beans with the ' + 135 | 'ApplicationContext so they can be easily injected with ' + 136 | '@Autowired and other injection methods.' 137 | url = 'https://github.com/sfcodes/grpc-client-spring-boot' 138 | licenses { 139 | license { 140 | name = 'The Apache License, Version 2.0' 141 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 142 | } 143 | } 144 | developers { 145 | developer { 146 | id = 'sfcodes' 147 | name = 'Semyon Fishman' 148 | email = 's.fishman@gmail.com' 149 | } 150 | } 151 | scm { 152 | connection = 'scm:git:git://github.com/sfcodes/grpc-client-spring-boot.git' 153 | developerConnection = 'scm:git:git@github.com:sfcodes/grpc-client-spring-boot.git' 154 | url = 'https://github.com/sfcodes/grpc-client-spring-boot' 155 | } 156 | } 157 | } 158 | } 159 | 160 | repositories { 161 | maven { 162 | name = 'OSSRH' 163 | url = 'https://oss.sonatype.org/service/local/staging/deploy/maven2/' 164 | credentials { 165 | username sonatypeUsername 166 | password sonatypePassword 167 | } 168 | } 169 | } 170 | } 171 | 172 | signing { 173 | sign publishing.publications.maven 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /docs/img/server-vs-client.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfcodes/grpc-client-spring-boot/696758848e905e3e4bc411cad060e5911576edde/docs/img/server-vs-client.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfcodes/grpc-client-spring-boot/696758848e905e3e4bc411cad060e5911576edde/gradle.properties -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sfcodes/grpc-client-spring-boot/696758848e905e3e4bc411cad060e5911576edde/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Oct 17 20:08:49 EDT 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /grpc-client-spring-boot.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /grpc-client-spring-boot.ipr: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 49 | 50 | 51 | 52 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 800 | 801 | 802 | 803 | 804 | 805 | 806 | 807 | 808 | 809 | 810 | 811 | 812 | 813 | 814 | 815 | 816 | 817 | 818 | 819 | 820 | 821 | 822 | 823 | 824 | 825 | 826 | 827 | 828 | 829 | 830 | 831 | 832 | 833 | 834 | 835 | 836 | 837 | 838 | 839 | 840 | 841 | 842 | 847 | 848 | 849 | 1.6 850 | 851 | 852 | 853 | 854 | 855 | 856 | 857 | 858 | 859 | 860 | 861 | 862 | -------------------------------------------------------------------------------- /grpc-client-spring-boot_main.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /grpc-client-spring-boot_test.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'grpc-client-spring-boot' 2 | 3 | enableFeaturePreview('STABLE_PUBLISHING') 4 | -------------------------------------------------------------------------------- /src/main/java/codes/sf/springboot/grpc/client/GrpcChannelSource.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client; 2 | 3 | import io.grpc.Channel; 4 | import io.grpc.ManagedChannelBuilder; 5 | import io.grpc.stub.AbstractStub; 6 | 7 | /** 8 | * A strategy interface for resolving {@linkplain Channel Channels} for gRPC stubs. 9 | * 10 | *

GrpcChannelSource is usually where your service discovery logic 11 | * goes. It is the place where you map the client stubs to their server 12 | * implementations. 13 | * 14 | *

Auto configuration will autodetect the GrpcChannelSource bean and 15 | * use it to resolve channels for new gRPC stub instances. In other words 16 | * you may register your GrpcChannelSource as a simple {@code @Bean} in 17 | * your {@code @Configuration} classes. 18 | * 19 | *

Note that there must be at most one GrpcChannelSource in the 20 | * ApplicationContext, otherwise you'll get an error. 21 | * 22 | *

If no GrpcChannelSource is found in the ApplicationContext, auto 23 | * configuration will 24 | *

    25 | *
  • 26 | * first look for a {@link Channel} in the ApplicationContext to use instead; 27 | *
  • 28 | *
  • 29 | * if no channel bean is found, it will configure a new 30 | * {@linkplain ManagedChannelBuilder#usePlaintext() plaintext} one 31 | * with target from environment property {@code grpc.client.target}; 32 | *
  • 33 | *
  • 34 | * finally if no property is found, it will default to creating a new 35 | * plaintext channel bean with target "{@code localhost:6565}" 36 | *
  • 37 | *
38 | * 39 | *

If you're creating a GrpcChannelSource that returns a constant channel, 40 | * consider using convenience function {@link #of(Channel) GrpcChannelSource.of(Channel)} 41 | * 42 | * @author Semyon Fishman 43 | * @since 0.0.1 44 | */ 45 | @FunctionalInterface 46 | public interface GrpcChannelSource { 47 | 48 | /** 49 | * Resolve channel for stub type. 50 | * 51 | * @param stubClass the stub type 52 | * @return resolved channel, may not be {@code null} 53 | */ 54 | Channel resolve(Class> stubClass); 55 | 56 | /** 57 | * Convenience method for build GrpcChannelSource that returns 58 | * a constant channel instance. 59 | * 60 | * @param channel the constant channel instance to return 61 | * @return newly constructed GrpcChannelSource 62 | */ 63 | static GrpcChannelSource of(Channel channel) { 64 | return stubClass -> channel; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/codes/sf/springboot/grpc/client/GrpcStubPostProcessor.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client; 2 | 3 | import io.grpc.stub.AbstractStub; 4 | 5 | /** 6 | * Factory hook that allows for custom modification of new gRPC stub instances. 7 | * 8 | *

Auto configuration will autodetect GrpcStubPostProcessor beans and 9 | * automatically apply them to new gRPC stub instances. In other words you 10 | * may register these GrpcStubPostProcessor's as simple {@code @Bean}'s in 11 | * your {@code @Configuration} classes. 12 | * 13 | *

An GrpcStubPostProcessor can generically declare the stub type that it is 14 | * interested in, in which case he processor will only be invoked on the 15 | * matching stub instances. 16 | * 17 | *

GrpcStubPostProcessor maybe be ordered using either the 18 | * {@linkplain org.springframework.core.Ordered ordered interface} or 19 | * {@link org.springframework.core.annotation.Order @Order annotation}. Note 20 | * that you may also apply {@code @Order} to your {@code @Bean} methods. 21 | * 22 | * @author Semyon Fishman 23 | * @see codes.sf.springboot.grpc.client.stubpostprocess.GenericGrpcStubPostProcessor GenericGrpcStubPostProcessor 24 | * @since 0.0.1 25 | */ 26 | @FunctionalInterface 27 | public interface GrpcStubPostProcessor> { 28 | 29 | /** 30 | * Apply this GrpcStubPostProcessor to the given new gRPC stub instance. 31 | * 32 | * @param stub the new gRPC stub instance 33 | * @return the gRPC stub instance to use, either the original or 34 | * wrapped one; may not be @{code null} 35 | */ 36 | S postProcess(S stub); 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/codes/sf/springboot/grpc/client/GrpcStubScan.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client; 2 | 3 | import org.springframework.beans.factory.support.BeanNameGenerator; 4 | import org.springframework.context.annotation.*; 5 | import org.springframework.core.annotation.AliasFor; 6 | 7 | import java.lang.annotation.*; 8 | 9 | /** 10 | * Configures gRPC stubs scanning. 11 | * 12 | *

Either {@link #basePackageClasses} or {@link #basePackages} may be 13 | * specified to define specific packages to scan. If specific packages are not 14 | * defined, scanning will occur from the package of the class that declares 15 | * this annotation. 16 | * 17 | *

If this annotation is missing, auto configuration will fallback on 18 | * environment property {@code grpc.client.scanPackages}. If the property 19 | * is missing as well, auto configuration will finally fall back on 20 | * the values in {@link ComponentScan @ComponentScan}. 21 | * 22 | * @author Semyon Fishman 23 | * @see 24 | * gRPC Basics - Java 25 | * @see 26 | * gRPC Java Generated Code Reference 27 | * @since 0.0.1 28 | */ 29 | @Retention(RetentionPolicy.RUNTIME) 30 | @Target(ElementType.TYPE) 31 | @Documented 32 | public @interface GrpcStubScan { 33 | 34 | 35 | /** 36 | * Alias for {@link #basePackages}. 37 | *

Allows for more concise annotation declarations if no other attributes 38 | * are needed — for example, {@code @GrpcStubScan("org.my.pkg")} 39 | * instead of {@code @GrpcStubScan(basePackages = "org.my.pkg")}. 40 | * 41 | * @return packages to scan for gRPC stubs 42 | */ 43 | @AliasFor("basePackages") 44 | String[] value() default {}; 45 | 46 | /** 47 | * Base packages to scan for gRPC stubs 48 | *

{@link #value} is an alias for (and mutually exclusive with) this 49 | * attribute. 50 | *

Use {@link #basePackageClasses} for a type-safe alternative to 51 | * String-based package names. 52 | * 53 | * @return packages to scan for gRPC stubs 54 | */ 55 | @AliasFor("value") 56 | String[] basePackages() default {}; 57 | 58 | /** 59 | * Type-safe alternative to {@link #basePackages} for specifying the packages 60 | * to scan for gRPC stubs. The package of each class specified will be scanned. 61 | *

Consider using this type-safe alternative with the generated stubs' 62 | * enclosing class. For example 63 | * {@code @GrpcStubScan(basePackageClasses = "org.my.pkg.grpc.MyServiceGrpc.class")} 64 | * 65 | * @return packages to scan for gRPC stubs 66 | */ 67 | Class[] basePackageClasses() default {}; 68 | 69 | /** 70 | * The {@link BeanNameGenerator} class to be used for naming detected gRPC stubs 71 | * within the Spring container. 72 | *

The default value of the {@link BeanNameGenerator} interface itself indicates 73 | * that the scanner used to process this {@code @GrpcStubScan} annotation should 74 | * use its inherited bean name generator, e.g. the default 75 | * {@link AnnotationBeanNameGenerator} or any custom instance supplied to the 76 | * application context at bootstrap time. 77 | * 78 | * @return class to be used for naming detected gRPC stub beans 79 | * @see AnnotationConfigApplicationContext#setBeanNameGenerator(BeanNameGenerator) 80 | */ 81 | Class nameGenerator() default BeanNameGenerator.class; 82 | 83 | /** 84 | * The {@link ScopeMetadataResolver} to be used for resolving the scope of detected gRPC stubs. 85 | * 86 | * @return ScopeMetadataResolver class 87 | */ 88 | Class scopeResolver() default AnnotationScopeMetadataResolver.class; 89 | 90 | /** 91 | * Indicates whether proxies should be generated for detected gRPC stubs, which may be 92 | * necessary when using scopes in a proxy-style fashion. 93 | *

The default is defer to the default behavior of the gRPC stub scanner used to 94 | * execute the actual scan. 95 | *

Note that setting this attribute overrides any value set for {@link #scopeResolver}. 96 | * 97 | * @return scoped proxy mode to use with stubs 98 | * @see codes.sf.springboot.grpc.client.context.GrpcStubScanner#setScopedProxyMode(ScopedProxyMode) GrpcStubScanner.setScopedProxyMode(ScopedProxyMode) 99 | */ 100 | ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT; 101 | 102 | /** 103 | * Controls the class files eligible for gRPC stubs detection. 104 | * 105 | * @return pattern of gRPC classes stubs 106 | */ 107 | String resourcePattern() default "**/*.class"; // Copied from ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN; 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/codes/sf/springboot/grpc/client/autoconfigure/GrpcClientAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client.autoconfigure; 2 | 3 | import codes.sf.springboot.grpc.client.GrpcChannelSource; 4 | import codes.sf.springboot.grpc.client.GrpcStubPostProcessor; 5 | import codes.sf.springboot.grpc.client.stubpostprocess.GenericGrpcStubPostProcessor; 6 | import codes.sf.springboot.grpc.client.stubpostprocess.GenericGrpcStubPostProcessorAdapter; 7 | import io.grpc.CallCredentials; 8 | import io.grpc.Channel; 9 | import io.grpc.ClientInterceptor; 10 | import io.grpc.ManagedChannelBuilder; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 13 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 14 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 15 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 16 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 17 | import org.springframework.context.annotation.Bean; 18 | import org.springframework.context.annotation.Configuration; 19 | import org.springframework.context.annotation.Import; 20 | 21 | import java.util.Collections; 22 | import java.util.List; 23 | import java.util.Optional; 24 | import java.util.concurrent.Executor; 25 | 26 | import static java.util.stream.Collectors.toList; 27 | 28 | /** 29 | * {@link EnableAutoConfiguration Auto-configuration} for gRPC client stubs. 30 | * 31 | *

This auto-configuration will scan the 32 | * {@linkplain codes.sf.springboot.grpc.client.context.GrpcStubScanner 33 | * classpath for gRPC stubs} and instantiate them with a default or 34 | * user-configured {@link Channel}. 35 | * 36 | * @author Semyon Fishman 37 | * @see GrpcClientProperties 38 | * @since 0.0.1 39 | */ 40 | @EnableConfigurationProperties(GrpcClientProperties.class) 41 | @Configuration 42 | @Import(GrpcStubScannerConfiguration.class) 43 | public class GrpcClientAutoConfiguration { 44 | 45 | public static final String GENERIC_GRPC_STUB_POST_PROCESSORS_BEAN_NAME = 46 | "GrpcClientAutoConfiguration_genericGrpcStubPostProcessors"; 47 | 48 | private List postProcessors; 49 | private Channel channel; 50 | 51 | private final GrpcClientProperties properties; 52 | 53 | GrpcClientAutoConfiguration(GrpcClientProperties properties) { 54 | this.properties = properties; 55 | } 56 | 57 | @Autowired(required = false) 58 | void setGrpcStubPostProcessors(List postProcessors) { 59 | this.postProcessors = postProcessors; 60 | } 61 | 62 | @Autowired(required = false) 63 | public void setChannel(Channel channel) { 64 | this.channel = channel; 65 | } 66 | 67 | @Bean(name = GENERIC_GRPC_STUB_POST_PROCESSORS_BEAN_NAME) 68 | public List genericGrpcStubPostProcessors() { 69 | return Optional.ofNullable(this.postProcessors) 70 | .orElse(Collections.emptyList()) 71 | .stream() 72 | .map(processor -> (processor instanceof GenericGrpcStubPostProcessor) ? 73 | (GenericGrpcStubPostProcessor) processor 74 | : new GenericGrpcStubPostProcessorAdapter(processor) 75 | ) 76 | .collect(toList()); 77 | } 78 | 79 | @Bean 80 | @ConditionalOnMissingBean(GrpcChannelSource.class) 81 | public GrpcChannelSource channelSource() { 82 | 83 | Channel channel; 84 | 85 | if (this.channel != null) 86 | channel = this.channel; 87 | else { 88 | String target = properties.getTarget(); 89 | if (target == null || target.isEmpty()) 90 | target = GrpcClientProperties.DEFAULT_TARGET; 91 | 92 | channel = ManagedChannelBuilder 93 | .forTarget(target) 94 | .usePlaintext() 95 | .build(); 96 | } 97 | 98 | return GrpcChannelSource.of(channel); 99 | } 100 | 101 | @Bean 102 | @ConditionalOnProperty(prefix = GrpcClientProperties.PREFIX, name = "springexecutor") 103 | @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") 104 | public GrpcStubPostProcessor springExecutorGrpcStubPostProcessor(Executor executor) { 105 | return stub -> stub.withExecutor(executor); 106 | } 107 | 108 | @Bean 109 | @ConditionalOnProperty(prefix = GrpcClientProperties.PREFIX, name = "compression") 110 | public GrpcStubPostProcessor compressionGrpcStubPostProcessor() { 111 | return stub -> stub.withCompression(properties.getCompression()); 112 | } 113 | 114 | @Bean 115 | @ConditionalOnBean(ClientInterceptor.class) 116 | public GrpcStubPostProcessor interceptorsGrpcStubPostProcessor(List interceptors) { 117 | return stub -> { 118 | for (ClientInterceptor interceptor : interceptors) 119 | stub = stub.withInterceptors(interceptor); 120 | return stub; 121 | }; 122 | } 123 | 124 | @Bean 125 | @ConditionalOnBean(CallCredentials.class) 126 | @SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection") 127 | public GrpcStubPostProcessor callCredentialsGrpcStubPostProcessor(CallCredentials credentials) { 128 | return stub -> stub.withCallCredentials(credentials); 129 | } 130 | 131 | @Bean 132 | @ConditionalOnProperty(prefix = GrpcClientProperties.PREFIX, name = "maxInboundMessageSize") 133 | public GrpcStubPostProcessor maxInboundMessageSizeGrpcStubPostProcessor() { 134 | return stub -> stub.withMaxInboundMessageSize(properties.getMaxInboundMessageSize()); 135 | } 136 | 137 | @Bean 138 | @ConditionalOnProperty(prefix = GrpcClientProperties.PREFIX, name = "maxOutboundMessageSize") 139 | public GrpcStubPostProcessor maxOutboundMessageSizeGrpcStubPostProcessor() { 140 | return stub -> stub.withMaxOutboundMessageSize(properties.getMaxOutboundMessageSize()); 141 | } 142 | 143 | } 144 | -------------------------------------------------------------------------------- /src/main/java/codes/sf/springboot/grpc/client/autoconfigure/GrpcClientProperties.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client.autoconfigure; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | /** 6 | * gRPC client properties. 7 | * 8 | * @author Semyon Fishman 9 | * @since 0.0.1 10 | */ 11 | @ConfigurationProperties(prefix = GrpcClientProperties.PREFIX) 12 | public class GrpcClientProperties { 13 | 14 | public static final String PREFIX = "grpc.client"; 15 | public static final String DEFAULT_HOST = "localhost"; 16 | public static final int DEFAULT_PORT = 6565; 17 | public static final String DEFAULT_TARGET = DEFAULT_HOST + ":" + DEFAULT_PORT; 18 | 19 | /** 20 | * Sets base package to scan for gRPC stubs . This property is ignored 21 | * if @GrpcStubScan annotation is present. 22 | */ 23 | private String[] scanPackages; 24 | 25 | /** 26 | * Sets target address for gRPC stubs' channel. 27 | */ 28 | private String target; 29 | 30 | /** 31 | * Configures the gRPC stubs to use the executor found in the 32 | * ApplicationContext, instead of the default one. 33 | */ 34 | private boolean springexecutor = false; 35 | 36 | /** 37 | * Sets the compressor name to use for gRPC calls. 38 | */ 39 | private String compression; 40 | 41 | /** 42 | * Limits the maximum acceptable message size from remote peers. 43 | */ 44 | private Integer maxInboundMessageSize; 45 | 46 | /** 47 | * Limits the maximum acceptable message size to send remote peers. 48 | */ 49 | private Integer maxOutboundMessageSize; 50 | 51 | public String[] getScanPackages() { 52 | return scanPackages; 53 | } 54 | 55 | public void setScanPackages(String[] scanPackages) { 56 | this.scanPackages = scanPackages; 57 | } 58 | 59 | public String getTarget() { 60 | return target; 61 | } 62 | 63 | public void setTarget(String target) { 64 | this.target = target; 65 | } 66 | 67 | public boolean isSpringexecutor() { 68 | return springexecutor; 69 | } 70 | 71 | public void setSpringexecutor(boolean springexecutor) { 72 | this.springexecutor = springexecutor; 73 | } 74 | 75 | public String getCompression() { 76 | return compression; 77 | } 78 | 79 | public void setCompression(String compression) { 80 | this.compression = compression; 81 | } 82 | 83 | public Integer getMaxInboundMessageSize() { 84 | return maxInboundMessageSize; 85 | } 86 | 87 | public void setMaxInboundMessageSize(Integer maxInboundMessageSize) { 88 | this.maxInboundMessageSize = maxInboundMessageSize; 89 | } 90 | 91 | public Integer getMaxOutboundMessageSize() { 92 | return maxOutboundMessageSize; 93 | } 94 | 95 | public void setMaxOutboundMessageSize(Integer maxOutboundMessageSize) { 96 | this.maxOutboundMessageSize = maxOutboundMessageSize; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/codes/sf/springboot/grpc/client/autoconfigure/GrpcScanAnnotationParser.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client.autoconfigure; 2 | 3 | import codes.sf.springboot.grpc.client.context.GrpcStubScanner; 4 | import org.springframework.beans.BeanUtils; 5 | import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; 6 | import org.springframework.beans.factory.config.BeanDefinition; 7 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 8 | import org.springframework.beans.factory.support.BeanNameGenerator; 9 | import org.springframework.context.ConfigurableApplicationContext; 10 | import org.springframework.context.annotation.ScopeMetadataResolver; 11 | import org.springframework.context.annotation.ScopedProxyMode; 12 | import org.springframework.core.annotation.AnnotationAttributes; 13 | import org.springframework.core.env.Environment; 14 | import org.springframework.core.type.AnnotationMetadata; 15 | import org.springframework.core.type.filter.AbstractTypeHierarchyTraversingFilter; 16 | import org.springframework.util.ClassUtils; 17 | import org.springframework.util.StringUtils; 18 | 19 | import java.lang.annotation.Annotation; 20 | import java.util.Collections; 21 | import java.util.LinkedHashSet; 22 | import java.util.Set; 23 | 24 | import static org.springframework.core.annotation.AnnotationAttributes.fromMap; 25 | 26 | /** 27 | * Parser for {@link codes.sf.springboot.grpc.client.GrpcStubScan @GrpcStubScan} 28 | * and {@link org.springframework.context.annotation.ComponentScan @ComponentScan} 29 | * for configuring gRPC stub scanning. 30 | * 31 | * @author Semyon Fishman 32 | * @see GrpcStubScanner 33 | * @since 0.0.1 34 | */ 35 | class GrpcScanAnnotationParser { 36 | 37 | private static final String DECLARING_CLASS_KEY = 38 | GrpcScanAnnotationParser.class.getCanonicalName() + "_DECLARING_CLASS_KEY"; 39 | 40 | private final Environment environment; 41 | private final BeanDefinitionRegistry registry; 42 | private final GrpcStubScanner scanner; 43 | 44 | public GrpcScanAnnotationParser(Environment environment, BeanDefinitionRegistry registry, GrpcStubScanner scanner) { 45 | this.environment = environment; 46 | this.registry = registry; 47 | this.scanner = scanner; 48 | } 49 | 50 | public String[] parse(Class annotationClass) { 51 | 52 | AnnotationAttributes annotation = findInRegistry(annotationClass); 53 | if (annotation == null) 54 | return null; 55 | 56 | // Generator Class 57 | Class generatorClass = annotation.getClass("nameGenerator"); 58 | if (BeanNameGenerator.class != generatorClass) 59 | scanner.setBeanNameGenerator(BeanUtils.instantiateClass(generatorClass)); 60 | 61 | // Scope 62 | ScopedProxyMode scopedProxyMode = annotation.getEnum("scopedProxy"); 63 | if (scopedProxyMode != ScopedProxyMode.DEFAULT) { 64 | scanner.setScopedProxyMode(scopedProxyMode); 65 | } else { 66 | Class resolverClass = annotation.getClass("scopeResolver"); 67 | scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass)); 68 | } 69 | 70 | // Resource Pattern 71 | scanner.setResourcePattern(annotation.getString("resourcePattern")); 72 | 73 | Set basePackages = new LinkedHashSet<>(); 74 | String[] basePackagesArray = annotation.getStringArray("basePackages"); 75 | for (String pkg : basePackagesArray) { 76 | String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg), 77 | ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); 78 | Collections.addAll(basePackages, tokenized); 79 | } 80 | for (Class clazz : annotation.getClassArray("basePackageClasses")) { 81 | basePackages.add(ClassUtils.getPackageName(clazz)); 82 | } 83 | if (basePackages.isEmpty()) { 84 | String declaringClass = (String) annotation.get(DECLARING_CLASS_KEY); 85 | basePackages.add(ClassUtils.getPackageName(declaringClass)); 86 | } 87 | 88 | scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) { 89 | @Override 90 | protected boolean matchClassName(String className) { 91 | String declaringClass = (String) annotation.get(DECLARING_CLASS_KEY); 92 | return declaringClass.equals(className); 93 | } 94 | }); 95 | 96 | return basePackages.toArray(new String[basePackages.size()]); 97 | } 98 | 99 | private AnnotationAttributes findInRegistry(Class annotationClass) { 100 | String[] candidateNames = registry.getBeanDefinitionNames(); 101 | for (String beanName : candidateNames) { 102 | BeanDefinition beanDef = registry.getBeanDefinition(beanName); 103 | if (beanDef instanceof AnnotatedBeanDefinition) { 104 | AnnotationMetadata metadata = ((AnnotatedBeanDefinition) beanDef).getMetadata(); 105 | if (metadata.isAnnotated(annotationClass.getName())) { 106 | AnnotationAttributes attributes = fromMap(metadata.getAnnotationAttributes(annotationClass.getName(), false)); 107 | attributes.put(DECLARING_CLASS_KEY, beanDef.getBeanClassName()); 108 | return attributes; 109 | } 110 | } 111 | } 112 | return null; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/codes/sf/springboot/grpc/client/autoconfigure/GrpcStubScannerConfiguration.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client.autoconfigure; 2 | 3 | import codes.sf.springboot.grpc.client.GrpcStubScan; 4 | import codes.sf.springboot.grpc.client.context.GrpcStubScanner; 5 | import org.springframework.beans.factory.config.BeanFactoryPostProcessor; 6 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 7 | import org.springframework.context.EnvironmentAware; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.ComponentScan; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.core.env.Environment; 12 | 13 | @Configuration 14 | class GrpcStubScannerConfiguration implements EnvironmentAware { 15 | 16 | private Environment environment; 17 | 18 | @Override 19 | public void setEnvironment(Environment environment) { 20 | this.environment = environment; 21 | } 22 | 23 | @Bean 24 | public BeanFactoryPostProcessor grpcStubScannerBeanFactoryPostProcessor() { 25 | return beanFactory -> { 26 | 27 | BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; 28 | 29 | GrpcStubScanner scanner = new GrpcStubScanner(registry); 30 | GrpcScanAnnotationParser annotationParser = new GrpcScanAnnotationParser(environment, registry, scanner); 31 | 32 | String[] scanPackages = annotationParser.parse(GrpcStubScan.class); 33 | if (scanPackages == null) scanPackages = getEnvironmentProperty(); 34 | if (scanPackages == null) scanPackages = annotationParser.parse(ComponentScan.class); 35 | 36 | if (scanPackages.length != 0) 37 | scanner.scan(scanPackages); 38 | }; 39 | } 40 | 41 | private String[] getEnvironmentProperty() { 42 | return environment.getProperty(GrpcClientProperties.PREFIX + ".scanPackages", String[].class); 43 | } 44 | 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/codes/sf/springboot/grpc/client/context/GrpcStubFactoryBean.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client.context; 2 | 3 | import codes.sf.springboot.grpc.client.GrpcChannelSource; 4 | import codes.sf.springboot.grpc.client.autoconfigure.GrpcClientAutoConfiguration; 5 | import codes.sf.springboot.grpc.client.stubpostprocess.GenericGrpcStubPostProcessor; 6 | import io.grpc.stub.AbstractStub; 7 | import org.springframework.beans.factory.FactoryBean; 8 | import org.springframework.beans.factory.annotation.Qualifier; 9 | import org.springframework.beans.factory.annotation.Required; 10 | import org.springframework.util.MethodInvoker; 11 | 12 | import java.util.List; 13 | 14 | import static java.util.Arrays.asList; 15 | 16 | /** 17 | * {@link FactoryBean} for instantiating gRPC stubs from static classes 18 | * generated by the 19 | * 20 | * gRPC Java Protobuf Compiler 21 | * . 22 | * 23 | *

These static factory classes follow a consistent pattern where each 24 | * stub type is instantiated with static function: 25 | *

    26 | *
  • {@code .newXyzStub(Channel)}
  • 27 | *
  • {@code .newXyzFutureStub(Channel)}
  • 28 | *
  • {@code .newXyzBlockingStub(Channel)}
  • 29 | *
30 | * where Xyz is the stub service name. This factory invokes 31 | * the appropriate static function via reflection. 32 | * 33 | * @param the gRPC stub type this factory produces 34 | * @author Semyon Fishman 35 | * @since 0.0.1 36 | */ 37 | class GrpcStubFactoryBean> implements FactoryBean { 38 | 39 | // Order matters! 40 | private static final List STUB_SUFFIXES 41 | = asList("BlockingStub", "FutureStub", "Stub"); 42 | 43 | private final String factoryClassName; 44 | private final Class stubClass; 45 | 46 | private GrpcChannelSource channelSource; 47 | private List postProcessors; 48 | 49 | public GrpcStubFactoryBean(String factoryClassName, Class stubClass) { 50 | this.factoryClassName = factoryClassName; 51 | this.stubClass = stubClass; 52 | } 53 | 54 | @Required 55 | public void setChannelSource(GrpcChannelSource channelSource) { 56 | this.channelSource = channelSource; 57 | } 58 | 59 | @Required 60 | @Qualifier(GrpcClientAutoConfiguration.GENERIC_GRPC_STUB_POST_PROCESSORS_BEAN_NAME) 61 | public void setGrpcStubPostProcessors(List postProcessors) { 62 | this.postProcessors = postProcessors; 63 | } 64 | 65 | @Override 66 | public boolean isSingleton() { 67 | return true; 68 | } 69 | 70 | @Override 71 | @SuppressWarnings("unchecked") 72 | public S getObject() throws Exception { 73 | 74 | String suffix = STUB_SUFFIXES.stream() 75 | .filter(s -> stubClass.getCanonicalName().endsWith(s)) 76 | .findFirst() 77 | .get(); 78 | 79 | MethodInvoker methodInvoker = new MethodInvoker(); 80 | methodInvoker.setStaticMethod(factoryClassName + ".new" + suffix); 81 | methodInvoker.setArguments(channelSource.resolve(stubClass)); 82 | methodInvoker.prepare(); 83 | 84 | S stub = (S) methodInvoker.invoke(); 85 | 86 | for (GenericGrpcStubPostProcessor processor : postProcessors) { 87 | if (processor.supportsStubType(stub.getClass())) 88 | stub = (S) processor.postProcess(stub); 89 | } 90 | 91 | return stub; 92 | } 93 | 94 | @Override 95 | public Class getObjectType() { 96 | return stubClass; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/codes/sf/springboot/grpc/client/context/GrpcStubScanner.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client.context; 2 | 3 | import org.springframework.beans.factory.CannotLoadBeanClassException; 4 | import org.springframework.beans.factory.config.BeanDefinitionHolder; 5 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 6 | import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; 7 | import org.springframework.context.annotation.ScannedGenericBeanDefinition; 8 | import org.springframework.core.type.filter.AssignableTypeFilter; 9 | 10 | import static org.springframework.beans.factory.support.AbstractBeanDefinition.AUTOWIRE_BY_TYPE; 11 | 12 | /** 13 | * A bean definition scanner that detects gRPC stubs on the classpath, and 14 | * registers them as bean definitions with a given registry ({@code BeanFactory} 15 | * or {@code ApplicationContext}). 16 | * 17 | *

By default it detects all classes on the classpath that extend 18 | * {@link io.grpc.stub.AbstractStub} 19 | * 20 | * @author Semyon Fishman 21 | * @see io.grpc.stub.AbstractStub 22 | * @see 23 | * gRPC Basics - Java 24 | * @see 25 | * gRPC Java Generated Code Reference 26 | * @since 0.0.1 27 | */ 28 | public class GrpcStubScanner extends ClassPathBeanDefinitionScanner { 29 | 30 | 31 | public GrpcStubScanner(BeanDefinitionRegistry registry) { 32 | super(registry, true); 33 | } 34 | 35 | @Override 36 | protected void registerDefaultFilters() { 37 | addIncludeFilter(new AssignableTypeFilter(io.grpc.stub.AbstractStub.class)); 38 | } 39 | 40 | @Override 41 | @SuppressWarnings("unchecked") 42 | protected void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) { 43 | 44 | String beanName = definitionHolder.getBeanName(); 45 | ScannedGenericBeanDefinition definition = (ScannedGenericBeanDefinition) definitionHolder.getBeanDefinition(); 46 | 47 | String factoryClassName = definition.getMetadata().getEnclosingClassName(); 48 | Class stubClass = resolveBeanClass(beanName, definition); 49 | 50 | definition.setBeanClass(GrpcStubFactoryBean.class); 51 | definition.setInstanceSupplier(() -> new GrpcStubFactoryBean(factoryClassName, stubClass)); 52 | definition.setAutowireMode(AUTOWIRE_BY_TYPE); 53 | 54 | super.registerBeanDefinition(definitionHolder, registry); 55 | } 56 | 57 | private Class resolveBeanClass(String beanName, ScannedGenericBeanDefinition definition) { 58 | try { 59 | return definition.resolveBeanClass(getResourceLoader().getClassLoader()); 60 | } catch (ClassNotFoundException e) { 61 | throw new CannotLoadBeanClassException(definition.getResourceDescription(), beanName, definition.getBeanClassName(), e); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/codes/sf/springboot/grpc/client/stubpostprocess/GenericGrpcStubPostProcessor.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client.stubpostprocess; 2 | 3 | import codes.sf.springboot.grpc.client.GrpcStubPostProcessor; 4 | import io.grpc.stub.AbstractStub; 5 | 6 | /** 7 | * Extended variant of {@link GrpcStubPostProcessor} interface that allows 8 | * implementing explicit logic deciding when to apply the processor. 9 | * 10 | * @author Semyon Fishman 11 | * @since 0.0.1 12 | */ 13 | public interface GenericGrpcStubPostProcessor extends GrpcStubPostProcessor { 14 | 15 | /** 16 | * Determine whether this stub post processor should apply to given stub type. 17 | * 18 | * @param stubClass the stub type (never {@code null}) 19 | * @return {@code true} is this processor should be applied to the given 20 | * stub type; {@code false} otherwise 21 | */ 22 | boolean supportsStubType(Class stubClass); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/codes/sf/springboot/grpc/client/stubpostprocess/GenericGrpcStubPostProcessorAdapter.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client.stubpostprocess; 2 | 3 | import codes.sf.springboot.grpc.client.GrpcStubPostProcessor; 4 | import io.grpc.stub.AbstractStub; 5 | import org.springframework.aop.support.AopUtils; 6 | import org.springframework.core.ResolvableType; 7 | import org.springframework.util.Assert; 8 | 9 | import static org.springframework.core.ResolvableType.forClass; 10 | 11 | /** 12 | * {@link GenericGrpcStubPostProcessor} adapter that determines supported stub types 13 | * through introspecting the generically declared type of the delegate listener. 14 | * 15 | * @author Semyon Fishman 16 | * @since 0.0.1 17 | */ 18 | public class GenericGrpcStubPostProcessorAdapter implements GenericGrpcStubPostProcessor { 19 | 20 | private final GrpcStubPostProcessor delegate; 21 | private final ResolvableType declaredStubType; 22 | 23 | public GenericGrpcStubPostProcessorAdapter(GrpcStubPostProcessor delegate) { 24 | Assert.notNull(delegate, "Delegate GrpcStubPostProcessor must not be null"); 25 | this.delegate = delegate; 26 | this.declaredStubType = resolveDeclaredStubType(delegate); 27 | } 28 | 29 | @Override 30 | public boolean supportsStubType(Class stubClass) { 31 | ResolvableType stubType = forClass(stubClass); 32 | return this.declaredStubType.isAssignableFrom(stubType); 33 | } 34 | 35 | @Override 36 | @SuppressWarnings("unchecked") 37 | public AbstractStub postProcess(AbstractStub stub) { 38 | return this.delegate.postProcess(stub); 39 | } 40 | 41 | // https://jira.spring.io/browse/SPR-13698 42 | private static ResolvableType resolveDeclaredStubType(GrpcStubPostProcessor processor) { 43 | ResolvableType declaredStubType = resolveDeclaredStubType(processor.getClass()); 44 | if (declaredStubType.isAssignableFrom(AbstractStub.class)) { 45 | Class targetClass = AopUtils.getTargetClass(processor); 46 | if (targetClass != processor.getClass()) { 47 | declaredStubType = resolveDeclaredStubType(targetClass); 48 | } 49 | } 50 | return declaredStubType; 51 | } 52 | 53 | private static ResolvableType resolveDeclaredStubType(Class processorType) { 54 | ResolvableType resolvableType = forClass(processorType).as(GrpcStubPostProcessor.class); 55 | return resolvableType.getGeneric(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | # Auto Configure 2 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 3 | codes.sf.springboot.grpc.client.autoconfigure.GrpcClientAutoConfiguration -------------------------------------------------------------------------------- /src/test/java/codes/sf/springboot/grpc/client/test/CallCredentialsTests.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client.test; 2 | 3 | import io.grpc.Attributes; 4 | import io.grpc.CallCredentials; 5 | import io.grpc.MethodDescriptor; 6 | import org.junit.Test; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | import java.util.concurrent.Executor; 11 | 12 | import static codes.sf.springboot.grpc.client.test.GrpcTestUtils.test; 13 | import static io.grpc.examples.generated.GreeterGrpc.GreeterStub; 14 | import static org.junit.Assert.assertSame; 15 | 16 | public class CallCredentialsTests { 17 | 18 | @Test 19 | public void callCredentialsTest() { 20 | test(runner -> runner 21 | .withUserConfiguration(CallCredentialsConfiguration.class) 22 | .run(context -> { 23 | CallCredentials callCredentials = context.getBean(CallCredentials.class); 24 | GreeterStub stub = context.getBean(GreeterStub.class); 25 | 26 | assertSame("CallCredentials", 27 | callCredentials, stub.getCallOptions().getCredentials()); 28 | }) 29 | ); 30 | } 31 | 32 | @Configuration 33 | static class CallCredentialsConfiguration { 34 | @Bean 35 | public CallCredentials callCredentials() { 36 | return new CallCredentials() { 37 | @Override 38 | public void applyRequestMetadata(MethodDescriptor method, Attributes attrs, Executor appExecutor, MetadataApplier applier) { 39 | // MOCK 40 | } 41 | 42 | @Override 43 | public void thisUsesUnstableApi() { 44 | // MOCK 45 | } 46 | }; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/test/java/codes/sf/springboot/grpc/client/test/CallOptionsTests.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client.test; 2 | 3 | import io.grpc.examples.generated.GreeterGrpc; 4 | import org.junit.Test; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.core.task.TaskExecutor; 8 | import org.springframework.scheduling.annotation.EnableAsync; 9 | import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor; 10 | 11 | import java.util.concurrent.Executor; 12 | import java.util.concurrent.Executors; 13 | 14 | import static codes.sf.springboot.grpc.client.test.GrpcTestUtils.test; 15 | import static org.junit.Assert.assertEquals; 16 | import static org.junit.Assert.assertSame; 17 | 18 | public class CallOptionsTests { 19 | 20 | @Test 21 | public void springexecutorOptionTest() { 22 | test(runner -> runner 23 | .withUserConfiguration(AsyncConfiguration.class) 24 | .withPropertyValues("grpc.client.springexecutor=true") 25 | .run(context -> { 26 | Executor executor = context.getBean(Executor.class); 27 | GreeterGrpc.GreeterStub stub 28 | = context.getBean(GreeterGrpc.GreeterStub.class); 29 | 30 | assertSame("grpc.client.springexecutor= property doesn't work.", 31 | executor, stub.getCallOptions().getExecutor()); 32 | }) 33 | ); 34 | } 35 | 36 | @EnableAsync 37 | @Configuration 38 | static class AsyncConfiguration { 39 | @Bean 40 | public TaskExecutor taskExecutor() { 41 | return new ConcurrentTaskExecutor( 42 | Executors.newSingleThreadExecutor()); 43 | } 44 | } 45 | 46 | @Test 47 | public void compressionOptionTest() { 48 | test(runner -> runner 49 | .withPropertyValues("grpc.client.compression=gzip") 50 | .run(context -> { 51 | GreeterGrpc.GreeterStub stub 52 | = context.getBean(GreeterGrpc.GreeterStub.class); 53 | 54 | assertEquals("grpc.client.compression= property doesn't work.", 55 | "gzip", stub.getCallOptions().getCompressor()); 56 | }) 57 | ); 58 | } 59 | 60 | @Test 61 | public void maxInboundMessageSizeOptionTest() { 62 | test(runner -> runner 63 | .withPropertyValues("grpc.client.maxInboundMessageSize=123") 64 | .run(context -> { 65 | GreeterGrpc.GreeterStub stub 66 | = context.getBean(GreeterGrpc.GreeterStub.class); 67 | 68 | assertEquals("grpc.client.maxInboundMessageSize= property doesn't work.", 69 | Integer.valueOf(123), stub.getCallOptions().getMaxInboundMessageSize()); 70 | }) 71 | ); 72 | } 73 | 74 | @Test 75 | public void maxOutboundMessageSizeOptionTest() { 76 | test(runner -> runner 77 | .withPropertyValues("grpc.client.maxOutboundMessageSize=567") 78 | .run(context -> { 79 | GreeterGrpc.GreeterStub stub 80 | = context.getBean(GreeterGrpc.GreeterStub.class); 81 | 82 | assertEquals("grpc.client.maxOutboundMessageSize= property doesn't work.", 83 | Integer.valueOf(567), stub.getCallOptions().getMaxOutboundMessageSize()); 84 | }) 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/test/java/codes/sf/springboot/grpc/client/test/ChannelTests.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client.test; 2 | 3 | import codes.sf.springboot.grpc.client.GrpcChannelSource; 4 | import io.grpc.Channel; 5 | import io.grpc.ManagedChannelBuilder; 6 | import io.grpc.examples.generated.GreeterGrpc; 7 | import org.junit.Test; 8 | import org.springframework.beans.factory.UnsatisfiedDependencyException; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | import static codes.sf.springboot.grpc.client.autoconfigure.GrpcClientProperties.DEFAULT_PORT; 13 | import static codes.sf.springboot.grpc.client.test.GrpcTestUtils.testWithServer; 14 | 15 | public class ChannelTests { 16 | 17 | @Test 18 | public void unConfiguredChannel() { 19 | testWithServer(DEFAULT_PORT, runner -> { 20 | runner.run(GrpcTestUtils::assertStubsWork); 21 | }); 22 | } 23 | 24 | @Test 25 | public void emptyConfiguredChannel() { 26 | testWithServer(DEFAULT_PORT, runner -> { 27 | runner.withPropertyValues("grpc.client.target=") 28 | .run(GrpcTestUtils::assertStubsWork); 29 | }); 30 | } 31 | 32 | @Test 33 | public void propertiesConfiguredChannel() { 34 | testWithServer(2002, runner -> { 35 | runner.withPropertyValues("grpc.client.target=localhost:2002") 36 | .run(GrpcTestUtils::assertStubsWork); 37 | }); 38 | } 39 | 40 | @Test 41 | public void beanConfiguredChannel() { 42 | testWithServer(2003, runner -> { 43 | runner.withUserConfiguration(Channel2003Configuration.class) 44 | .run(GrpcTestUtils::assertStubsWork); 45 | }); 46 | } 47 | 48 | @Configuration 49 | static class Channel2003Configuration { 50 | @Bean 51 | public Channel channel2003() { 52 | Channel channel = ManagedChannelBuilder 53 | .forAddress("localhost", 2003) 54 | .usePlaintext() 55 | .build(); 56 | return channel; 57 | } 58 | } 59 | 60 | @Test 61 | public void beanConfiguredChannelSource() { 62 | testWithServer(2005, runner -> { 63 | runner.withUserConfiguration(GrpcChannelSource2005Configuration.class) 64 | .run(GrpcTestUtils::assertStubsWork); 65 | }); 66 | } 67 | 68 | @Configuration 69 | static class GrpcChannelSource2005Configuration { 70 | 71 | private final Channel channel = ManagedChannelBuilder 72 | .forAddress("localhost", 2005) 73 | .usePlaintext() 74 | .build(); 75 | 76 | @Bean 77 | public GrpcChannelSource grpcChannelSource2004() { 78 | return GrpcChannelSource.of(channel); 79 | } 80 | } 81 | 82 | @Test(expected = UnsatisfiedDependencyException.class) 83 | public void multipleBeanConfiguredChannelSources() throws Throwable { 84 | try { 85 | testWithServer(2005, runner -> { 86 | runner.withUserConfiguration(MultipleGrpcChannelSourcesConfiguration.class) 87 | .run(context -> { 88 | context.getBean(GreeterGrpc.GreeterStub.class); 89 | }); 90 | }); 91 | } catch (IllegalStateException e) { 92 | if (e.getCause() instanceof UnsatisfiedDependencyException) 93 | throw e.getCause(); 94 | else 95 | throw e; 96 | } 97 | } 98 | 99 | @Configuration 100 | static class MultipleGrpcChannelSourcesConfiguration { 101 | 102 | private final Channel channel = ManagedChannelBuilder 103 | .forAddress("localhost", 2005) 104 | .usePlaintext() 105 | .build(); 106 | 107 | @Bean 108 | public GrpcChannelSource grpcChannelSource1() { 109 | return GrpcChannelSource.of(channel); 110 | } 111 | 112 | @Bean 113 | public GrpcChannelSource grpcChannelSource2() { 114 | return GrpcChannelSource.of(channel); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/codes/sf/springboot/grpc/client/test/ClientInterceptorTests.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client.test; 2 | 3 | import io.grpc.*; 4 | import io.grpc.examples.generated.GreeterGrpc; 5 | import io.grpc.examples.generated.GreeterOuterClass; 6 | import org.junit.Test; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.core.annotation.Order; 10 | 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | 14 | import static codes.sf.springboot.grpc.client.test.GrpcTestUtils.test; 15 | import static org.junit.Assert.assertEquals; 16 | import static org.junit.Assert.assertTrue; 17 | 18 | public class ClientInterceptorTests { 19 | 20 | private static List testlog = new ArrayList<>(); 21 | 22 | @Test 23 | public void interceptorsOptionTest() { 24 | 25 | testlog.clear(); // make sure it's empty 26 | 27 | test(runner -> runner 28 | .withUserConfiguration(ClientInterceptorsConfiguration.class) 29 | .run(context -> { 30 | GreeterGrpc.GreeterFutureStub stub 31 | = context.getBean(GreeterGrpc.GreeterFutureStub.class); 32 | 33 | stub.sayHello(GreeterOuterClass.HelloRequest.newBuilder().build()); 34 | 35 | // First check that interceptors were called in the first place 36 | assertTrue("clientInterceptorA did not run!", 37 | testlog.contains("clientInterceptorA")); 38 | assertTrue("clientInterceptorB did not run!", 39 | testlog.contains("clientInterceptorB")); 40 | assertTrue("clientInterceptorC did not run!", 41 | testlog.contains("clientInterceptorC")); 42 | 43 | // Then check that interceptors were called in correct order 44 | assertEquals("clientInterceptorA run out of order!", 45 | "clientInterceptorA", testlog.get(2)); 46 | assertEquals("clientInterceptorB run out of order!", 47 | "clientInterceptorB", testlog.get(1)); 48 | assertEquals("clientInterceptorC run out of order!", 49 | "clientInterceptorC", testlog.get(0)); 50 | }) 51 | ); 52 | } 53 | 54 | @Configuration 55 | static class ClientInterceptorsConfiguration { 56 | 57 | @Order(1) 58 | @Bean 59 | public ClientInterceptor clientInterceptorA() { 60 | return new ClientInterceptor() { 61 | @Override 62 | public ClientCall interceptCall( 63 | MethodDescriptor method, 64 | CallOptions callOptions, 65 | Channel next) { 66 | 67 | testlog.add("clientInterceptorA"); 68 | 69 | return next.newCall(method, callOptions); 70 | } 71 | }; 72 | } 73 | 74 | @Order(2) 75 | @Bean 76 | public ClientInterceptor clientInterceptorB() { 77 | return new ClientInterceptor() { 78 | @Override 79 | public ClientCall interceptCall( 80 | MethodDescriptor method, 81 | CallOptions callOptions, 82 | Channel next) { 83 | 84 | testlog.add("clientInterceptorB"); 85 | 86 | return next.newCall(method, callOptions); 87 | } 88 | }; 89 | } 90 | 91 | @Order(3) 92 | @Bean 93 | public ClientInterceptor clientInterceptorC() { 94 | return new ClientInterceptor() { 95 | @Override 96 | public ClientCall interceptCall( 97 | MethodDescriptor method, 98 | CallOptions callOptions, 99 | Channel next) { 100 | 101 | testlog.add("clientInterceptorC"); 102 | 103 | return next.newCall(method, callOptions); 104 | } 105 | }; 106 | } 107 | 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/test/java/codes/sf/springboot/grpc/client/test/GreeterService.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client.test; 2 | 3 | import io.grpc.examples.generated.GreeterGrpc; 4 | import io.grpc.examples.generated.GreeterOuterClass; 5 | import io.grpc.stub.StreamObserver; 6 | 7 | import java.util.logging.Logger; 8 | 9 | import static io.grpc.examples.generated.GreeterOuterClass.HelloReply; 10 | 11 | public class GreeterService extends GreeterGrpc.GreeterImplBase { 12 | 13 | private static final Logger LOGGER = Logger.getLogger(GreeterService.class.getName()); 14 | 15 | @Override 16 | public void sayHello(GreeterOuterClass.HelloRequest request, StreamObserver responseObserver) { 17 | 18 | LOGGER.info("gRPC request{" + request + "}"); 19 | 20 | HelloReply reply = HelloReply.newBuilder() 21 | .setMessage(expectedSayHello(request.getName())) 22 | .build(); 23 | responseObserver.onNext(reply); 24 | 25 | LOGGER.info("gRPC response{" + reply + "}"); 26 | 27 | responseObserver.onCompleted(); 28 | } 29 | 30 | /** 31 | * EXPOSED STATICALLY FOR TESTING PURPOSES ONLY! 32 | * 33 | * @param name 34 | * @return 35 | */ 36 | public static String expectedSayHello(String name) { 37 | return "Hello " + name; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/codes/sf/springboot/grpc/client/test/GrpcClientPropertiesTests.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client.test; 2 | 3 | import com.openpojo.reflection.filters.FilterClassName; 4 | import com.openpojo.validation.Validator; 5 | import com.openpojo.validation.ValidatorBuilder; 6 | import com.openpojo.validation.rule.impl.GetterMustExistRule; 7 | import com.openpojo.validation.rule.impl.SetterMustExistRule; 8 | import com.openpojo.validation.test.impl.GetterTester; 9 | import com.openpojo.validation.test.impl.SetterTester; 10 | import org.junit.Test; 11 | 12 | public class GrpcClientPropertiesTests { 13 | 14 | @Test 15 | public void propertiesTest() { 16 | 17 | Validator validator = ValidatorBuilder.create() 18 | .with(new GetterMustExistRule()) 19 | .with(new SetterMustExistRule()) 20 | .with(new SetterTester()) 21 | .with(new GetterTester()) 22 | .build(); 23 | 24 | validator.validate("codes.sf.springboot.grpc.client.autoconfigure", 25 | new FilterClassName("GrpcClientProperties")); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/codes/sf/springboot/grpc/client/test/GrpcScannerTests.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client.test; 2 | 3 | import codes.sf.springboot.grpc.client.GrpcStubScan; 4 | import codes.sf.springboot.grpc.client.autoconfigure.GrpcClientAutoConfiguration; 5 | import codes.sf.springboot.grpc.client.context.GrpcStubScanner; 6 | import org.junit.Test; 7 | import org.springframework.beans.factory.CannotLoadBeanClassException; 8 | import org.springframework.beans.factory.config.BeanDefinition; 9 | import org.springframework.beans.factory.config.BeanDefinitionHolder; 10 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 11 | import org.springframework.beans.factory.support.DefaultBeanNameGenerator; 12 | import org.springframework.beans.factory.support.DefaultListableBeanFactory; 13 | import org.springframework.boot.autoconfigure.AutoConfigurations; 14 | import org.springframework.boot.test.context.assertj.AssertableApplicationContext; 15 | import org.springframework.boot.test.context.runner.ApplicationContextRunner; 16 | import org.springframework.context.annotation.ScopedProxyMode; 17 | 18 | import static io.grpc.examples.generated.GreeterGrpc.*; 19 | import static org.assertj.core.api.Assertions.assertThat; 20 | 21 | 22 | public class GrpcScannerTests { 23 | 24 | @Test 25 | public void emptyScanPackagesPropertyTest() { 26 | 27 | new ApplicationContextRunner() 28 | .withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class)) 29 | .withPropertyValues("grpc.client.scanPackages=") 30 | .run(this::assertDoesNotHaveGreeterStubs); 31 | } 32 | 33 | @Test 34 | public void springBootApplicationAnnotationTest() { 35 | 36 | new ApplicationContextRunner() 37 | .withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class)) 38 | .withUserConfiguration(io.grpc.examples.SpringBootApplicationAnnotationConfiguration.class) 39 | .run(this::assertHasGreeterStubs); 40 | } 41 | 42 | @Test 43 | public void componentScanAnnotationTest() { 44 | 45 | new ApplicationContextRunner() 46 | .withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class)) 47 | .withUserConfiguration(io.grpc.examples.ComponentScanAnnotationConfiguration.class) 48 | .run(this::assertHasGreeterStubs); 49 | } 50 | 51 | // 52 | @Test 53 | public void grpcStubScanAnnotationTest() { 54 | 55 | new ApplicationContextRunner() 56 | .withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class)) 57 | .withUserConfiguration(io.grpc.examples.GrpcStubScanAnnotationConfiguration.class) 58 | .run(this::assertHasGreeterStubs); 59 | } 60 | 61 | @Test 62 | public void basePackageClassesAnnotationTest() { 63 | 64 | new ApplicationContextRunner() 65 | .withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class)) 66 | .withUserConfiguration(BasePackageClassesAnnotationConfiguration.class) 67 | .run(this::assertHasGreeterStubs); 68 | } 69 | 70 | @GrpcStubScan(basePackageClasses = io.grpc.examples.generated.GreeterGrpc.class) 71 | static class BasePackageClassesAnnotationConfiguration { 72 | } 73 | 74 | @Test 75 | public void basePackagesAnnotationTest() { 76 | 77 | new ApplicationContextRunner() 78 | .withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class)) 79 | .withUserConfiguration(BasePackagesAnnotationConfiguration.class) 80 | .run(this::assertHasGreeterStubs); 81 | } 82 | 83 | @GrpcStubScan(basePackages = "io.grpc.examples") 84 | static class BasePackagesAnnotationConfiguration { 85 | } 86 | 87 | @Test 88 | public void beanNameAnnotationTest() { 89 | 90 | new ApplicationContextRunner() 91 | .withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class)) 92 | .withUserConfiguration(BeanNameGeneratorAnnotationConfiguration.class) 93 | .run(context -> { 94 | this.assertHasGreeterStubs(context); 95 | assertThat(context).hasBean("Stub1"); 96 | assertThat(context).hasBean("Stub2"); 97 | assertThat(context).hasBean("Stub3"); 98 | }); 99 | } 100 | 101 | @GrpcStubScan(basePackages = "io.grpc.examples", 102 | nameGenerator = TestBeanNameGenerator.class) 103 | static class BeanNameGeneratorAnnotationConfiguration { 104 | } 105 | 106 | static class TestBeanNameGenerator extends DefaultBeanNameGenerator { 107 | 108 | private int i = 0; 109 | 110 | @Override 111 | public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) { 112 | i++; 113 | return "Stub" + i; 114 | } 115 | } 116 | 117 | @Test 118 | public void scopedProxyAnnotationTest() { 119 | 120 | new ApplicationContextRunner() 121 | .withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class)) 122 | .withUserConfiguration(ScopedProxyAnnotationConfiguration.class) 123 | .run(context -> { 124 | // TODO: how do I test this other than just setting it? 125 | }); 126 | } 127 | 128 | @GrpcStubScan(basePackages = "io.grpc.examples", 129 | scopedProxy = ScopedProxyMode.TARGET_CLASS) 130 | static class ScopedProxyAnnotationConfiguration { 131 | } 132 | 133 | ////// 134 | 135 | private void assertHasGreeterStubs(AssertableApplicationContext context) { 136 | 137 | // io.grpc.examples stubs should be found by default 138 | assertThat(context).hasSingleBean(GreeterStub.class); 139 | assertThat(context).hasSingleBean(GreeterBlockingStub.class); 140 | assertThat(context).hasSingleBean(GreeterFutureStub.class); 141 | } 142 | 143 | private void assertDoesNotHaveGreeterStubs(AssertableApplicationContext context) { 144 | 145 | // io.grpc.examples stubs should be found by default 146 | assertThat(context).doesNotHaveBean(GreeterStub.class); 147 | assertThat(context).doesNotHaveBean(GreeterBlockingStub.class); 148 | assertThat(context).doesNotHaveBean(GreeterFutureStub.class); 149 | } 150 | 151 | @Test(expected = CannotLoadBeanClassException.class) 152 | public void cannotLoadBeanClassExceptionTest() { 153 | 154 | BeanDefinitionRegistry registry = new DefaultListableBeanFactory(); 155 | GrpcStubScanner scanner = new BrokenGrpcStubScanner(registry); 156 | scanner.scan("io.grpc.examples"); 157 | } 158 | 159 | static class BrokenGrpcStubScanner extends GrpcStubScanner { 160 | public BrokenGrpcStubScanner(BeanDefinitionRegistry registry) { 161 | super(registry); 162 | } 163 | 164 | @Override 165 | protected void registerBeanDefinition(BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) { 166 | definitionHolder.getBeanDefinition().setBeanClassName("invalid.class.name"); 167 | super.registerBeanDefinition(definitionHolder, registry); 168 | } 169 | } 170 | 171 | 172 | } 173 | -------------------------------------------------------------------------------- /src/test/java/codes/sf/springboot/grpc/client/test/GrpcStubPostProcessorTests.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client.test; 2 | 3 | import codes.sf.springboot.grpc.client.GrpcStubPostProcessor; 4 | import codes.sf.springboot.grpc.client.stubpostprocess.GenericGrpcStubPostProcessor; 5 | import io.grpc.CallOptions; 6 | import io.grpc.stub.AbstractStub; 7 | import org.junit.Test; 8 | import org.springframework.aop.framework.AdvisedSupport; 9 | import org.springframework.aop.framework.AopProxy; 10 | import org.springframework.aop.framework.DefaultAopProxyFactory; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.core.annotation.Order; 14 | 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | 18 | import static codes.sf.springboot.grpc.client.test.GrpcTestUtils.test; 19 | import static io.grpc.examples.generated.GreeterGrpc.*; 20 | import static java.util.regex.Pattern.quote; 21 | import static java.util.stream.Collectors.toList; 22 | import static org.junit.Assert.assertEquals; 23 | import static org.junit.Assert.assertNull; 24 | 25 | public class GrpcStubPostProcessorTests { 26 | 27 | private static final CallOptions.Key regularProcessorKey 28 | = CallOptions.Key.create("regularProcessorKey"); 29 | 30 | private static final CallOptions.Key blockingStubOnlyProcessorKey 31 | = CallOptions.Key.create("blockingStubOnlyProcessorKey"); 32 | 33 | private static final CallOptions.Key proxiedProcessorKey 34 | = CallOptions.Key.create("proxiedProcessorKey"); 35 | 36 | private static final CallOptions.Key genericProcessorKey 37 | = CallOptions.Key.create("genericProcessorKey"); 38 | 39 | private static final List testlog = new ArrayList<>(); 40 | 41 | private static void log(Class aClass, String message) { 42 | testlog.add(aClass.getCanonicalName() + message); 43 | } 44 | 45 | private static List getLogFor(Class aClass) { 46 | return testlog.stream() 47 | .filter(s -> s.startsWith(aClass.getCanonicalName())) 48 | .map(s -> s.replaceFirst(quote(aClass.getCanonicalName()), "")) 49 | .collect(toList()); 50 | } 51 | 52 | @Test 53 | public void grpcStubPostProcessorTest() { 54 | 55 | testlog.clear(); // clear before test 56 | 57 | test(runner -> runner 58 | .withUserConfiguration(GrpcStubPostProcessorConfiguration.class) 59 | .run(context -> { 60 | 61 | // 62 | GreeterStub stub = context.getBean(GreeterStub.class); 63 | GreeterFutureStub futureStub = context.getBean(GreeterFutureStub.class); 64 | GreeterBlockingStub blockingStub = context.getBean(GreeterBlockingStub.class); 65 | 66 | // This checks if the order was right 67 | List stubLog = getLogFor(GreeterStub.class); 68 | assertEquals(2, stubLog.size()); 69 | assertEquals("proxiedProcessored", stubLog.get(0)); 70 | assertEquals("regularProcessored", stubLog.get(1)); 71 | 72 | // 73 | List futureStubLog = getLogFor(GreeterFutureStub.class); 74 | assertEquals(3, futureStubLog.size()); 75 | assertEquals("proxiedProcessored", futureStubLog.get(0)); 76 | assertEquals("regularProcessored", futureStubLog.get(1)); 77 | assertEquals("genericGrpcStubPostProcessored", futureStubLog.get(2)); 78 | 79 | // 80 | List blockingStubLog = getLogFor(GreeterBlockingStub.class); 81 | assertEquals(3, blockingStubLog.size()); 82 | assertEquals("proxiedProcessored", blockingStubLog.get(0)); 83 | assertEquals("blockingStubOnlyProcessored", blockingStubLog.get(1)); 84 | assertEquals("regularProcessored", blockingStubLog.get(2)); 85 | 86 | // 87 | assertEquals("regularProcessorValue", stub.getCallOptions().getOption(regularProcessorKey)); 88 | assertEquals("proxiedProcessorValue", stub.getCallOptions().getOption(proxiedProcessorKey)); 89 | assertNull(stub.getCallOptions().getOption(blockingStubOnlyProcessorKey)); 90 | assertNull(stub.getCallOptions().getOption(genericProcessorKey)); 91 | 92 | // 93 | assertEquals("regularProcessorValue", futureStub.getCallOptions().getOption(regularProcessorKey)); 94 | assertEquals("proxiedProcessorValue", futureStub.getCallOptions().getOption(proxiedProcessorKey)); 95 | assertNull(futureStub.getCallOptions().getOption(blockingStubOnlyProcessorKey)); 96 | assertEquals("genericProcessorValue", futureStub.getCallOptions().getOption(genericProcessorKey)); 97 | 98 | // 99 | assertEquals("regularProcessorValue", blockingStub.getCallOptions().getOption(regularProcessorKey)); 100 | assertEquals("proxiedProcessorValue", blockingStub.getCallOptions().getOption(proxiedProcessorKey)); 101 | assertEquals("blockingStubOnlyProcessorValue", blockingStub.getCallOptions().getOption(blockingStubOnlyProcessorKey)); 102 | assertNull(blockingStub.getCallOptions().getOption(genericProcessorKey)); 103 | 104 | }) 105 | ); 106 | } 107 | 108 | @Configuration 109 | static class GrpcStubPostProcessorConfiguration { 110 | 111 | 112 | @Order(3) 113 | @Bean 114 | public GrpcStubPostProcessor regularProcessor() { 115 | return stub -> { 116 | log(stub.getClass(), "regularProcessored"); 117 | return stub.withOption(regularProcessorKey, "regularProcessorValue"); 118 | }; 119 | } 120 | 121 | @Order(1) 122 | @Bean 123 | public GrpcStubPostProcessor proxiedProcessor() { 124 | DefaultAopProxyFactory proxyFactory = new DefaultAopProxyFactory(); 125 | AdvisedSupport advisedSupport = new AdvisedSupport(); 126 | advisedSupport.setTarget((GrpcStubPostProcessor) stub -> { 127 | log(stub.getClass(), "proxiedProcessored"); 128 | return stub.withOption(proxiedProcessorKey, "proxiedProcessorValue"); 129 | }); 130 | advisedSupport.setProxyTargetClass(true); 131 | AopProxy aopProxy = proxyFactory.createAopProxy(advisedSupport); 132 | return (GrpcStubPostProcessor) aopProxy.getProxy(); 133 | } 134 | 135 | @Order(2) 136 | @Bean 137 | public GrpcStubPostProcessor blockingStubOnlyProcessor() { 138 | // Due to known bug in Spring, this implementation cannot be lambda because 139 | // it'll lose it's generic type, see https://jira.spring.io/browse/SPR-13698 140 | //noinspection Convert2Lambda 141 | return new GrpcStubPostProcessor() { 142 | @Override 143 | public GreeterBlockingStub postProcess(GreeterBlockingStub stub) { 144 | log(stub.getClass(), "blockingStubOnlyProcessored"); 145 | return stub.withOption(blockingStubOnlyProcessorKey, "blockingStubOnlyProcessorValue"); 146 | } 147 | }; 148 | } 149 | 150 | @Order(4) 151 | @Bean 152 | public GenericGrpcStubPostProcessor genericGrpcStubPostProcessor() { 153 | return new GenericGrpcStubPostProcessor() { 154 | @Override 155 | public boolean supportsStubType(Class stubClass) { 156 | return GreeterFutureStub.class.isAssignableFrom(stubClass); 157 | } 158 | 159 | @Override 160 | public AbstractStub postProcess(AbstractStub stub) { 161 | log(stub.getClass(), "genericGrpcStubPostProcessored"); 162 | return stub.withOption(genericProcessorKey, "genericProcessorValue"); 163 | } 164 | }; 165 | } 166 | 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/test/java/codes/sf/springboot/grpc/client/test/GrpcTestUtils.java: -------------------------------------------------------------------------------- 1 | package codes.sf.springboot.grpc.client.test; 2 | 3 | import codes.sf.springboot.grpc.client.autoconfigure.GrpcClientAutoConfiguration; 4 | import com.github.javafaker.Faker; 5 | import com.google.common.util.concurrent.ListenableFuture; 6 | import io.grpc.Server; 7 | import io.grpc.ServerBuilder; 8 | import io.grpc.examples.generated.GreeterGrpc; 9 | import io.grpc.stub.StreamObserver; 10 | import org.springframework.boot.autoconfigure.AutoConfigurations; 11 | import org.springframework.boot.test.context.assertj.AssertableApplicationContext; 12 | import org.springframework.boot.test.context.runner.ApplicationContextRunner; 13 | 14 | import java.io.IOException; 15 | import java.io.UncheckedIOException; 16 | import java.util.Random; 17 | import java.util.concurrent.CompletableFuture; 18 | import java.util.concurrent.ExecutionException; 19 | import java.util.function.Consumer; 20 | 21 | import static codes.sf.springboot.grpc.client.test.GreeterService.expectedSayHello; 22 | import static io.grpc.examples.generated.GreeterOuterClass.HelloReply; 23 | import static io.grpc.examples.generated.GreeterOuterClass.HelloRequest.newBuilder; 24 | import static org.assertj.core.api.Assertions.assertThat; 25 | import static org.assertj.core.api.Assertions.fail; 26 | import static org.junit.Assert.assertEquals; 27 | 28 | abstract class GrpcTestUtils { 29 | 30 | private static final Faker faker = new Faker(new Random(1)); 31 | 32 | private GrpcTestUtils() { 33 | // static class; prevent instantiation 34 | } 35 | 36 | private static ApplicationContextRunner applicationContextRunner() { 37 | return new ApplicationContextRunner() 38 | .withPropertyValues("grpc.client.scanPackages=io.grpc.examples") 39 | .withConfiguration(AutoConfigurations.of(GrpcClientAutoConfiguration.class)); 40 | } 41 | 42 | public static void test(Consumer runnerConsumer) { 43 | runnerConsumer.accept(applicationContextRunner()); 44 | } 45 | 46 | public static void testWithServer(int serverPort, Consumer runnerConsumer) { 47 | 48 | Server server = ServerBuilder 49 | .forPort(serverPort) 50 | .addService(new GreeterService()) 51 | .build(); 52 | 53 | try { 54 | server.start(); 55 | } catch (IOException e) { 56 | throw new UncheckedIOException("Failed to start gRPC server for test.", e); 57 | } 58 | 59 | try { 60 | test(runnerConsumer); 61 | } finally { 62 | server.shutdownNow(); 63 | } 64 | 65 | try { 66 | server.awaitTermination(); 67 | } catch (InterruptedException e) { 68 | throw new RuntimeException(e); 69 | } 70 | } 71 | 72 | public static void assertStubsWork(AssertableApplicationContext context) { 73 | assertStubWorks(context); 74 | assertBlockingStubWorks(context); 75 | assertFutureStubWorks(context); 76 | } 77 | 78 | public static void assertBlockingStubWorks(AssertableApplicationContext context) { 79 | 80 | assertThat(context).hasSingleBean(GreeterGrpc.GreeterBlockingStub.class); 81 | 82 | GreeterGrpc.GreeterBlockingStub stub 83 | = context.getBean(GreeterGrpc.GreeterBlockingStub.class); 84 | 85 | String name = faker.name().fullName(); 86 | 87 | HelloReply reply = stub.sayHello(newBuilder().setName(name).build()); 88 | 89 | assertEquals(expectedSayHello(name), reply.getMessage()); 90 | } 91 | 92 | private static void assertStubWorks(AssertableApplicationContext context) { 93 | 94 | assertThat(context).hasSingleBean(GreeterGrpc.GreeterStub.class); 95 | 96 | GreeterGrpc.GreeterStub stub 97 | = context.getBean(GreeterGrpc.GreeterStub.class); 98 | 99 | String name = faker.name().fullName(); 100 | 101 | CompletableFuture future = new CompletableFuture<>(); 102 | stub.sayHello(newBuilder().setName(name).build(), new StreamObserver() { 103 | 104 | private HelloReply value; 105 | 106 | @Override 107 | public void onNext(HelloReply value) { 108 | this.value = value; 109 | } 110 | 111 | @Override 112 | public void onError(Throwable t) { 113 | future.completeExceptionally(t); 114 | } 115 | 116 | @Override 117 | public void onCompleted() { 118 | future.complete(value); 119 | } 120 | }); 121 | 122 | try { 123 | assertEquals(expectedSayHello(name), future.get().getMessage()); 124 | } catch (InterruptedException e) { 125 | fail(e.getMessage(), e); 126 | } catch (ExecutionException e) { 127 | fail(e.getCause().getMessage(), e.getCause()); 128 | } 129 | } 130 | 131 | private static void assertFutureStubWorks(AssertableApplicationContext context) { 132 | 133 | assertThat(context).hasSingleBean(GreeterGrpc.GreeterStub.class); 134 | 135 | GreeterGrpc.GreeterFutureStub stub 136 | = context.getBean(GreeterGrpc.GreeterFutureStub.class); 137 | 138 | String name = faker.name().fullName(); 139 | 140 | 141 | ListenableFuture future = stub.sayHello(newBuilder().setName(name).build()); 142 | try { 143 | assertEquals(expectedSayHello(name), future.get().getMessage()); 144 | } catch (InterruptedException e) { 145 | fail(e.getMessage(), e); 146 | } catch (ExecutionException e) { 147 | fail(e.getCause().getMessage(), e.getCause()); 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/test/java/io/grpc/examples/ComponentScanAnnotationConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.grpc.examples; 2 | 3 | import org.springframework.context.annotation.ComponentScan; 4 | 5 | @ComponentScan 6 | public class ComponentScanAnnotationConfiguration { 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/io/grpc/examples/GrpcStubScanAnnotationConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.grpc.examples; 2 | 3 | import codes.sf.springboot.grpc.client.GrpcStubScan; 4 | 5 | @GrpcStubScan 6 | public class GrpcStubScanAnnotationConfiguration { 7 | } 8 | -------------------------------------------------------------------------------- /src/test/java/io/grpc/examples/SpringBootApplicationAnnotationConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.grpc.examples; 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | @SpringBootApplication 6 | public class SpringBootApplicationAnnotationConfiguration { 7 | } 8 | -------------------------------------------------------------------------------- /src/test/proto/greeter.proto: -------------------------------------------------------------------------------- 1 | // Copied from https://grpc.io/docs/quickstart/java.html#update-a-grpc-service 2 | 3 | syntax = "proto3"; 4 | 5 | option java_package = "io.grpc.examples.generated"; 6 | 7 | // The greeter service definition. 8 | service Greeter { 9 | // Sends a greeting 10 | rpc SayHello (HelloRequest) returns (HelloReply) { 11 | } 12 | 13 | } 14 | 15 | // The request message containing the user's name. 16 | message HelloRequest { 17 | string name = 1; 18 | } 19 | 20 | // The response message containing the greetings 21 | message HelloReply { 22 | string message = 1; 23 | } 24 | 25 | 26 | --------------------------------------------------------------------------------