├── .github └── workflows │ └── maven.yml ├── .gitignore ├── README-en.md ├── README.md ├── calculator-api ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── alibaba │ └── calculator │ ├── ExchangeCalculatorService.java │ ├── ExchangeRequest.java │ └── MathCalculatorService.java ├── client-app ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── alibaba │ │ │ └── rsocket │ │ │ └── client │ │ │ ├── PortalController.java │ │ │ ├── RSocketClientApplication.java │ │ │ └── RSocketConfig.java │ └── resources │ │ ├── application.properties │ │ └── bootstrap.properties │ └── test │ └── java │ └── com │ └── alibaba │ └── rsocket │ └── registry │ └── ConsulDiscoveryClientTest.java ├── docker-compose.yml ├── index.http ├── justfile ├── loadbalance-structure.png ├── pom.xml ├── rsocket-loadbalance-spring-boot-starter ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── alibaba │ │ └── rsocket │ │ └── loadbalance │ │ ├── RSocketLoadBalanceConfiguration.java │ │ ├── RSocketLoadBalanceEndpoint.java │ │ ├── RSocketServerInstance.java │ │ ├── RSocketServiceDiscoveryRegistry.java │ │ ├── RSocketServiceRegistry.java │ │ └── proxy │ │ ├── DefaultMethodHandler.java │ │ ├── RSocketRemoteCallInvocationHandler.java │ │ └── RSocketRemoteServiceBuilder.java │ └── resources │ └── META-INF │ └── spring.factories └── server-app ├── pom.xml └── src └── main ├── java └── com │ └── alibaba │ └── calculator │ ├── RSocketServerApplication.java │ ├── annotations │ ├── RSocketHandler.java │ └── SpringRSocketService.java │ └── impl │ ├── ExchangeCalculatorImpl.java │ └── MathCalculatorImpl.java └── resources ├── application.properties └── bootstrap.properties /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 1.8 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 1.8 23 | - name: Build with Maven 24 | run: mvn -DskipTests clean package --file pom.xml 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/java,maven,jetbrains+all 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=java,maven,jetbrains+all 3 | 4 | ### Java ### 5 | # Compiled class file 6 | *.class 7 | 8 | # Log file 9 | *.log 10 | 11 | # BlueJ files 12 | *.ctxt 13 | 14 | # Mobile Tools for Java (J2ME) 15 | .mtj.tmp/ 16 | 17 | # Package Files # 18 | *.jar 19 | *.war 20 | *.nar 21 | *.ear 22 | *.zip 23 | *.tar.gz 24 | *.rar 25 | 26 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 27 | hs_err_pid* 28 | 29 | ### JetBrains+all ### 30 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 31 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 32 | 33 | # User-specific stuff 34 | .idea/**/workspace.xml 35 | .idea/**/tasks.xml 36 | .idea/**/usage.statistics.xml 37 | .idea/**/dictionaries 38 | .idea/**/shelf 39 | 40 | # Generated files 41 | .idea/**/contentModel.xml 42 | 43 | # Sensitive or high-churn files 44 | .idea/**/dataSources/ 45 | .idea/**/dataSources.ids 46 | .idea/**/dataSources.local.xml 47 | .idea/**/sqlDataSources.xml 48 | .idea/**/dynamic.xml 49 | .idea/**/uiDesigner.xml 50 | .idea/**/dbnavigator.xml 51 | 52 | # Gradle 53 | .idea/**/gradle.xml 54 | .idea/**/libraries 55 | 56 | # Gradle and Maven with auto-import 57 | # When using Gradle or Maven with auto-import, you should exclude module files, 58 | # since they will be recreated, and may cause churn. Uncomment if using 59 | # auto-import. 60 | # .idea/artifacts 61 | # .idea/compiler.xml 62 | # .idea/jarRepositories.xml 63 | # .idea/modules.xml 64 | # .idea/*.iml 65 | # .idea/modules 66 | # *.iml 67 | # *.ipr 68 | 69 | # CMake 70 | cmake-build-*/ 71 | 72 | # Mongo Explorer plugin 73 | .idea/**/mongoSettings.xml 74 | 75 | # File-based project format 76 | *.iws 77 | 78 | # IntelliJ 79 | out/ 80 | 81 | # mpeltonen/sbt-idea plugin 82 | .idea_modules/ 83 | 84 | # JIRA plugin 85 | atlassian-ide-plugin.xml 86 | 87 | # Cursive Clojure plugin 88 | .idea/replstate.xml 89 | 90 | # Crashlytics plugin (for Android Studio and IntelliJ) 91 | com_crashlytics_export_strings.xml 92 | crashlytics.properties 93 | crashlytics-build.properties 94 | fabric.properties 95 | 96 | # Editor-based Rest Client 97 | .idea/httpRequests 98 | 99 | # Android studio 3.1+ serialized cache file 100 | .idea/caches/build_file_checksums.ser 101 | 102 | ### JetBrains+all Patch ### 103 | # Ignores the whole .idea folder and all .iml files 104 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 105 | 106 | .idea/ 107 | 108 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 109 | 110 | *.iml 111 | modules.xml 112 | .idea/misc.xml 113 | *.ipr 114 | 115 | # Sonarlint plugin 116 | .idea/sonarlint 117 | 118 | ### Maven ### 119 | target/ 120 | pom.xml.tag 121 | pom.xml.releaseBackup 122 | pom.xml.versionsBackup 123 | pom.xml.next 124 | release.properties 125 | dependency-reduced-pom.xml 126 | buildNumber.properties 127 | .mvn/timing.properties 128 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar 129 | .mvn/wrapper/maven-wrapper.jar 130 | 131 | # End of https://www.toptal.com/developers/gitignore/api/java,maven,jetbrains+all 132 | 133 | -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 | RSocket Load Balancing with Spring Cloud Registry 2 | ================================================= 3 | 4 | RSocket load balance based on Spring Cloud Service Registry. 5 | 6 | ![LoadBalance Structure](./loadbalance-structure.png) 7 | 8 | # How to run? 9 | 10 | * Start Consul first: `docker-compose up -d consul` or `consul agent -dev` ,then open http://localhost:8500 11 | * Build the project: `mvn -DskipTests clean package` 12 | * Start server-app: `java -jar server-app/target/server-app-1.0.0-SNAPSHOT.jar` 13 | * Start client-app: `java -jar client-app/target/client-app-1.0.0-SNAPSHOT.jar` 14 | * Test your RSocket service invocation: `curl http://localhost:9080/square/3` 15 | 16 | # App & Service interface naming specification 17 | Spring Cloud Service Registry uses `spring.application.name` as service name on registry server, and appName is the serviceId argument in `ReactiveDiscoveryClient.getInstances(String serviceId);` 18 | 19 | For example, we have a service app with two service interfaces: MathCalculatorService and ExchangeCalculatorService. 20 | Please use Java package naming style to name your app, such as `com-alibaba-calculator`. 21 | Service interface naming should follow `String serviceName = appName.replace("-", ".") + "." + interfaceName; ` rule, example as following: 22 | 23 | * com.alibaba.calculator.MathCalculatorService 24 | * com.alibaba.calculator.ExchangeCalculatorService 25 | 26 | If you use RSocket Broker to proxy the requests, and you should use `broker:` as prefix for service name, such as `broker:com.alibaba.calculator.ExchangeCalculatorService`, 27 | and `broker` is the RSocket Broker's name, and you can use `ReactiveDiscoveryClient.getInstances(appName)` to query broker instance list. 28 | 29 | Why this naming style? Take a look at the following steps to call remote RSocket services: 30 | 31 | * Extract appName from service full name. For example, appName is `com-alibaba-calculator` from `com.alibaba.calculator.MathCalculatorService` 32 | * Invoke `ReactiveDiscoveryClient.getInstances(appName)` to get app instance list 33 | * Build RSocketRequester with load balance support with `RSocketRequester.Builder.transports(servers)` 34 | * Call RSocketRequester api with service full name as routing key 35 | 36 | ``` 37 | rsocketRequester.route("com.alibaba.calculator.MathCalculatorService.square") 38 | .data(number) 39 | .retrieveMono(Integer.class) 40 | ``` 41 | 42 | This naming style is easy for RSocket to interact with service registry and RSocket service routing. 43 | 44 | If you can not inline the appName in service's full name, and you can use appName as schema style for service invocation, 45 | such as `calculator-server:com.alibaba.calculator.math.MathCalculatorService`. With this way, 46 | and it's easy to migrate other applications into RSocket RPC design. 47 | 48 | # References 49 | 50 | * YMNNALFT: Easy RPC with RSocket: https://spring.io/blog/2021/01/18/ymnnalft-easy-rpc-with-rsocket 51 | * Spring Cloud Consul: https://docs.spring.io/spring-cloud-consul/docs/current/reference/html/ 52 | * Spring Retrosocket: https://github.com/spring-projects-experimental/spring-retrosocket 53 | * RSocket Load Balancing: https://www.vinsguru.com/rsocket-load-balancing-client-side/ 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RSocket Load Balancing with Spring Cloud Registry 2 | ================================================= 3 | 4 | 基于Spring Cloud服务注册发现的RSocket负载均衡,架构如下: 5 | 6 | ![LoadBalance Structure](loadbalance-structure.png) 7 | 8 | # 如何运行? 9 | 10 | * 首先启动Consul: `docker-compose up -d consul` ,然后访问 http://localhost:8500 11 | * 编译整个项目: `mvn -DskipTests clean package` 12 | * 启动server-app: `java -jar server-app/target/server-app-1.0.0-SNAPSHOT.jar` 13 | * 启动client-app: `java -jar client-app/target/client-app-1.0.0-SNAPSHOT.jar` 14 | * 测试服务: `curl http://localhost:9080/square/3` 15 | 16 | # 应用和服务命名规范 17 | Spring Cloud的注册发现机制是基于`spring.application.name`,也就是后续的服务查找就是基于该名称进行的。 18 | 如果你调用`ReactiveDiscoveryClient.getInstances(String serviceId);`查找服务实例列表时,这个serviceId参数其实就是Spring的应用名称。 19 | 考虑到服务注册和后续的RSocket服务路由,所以我们打算设计一个简单的命名规范。 20 | 21 | **注意:** 应用名称不能包含".",这个不是合法的DNS主机名,会被Service Registry转换为"-"。 22 | 23 | 假设你有一个服务应用,功能名称为calculator,同时提供两个服务: 数学计算器服务(MathCalculatorService)和汇率计算服务(ExchangeCalculatorService), 24 | 那么我们该如何如何来命名应用? 这里我们采用Java package命名规范,如 `com-alibaba-calculator`,这样可以确保不会和其他应用重名,另外也方便和Java Package名称进行转换。 25 | 26 | 那么服务接口应该如何命名? 服务接口基于应用名称和interface名称构建,规则为 `String serviceName = appName.replace("-", ".") + "." + interfaceName; ` , 27 | 如下述命名都是合乎规范的: 28 | 29 | * com.alibaba.calculator.MathCalculatorService 30 | * com.alibaba.calculator.ExchangeCalculatorService 31 | 32 | 而 `com.alibaba.calculator.math.MathCalculatorService` 则是错误的 :x:, 因为在应用名称和接口名称之间多了`math`。 33 | 34 | 在RSocket的架构中还有另外一种架构方式,就是Broker架构,如果一个RSocket服务提供者同时端口监听和Broker注册,那么如何通过broker来访问该服务? 35 | 这里我们采用一个`broker:`前缀来判断,如缺省的broker集群的应用名称为`broker`, 当然也可以为`broker1`, `broker2`,通过这种添加前缀的方式,我们可以识别出是否要经过Broker进行通讯, 36 | 样例的名称如 `broker:com.alibaba.calculator.MathCalculatorService`。当然你可以可以通过这种方式指定特定的应用名称,如`server-1:com.alibaba.calculator.MathCalculatorService`。 37 | 38 | 为何要采用这种命名规范? 首先让我们看一下是如何调用远程RSocket服务的: 39 | 40 | * 首先我们根据Service全面提取处对应的应用名称(appName),如 `com.alibaba.calculator.MathCalculatorService` 服务对应的appName则为`com-alibaba-calculator` 41 | * 调用`ReactiveDiscoveryClient.getInstances(appName)` 获取应用名对应的实例列表 42 | * 根据`RSocketRequester.Builder.transports(servers)` 构建具有负载均衡能力的RSocketRequester 43 | * 使用服务全称作为路由进行RSocketRequester的API调用,样例代码如下: 44 | 45 | ``` 46 | rsocketRequester.route("com.alibaba.calculator.MathCalculatorService.square") 47 | .data(number) 48 | .retrieveMono(Integer.class) 49 | ``` 50 | 51 | 通过该种命名方式,我们可以从服务全称中提取中应用名,然后和服务注册中心交互查找对应的实例列表,然后建立和服务提供者的连接,最后基于服务名称进行服务调用。 52 | 53 | 回到最前面说到的规范,如果应用名和服务接口的绑定关系你实在做不到,那么你可以使用这种方式实现服务调用, 54 | 如`calculator-server:com.alibaba.calculator.math.MathCalculatorService`, 55 | 只是你需要更完整的文档说明,当然这种方式也可以解决之前系统接入到目前的架构上,应用的迁移成本也比较小。 56 | 57 | 58 | # References 59 | 60 | * YMNNALFT: Easy RPC with RSocket: https://spring.io/blog/2021/01/18/ymnnalft-easy-rpc-with-rsocket 61 | * Spring Cloud Consul: https://docs.spring.io/spring-cloud-consul/docs/current/reference/html/ 62 | * RSocket Load Balancing: https://www.vinsguru.com/rsocket-load-balancing-client-side/ 63 | -------------------------------------------------------------------------------- /calculator-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | 8 | rsocket-load-balance-parent 9 | com.alibaba.rsocket 10 | 1.0.0-SNAPSHOT 11 | 12 | 13 | calculator-api 14 | 1.0.0-SNAPSHOT 15 | 16 | calculator-api 17 | 18 | 19 | 20 | io.projectreactor 21 | reactor-core 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /calculator-api/src/main/java/com/alibaba/calculator/ExchangeCalculatorService.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.calculator; 2 | 3 | import reactor.core.publisher.Mono; 4 | 5 | public interface ExchangeCalculatorService { 6 | String RSOCKET_SERVICE_NAME = "com.alibaba.calculator.ExchangeCalculatorService"; 7 | 8 | Mono exchange(ExchangeRequest request); 9 | 10 | default Mono dollarToRMB(double amount) { 11 | return exchange(new ExchangeRequest(amount, "USD", "CNY")); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /calculator-api/src/main/java/com/alibaba/calculator/ExchangeRequest.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.calculator; 2 | 3 | public class ExchangeRequest { 4 | private double amount; 5 | private String source; 6 | private String target; 7 | 8 | public ExchangeRequest() { 9 | } 10 | 11 | public ExchangeRequest(double amount, String source, String target) { 12 | this.amount = amount; 13 | this.source = source; 14 | this.target = target; 15 | } 16 | 17 | public double getAmount() { 18 | return amount; 19 | } 20 | 21 | public void setAmount(double amount) { 22 | this.amount = amount; 23 | } 24 | 25 | public String getSource() { 26 | return source; 27 | } 28 | 29 | public void setSource(String source) { 30 | this.source = source; 31 | } 32 | 33 | public String getTarget() { 34 | return target; 35 | } 36 | 37 | public void setTarget(String target) { 38 | this.target = target; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /calculator-api/src/main/java/com/alibaba/calculator/MathCalculatorService.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.calculator; 2 | 3 | import reactor.core.publisher.Mono; 4 | 5 | public interface MathCalculatorService { 6 | String RSOCKET_SERVICE_NAME = "com.alibaba.calculator.MathCalculatorService"; 7 | 8 | Mono square(Integer input); 9 | } 10 | -------------------------------------------------------------------------------- /client-app/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | rsocket-load-balance-parent 9 | com.alibaba.rsocket 10 | 1.0.0-SNAPSHOT 11 | 12 | 13 | client-app 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter-webflux 19 | 20 | 21 | org.springframework.cloud 22 | spring-cloud-starter-consul-discovery 23 | 24 | 25 | com.alibaba.rsocket 26 | calculator-api 27 | ${project.version} 28 | 29 | 30 | com.alibaba.rsocket 31 | rsocket-loadbalance-spring-boot-starter 32 | 1.0.0-SNAPSHOT 33 | 34 | 35 | 36 | 37 | 38 | 39 | org.springframework.boot 40 | spring-boot-maven-plugin 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /client-app/src/main/java/com/alibaba/rsocket/client/PortalController.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.rsocket.client; 2 | 3 | import com.alibaba.calculator.ExchangeCalculatorService; 4 | import com.alibaba.calculator.MathCalculatorService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.messaging.rsocket.RSocketRequester; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.PathVariable; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import reactor.core.publisher.Mono; 11 | 12 | @RestController 13 | public class PortalController { 14 | @Autowired 15 | private RSocketRequester mathCalculatorRequester; 16 | @Autowired 17 | private MathCalculatorService mathCalculatorService; 18 | @Autowired 19 | private ExchangeCalculatorService exchangeCalculatorService; 20 | 21 | @GetMapping("/square/{number}") 22 | public Mono square(@PathVariable("number") int number) { 23 | return mathCalculatorService.square(number) 24 | .map(result -> number + "*" + number + "=" + result); 25 | } 26 | 27 | @GetMapping("/square2/{number}") 28 | public Mono square2(@PathVariable("number") int number) { 29 | return mathCalculatorRequester.route("com.example.calculator.MathCalculatorService.square") 30 | .data(number) 31 | .retrieveMono(Integer.class) 32 | .map(result -> number + "*" + number + "=" + result); 33 | } 34 | 35 | @GetMapping("/exchange/{number}") 36 | public Mono exchangeUSD2CNY(@PathVariable("number") int number) { 37 | return exchangeCalculatorService.dollarToRMB(number) 38 | .map(result -> number + "$ = " + result + "RMB"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /client-app/src/main/java/com/alibaba/rsocket/client/RSocketClientApplication.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.rsocket.client; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | 7 | @SpringBootApplication 8 | @EnableDiscoveryClient 9 | public class RSocketClientApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(RSocketClientApplication.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /client-app/src/main/java/com/alibaba/rsocket/client/RSocketConfig.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.rsocket.client; 2 | 3 | import com.alibaba.calculator.ExchangeCalculatorService; 4 | import com.alibaba.calculator.MathCalculatorService; 5 | import com.alibaba.rsocket.loadbalance.RSocketServiceRegistry; 6 | import com.alibaba.rsocket.loadbalance.proxy.RSocketRemoteServiceBuilder; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.http.codec.cbor.Jackson2CborDecoder; 10 | import org.springframework.http.codec.cbor.Jackson2CborEncoder; 11 | import org.springframework.messaging.rsocket.RSocketRequester; 12 | import org.springframework.messaging.rsocket.RSocketStrategies; 13 | 14 | @Configuration 15 | public class RSocketConfig { 16 | 17 | @Bean 18 | public RSocketStrategies rSocketStrategies() { 19 | return RSocketStrategies.builder() 20 | .encoders(encoders -> encoders.add(new Jackson2CborEncoder())) 21 | .decoders(decoders -> decoders.add(new Jackson2CborDecoder())) 22 | .build(); 23 | } 24 | 25 | @Bean 26 | public RSocketRequester mathCalculatorRequester(RSocketRequester.Builder builder, 27 | RSocketServiceRegistry rsocketServiceRegistry) { 28 | return rsocketServiceRegistry.buildLoadBalanceRSocket(MathCalculatorService.RSOCKET_SERVICE_NAME, builder); 29 | } 30 | 31 | @Bean 32 | public RSocketRequester exchangeCalculatorRequester(RSocketRequester.Builder builder, 33 | RSocketServiceRegistry rsocketServiceRegistry) { 34 | return rsocketServiceRegistry.buildLoadBalanceRSocket(ExchangeCalculatorService.RSOCKET_SERVICE_NAME, builder); 35 | } 36 | 37 | @Bean 38 | public MathCalculatorService mathCalculatorService(RSocketRequester mathCalculatorRequester) { 39 | RSocketRemoteServiceBuilder builder = new RSocketRemoteServiceBuilder<>(); 40 | return builder.serviceName(MathCalculatorService.RSOCKET_SERVICE_NAME) 41 | .serviceInterface(MathCalculatorService.class) 42 | .rsocketRequester(mathCalculatorRequester) 43 | .build(); 44 | } 45 | 46 | @Bean 47 | public ExchangeCalculatorService exchangeCalculatorService(RSocketRequester exchangeCalculatorRequester) { 48 | RSocketRemoteServiceBuilder builder = new RSocketRemoteServiceBuilder<>(); 49 | return builder.serviceName(ExchangeCalculatorService.RSOCKET_SERVICE_NAME) 50 | .serviceInterface(ExchangeCalculatorService.class) 51 | .rsocketRequester(exchangeCalculatorRequester) 52 | .build(); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /client-app/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=client-app 2 | server.port=9080 3 | ### management 4 | management.endpoints.web.exposure.include=* 5 | management.endpoint.health.show-components=always 6 | management.endpoint.health.show-details=always -------------------------------------------------------------------------------- /client-app/src/main/resources/bootstrap.properties: -------------------------------------------------------------------------------- 1 | spring.cloud.consul.host=localhost 2 | spring.cloud.consul.port=8500 -------------------------------------------------------------------------------- /client-app/src/test/java/com/alibaba/rsocket/registry/ConsulDiscoveryClientTest.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.rsocket.registry; 2 | 3 | import com.ecwid.consul.v1.ConsulClient; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.BeforeAll; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.cloud.client.ServiceInstance; 8 | import org.springframework.cloud.commons.util.InetUtils; 9 | import org.springframework.cloud.commons.util.InetUtilsProperties; 10 | import org.springframework.cloud.consul.discovery.ConsulDiscoveryProperties; 11 | import org.springframework.cloud.consul.discovery.reactive.ConsulReactiveDiscoveryClient; 12 | 13 | import java.util.List; 14 | 15 | public class ConsulDiscoveryClientTest { 16 | 17 | private static ConsulReactiveDiscoveryClient discoveryClient; 18 | 19 | @BeforeAll 20 | public static void beforeAll() { 21 | ConsulClient consulClient = new ConsulClient("127.0.0.1", 8500); 22 | ConsulDiscoveryProperties properties = new ConsulDiscoveryProperties(new InetUtils(new InetUtilsProperties())); 23 | discoveryClient = new ConsulReactiveDiscoveryClient(consulClient, properties); 24 | } 25 | 26 | @Test 27 | public void testFindServiceServers() { 28 | List serviceInstances = discoveryClient.getInstances("com-alibaba-calculator").collectList().block(); 29 | Assertions.assertNotNull(serviceInstances); 30 | for (ServiceInstance serviceInstance : serviceInstances) { 31 | System.out.println("Host: " + serviceInstance.getHost()); 32 | System.out.println("RSocket Port:" + serviceInstance.getMetadata().get("rsocketPort")); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | consul: 5 | image: consul:1.11.3 6 | ports: 7 | - "8500:8500" 8 | - "8300:8300" 9 | - "8301:8301" 10 | - "8302:8302" 11 | - "8400:8400" 12 | - "8600:53/udp" 13 | nacos: 14 | image: nacos/nacos-server:1.4.2 15 | container_name: nacos-standalone 16 | environment: 17 | - PREFER_HOST_MODE=hostname 18 | - MODE=standalone 19 | ports: 20 | - "8848:8848" -------------------------------------------------------------------------------- /index.http: -------------------------------------------------------------------------------- 1 | ### square number 2 | GET http://localhost:9080/square/3 3 | 4 | ### exchange USD to CNY 5 | GET http://localhost:9080/exchange/1 6 | 7 | ### get rsocketlb endpoint 8 | GET http://localhost:9080/actuator/rsocketlb -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | build: 2 | mvn -DskipTests clean package 3 | 4 | deploy-dry-run: 5 | mvn -pl rsocket-loadbalance-spring-boot-starter -P release -DskipTests clean package 6 | 7 | deploy: 8 | mvn -pl rsocket-loadbalance-spring-boot-starter -P release -DskipTests clean package deploy 9 | 10 | start-servers: build 11 | java -jar server-app/target/server-app-1.0.0-SNAPSHOT.jar --spring.rsocket.server.port=6565 & 12 | java -jar server-app/target/server-app-1.0.0-SNAPSHOT.jar --spring.rsocket.server.port=6566 & 13 | java -jar server-app/target/server-app-1.0.0-SNAPSHOT.jar --spring.rsocket.server.port=6567 & 14 | 15 | stop-servers: 16 | kill -9 $(lsof -t -i:6565) & 17 | kill -9 $(lsof -t -i:6566) & 18 | kill -9 $(lsof -t -i:6567) & 19 | 20 | consul-server: 21 | consul agent -dev -------------------------------------------------------------------------------- /loadbalance-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reactive-rsocket-broker/rsocket-load-balance/28c091c30ef85c992514027fc0620863f8d9f4d6/loadbalance-structure.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.alibaba.rsocket 7 | rsocket-load-balance-parent 8 | 1.0.0-SNAPSHOT 9 | pom 10 | 11 | 12 | rsocket-loadbalance-spring-boot-starter 13 | calculator-api 14 | server-app 15 | client-app 16 | 17 | 18 | 19 | 1.8 20 | ${java.version} 21 | ${java.version} 22 | 2.7.1 23 | 2021.0.3 24 | 25 | 26 | rsocket-load-balancing 27 | RSocket load balance with Spring Cloud Registry 28 | https://github.com/alibaba-rsocket-broker/rsocket-load-balance 29 | 2022 30 | 31 | 32 | 33 | Apache License 34 | https://www.apache.org/licenses/LICENSE-2.0.txt 35 | repo 36 | 37 | 38 | 39 | 40 | leijuan 41 | 雷卷 42 | 43 | Developer 44 | 45 | Alibaba 46 | 47 | 48 | 49 | 50 | scm:git:git@github.com:alibaba-rsocket-broker/rsocket-load-balance.git 51 | scm:git:git@github.com:alibaba-rsocket-broker/rsocket-load-balance.git 52 | https://github.com/alibaba-rsocket-broker/rsocket-load-balance 53 | 54 | 55 | 56 | Github 57 | https://github.com/alibaba-rsocket-broker/rsocket-load-balance/issues 58 | 59 | 60 | 61 | Github actions 62 | https://github.com/alibaba-rsocket-broker/rsocket-load-balance/actions 63 | 64 | 65 | 66 | 67 | org.junit.jupiter 68 | junit-jupiter 69 | test 70 | 71 | 72 | org.assertj 73 | assertj-core 74 | 3.23.1 75 | test 76 | 77 | 78 | 79 | 80 | 81 | 82 | org.junit 83 | junit-bom 84 | 5.8.2 85 | import 86 | pom 87 | 88 | 89 | org.springframework.boot 90 | spring-boot-dependencies 91 | ${spring-boot.version} 92 | import 93 | pom 94 | 95 | 96 | org.springframework.cloud 97 | spring-cloud-dependencies 98 | ${spring-cloud.version} 99 | pom 100 | import 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | org.apache.maven.plugins 109 | maven-compiler-plugin 110 | 3.10.1 111 | 112 | true 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | org.springframework.boot 121 | spring-boot-maven-plugin 122 | ${spring-boot.version} 123 | 124 | 125 | 126 | repackage 127 | build-info 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /rsocket-loadbalance-spring-boot-starter/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.alibaba.rsocket 7 | rsocket-loadbalance-spring-boot-starter 8 | 1.0.0-SNAPSHOT 9 | 10 | 11 | 1.8 12 | 2.7.1 13 | 2021.0.3 14 | 15 | 16 | rsocket-loadbalance-spring-boot-starter 17 | Spring Boot starter for RSocket load balance with Spring Cloud registry support 18 | https://github.com/alibaba-rsocket-broker/rsocket-load-balance 19 | 2022 20 | 21 | 22 | 23 | Apache License 24 | https://www.apache.org/licenses/LICENSE-2.0.txt 25 | repo 26 | 27 | 28 | 29 | 30 | 31 | leijuan 32 | 雷卷 33 | 34 | Developer 35 | 36 | Alibaba 37 | 38 | 39 | 40 | 41 | scm:git:git@github.com:alibaba-rsocket-broker/rsocket-load-balance.git 42 | scm:git:git@github.com:alibaba-rsocket-broker/rsocket-load-balance.git 43 | https://github.com/alibaba-rsocket-broker/rsocket-load-balance 44 | 45 | 46 | 47 | Github 48 | https://github.com/alibaba-rsocket-broker/rsocket-load-balance/issues 49 | 50 | 51 | 52 | Github actions 53 | https://github.com/alibaba-rsocket-broker/rsocket-load-balance/actions 54 | 55 | 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-starter-rsocket 60 | 61 | 62 | org.springframework.cloud 63 | spring-cloud-commons 64 | 65 | 66 | org.springframework.boot 67 | spring-boot-starter-actuator 68 | 69 | 70 | 71 | 72 | 73 | 74 | org.springframework.boot 75 | spring-boot-dependencies 76 | ${spring-boot.version} 77 | import 78 | pom 79 | 80 | 81 | org.springframework.cloud 82 | spring-cloud-dependencies 83 | ${spring-cloud.version} 84 | pom 85 | import 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | org.apache.maven.plugins 94 | maven-compiler-plugin 95 | 3.10.1 96 | 97 | 1.8 98 | 1.8 99 | true 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | release 108 | 109 | 110 | 111 | org.sonatype.plugins 112 | nexus-staging-maven-plugin 113 | 1.6.8 114 | true 115 | 116 | ossrh 117 | https://oss.sonatype.org/ 118 | true 119 | 120 | 121 | 122 | org.apache.maven.plugins 123 | maven-source-plugin 124 | 3.2.1 125 | 126 | 127 | attach-sources 128 | 129 | jar-no-fork 130 | 131 | 132 | 133 | 134 | 135 | org.apache.maven.plugins 136 | maven-javadoc-plugin 137 | 3.1.1 138 | 139 | 140 | attach-javadocs 141 | 142 | jar 143 | 144 | 145 | 146 | 147 | 148 | org.apache.maven.plugins 149 | maven-gpg-plugin 150 | 1.6 151 | 152 | 153 | sign-artifacts 154 | verify 155 | 156 | sign 157 | 158 | 159 | 160 | --pinentry-mode 161 | loopback 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /rsocket-loadbalance-spring-boot-starter/src/main/java/com/alibaba/rsocket/loadbalance/RSocketLoadBalanceConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.rsocket.loadbalance; 2 | 3 | import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.scheduling.annotation.EnableScheduling; 7 | 8 | @Configuration 9 | @EnableScheduling 10 | public class RSocketLoadBalanceConfiguration { 11 | 12 | @Bean 13 | public RSocketServiceDiscoveryRegistry rsocketServiceDiscoveryRegistry(ReactiveDiscoveryClient discoveryClient) { 14 | return new RSocketServiceDiscoveryRegistry(discoveryClient); 15 | } 16 | 17 | @Bean 18 | public RSocketLoadBalanceEndpoint rsocketLoadBalanceEndpoint(RSocketServiceRegistry rsocketServiceRegistry) { 19 | return new RSocketLoadBalanceEndpoint(rsocketServiceRegistry); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /rsocket-loadbalance-spring-boot-starter/src/main/java/com/alibaba/rsocket/loadbalance/RSocketLoadBalanceEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.rsocket.loadbalance; 2 | 3 | import org.springframework.boot.actuate.endpoint.annotation.Endpoint; 4 | import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; 5 | 6 | import java.util.HashMap; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.stream.Collectors; 10 | 11 | @Endpoint(id = "rsocketlb") 12 | public class RSocketLoadBalanceEndpoint { 13 | private final RSocketServiceRegistry rsocketServiceRegistry; 14 | 15 | public RSocketLoadBalanceEndpoint(RSocketServiceRegistry rsocketServiceRegistry) { 16 | this.rsocketServiceRegistry = rsocketServiceRegistry; 17 | } 18 | 19 | @ReadOperation 20 | public Map info() { 21 | Map info = new HashMap<>(); 22 | Map> services = new HashMap<>(); 23 | for (Map.Entry> entry : rsocketServiceRegistry.getSnapshots().entrySet()) { 24 | services.put(entry.getKey(), entry.getValue().stream().map(RSocketServerInstance::getURI).collect(Collectors.toList())); 25 | } 26 | info.put("services", services); 27 | info.put("lastRefreshAt", rsocketServiceRegistry.getLastRefreshTimestamp()); 28 | return info; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /rsocket-loadbalance-spring-boot-starter/src/main/java/com/alibaba/rsocket/loadbalance/RSocketServerInstance.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.rsocket.loadbalance; 2 | 3 | import io.rsocket.transport.ClientTransport; 4 | import io.rsocket.transport.netty.client.TcpClientTransport; 5 | import io.rsocket.transport.netty.client.WebsocketClientTransport; 6 | 7 | import java.net.URI; 8 | 9 | public class RSocketServerInstance { 10 | private String host; 11 | private int port; 12 | /** 13 | * schema, such as tcp, ws, wss 14 | */ 15 | private String schema = "tcp"; 16 | /** 17 | * path, for websocket only 18 | */ 19 | private String path; 20 | 21 | public RSocketServerInstance() { 22 | } 23 | 24 | public RSocketServerInstance(String host, int port) { 25 | this.host = host; 26 | this.port = port; 27 | } 28 | 29 | public String getHost() { 30 | return host; 31 | } 32 | 33 | public void setHost(String host) { 34 | this.host = host; 35 | } 36 | 37 | public int getPort() { 38 | return port; 39 | } 40 | 41 | public void setPort(int port) { 42 | this.port = port; 43 | } 44 | 45 | public String getSchema() { 46 | return schema; 47 | } 48 | 49 | public void setSchema(String schema) { 50 | this.schema = schema; 51 | } 52 | 53 | public String getPath() { 54 | return path; 55 | } 56 | 57 | public void setPath(String path) { 58 | this.path = path; 59 | } 60 | 61 | public boolean isWebSocket() { 62 | return "ws".equals(this.schema) || "wss".equals(this.schema); 63 | } 64 | 65 | public String getURI() { 66 | if(isWebSocket()) { 67 | return schema + "://" + host + ":" + port + path; 68 | } else { 69 | return schema +"://"+host+":"+port; 70 | } 71 | } 72 | 73 | public ClientTransport constructClientTransport() { 74 | if (this.isWebSocket()) { 75 | return WebsocketClientTransport.create(URI.create(getURI())); 76 | } 77 | return TcpClientTransport.create(host, port); 78 | } 79 | 80 | @Override 81 | public boolean equals(Object o) { 82 | if (this == o) return true; 83 | if (o == null || getClass() != o.getClass()) return false; 84 | RSocketServerInstance that = (RSocketServerInstance) o; 85 | if (port != that.port) return false; 86 | return host.equals(that.host); 87 | } 88 | 89 | @Override 90 | public int hashCode() { 91 | int result = host.hashCode(); 92 | result = 31 * result + port; 93 | return result; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /rsocket-loadbalance-spring-boot-starter/src/main/java/com/alibaba/rsocket/loadbalance/RSocketServiceDiscoveryRegistry.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.rsocket.loadbalance; 2 | 3 | import io.rsocket.loadbalance.LoadbalanceTarget; 4 | import io.rsocket.loadbalance.RoundRobinLoadbalanceStrategy; 5 | import org.springframework.cloud.client.ServiceInstance; 6 | import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; 7 | import org.springframework.messaging.rsocket.RSocketRequester; 8 | import org.springframework.scheduling.annotation.Scheduled; 9 | import reactor.core.publisher.Flux; 10 | import reactor.core.publisher.Sinks; 11 | 12 | import java.util.Date; 13 | import java.util.HashMap; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.concurrent.ConcurrentHashMap; 17 | import java.util.stream.Collectors; 18 | 19 | public class RSocketServiceDiscoveryRegistry implements RSocketServiceRegistry { 20 | /** 21 | * appName and server instance list mapping 22 | */ 23 | private final Map>> service2Servers = new ConcurrentHashMap<>(); 24 | 25 | private final Map> snapshots = new HashMap<>(); 26 | private final ReactiveDiscoveryClient discoveryClient; 27 | private Date lastRefreshTimeStamp = new Date(); 28 | private boolean refreshing = false; 29 | 30 | public RSocketServiceDiscoveryRegistry(ReactiveDiscoveryClient discoveryClient) { 31 | this.discoveryClient = discoveryClient; 32 | } 33 | 34 | @Override 35 | public Map> getSnapshots() { 36 | return this.snapshots; 37 | } 38 | 39 | @Override 40 | public Date getLastRefreshTimestamp() { 41 | return this.lastRefreshTimeStamp; 42 | } 43 | 44 | @Scheduled(initialDelay = 5000, fixedRate = 15000) 45 | public void refreshServers() { 46 | if (!refreshing) { 47 | refreshing = true; 48 | lastRefreshTimeStamp = new Date(); 49 | try { 50 | if (!snapshots.isEmpty()) { 51 | for (String serviceName : service2Servers.keySet()) { 52 | discoveryClient.getInstances(serviceName) 53 | .map(this::convertToRSocketServerInstance) 54 | .collectList().subscribe(newServiceInstances -> { 55 | List currentServerInstances = snapshots.get(serviceName); 56 | //not same 57 | if (!(currentServerInstances.size() == newServiceInstances.size() && currentServerInstances.containsAll(newServiceInstances))) { 58 | System.out.println("Begin to refresh upstream RSocket servers"); 59 | setServers(serviceName, newServiceInstances); 60 | } 61 | }); 62 | } 63 | } 64 | } finally { 65 | refreshing = false; 66 | } 67 | } 68 | } 69 | 70 | public void setServers(String serviceName, List servers) { 71 | String appName = convertToAppName(serviceName); 72 | if (service2Servers.containsKey(appName)) { 73 | this.service2Servers.get(appName).tryEmitNext(servers); 74 | this.snapshots.put(appName, servers); 75 | } 76 | } 77 | 78 | @Override 79 | public RSocketRequester buildLoadBalanceRSocket(String serviceName, RSocketRequester.Builder builder) { 80 | return builder.transports(this.getServers(serviceName), new RoundRobinLoadbalanceStrategy()); 81 | } 82 | 83 | public Flux> getServers(String serviceName) { 84 | final String appName = convertToAppName(serviceName); 85 | if (!service2Servers.containsKey(appName)) { 86 | service2Servers.put(appName, Sinks.many().replay().latest()); 87 | return Flux.from(discoveryClient.getInstances(appName) 88 | .map(this::convertToRSocketServerInstance) 89 | .collectList() 90 | .doOnNext(rSocketServerInstances -> { 91 | snapshots.put(appName, rSocketServerInstances); 92 | service2Servers.get(appName).tryEmitNext(rSocketServerInstances); 93 | })) 94 | .thenMany(service2Servers.get(appName).asFlux().map(this::toLoadBalanceTarget)); 95 | } 96 | return service2Servers.get(appName) 97 | .asFlux() 98 | .map(this::toLoadBalanceTarget); 99 | } 100 | 101 | private List toLoadBalanceTarget(List rSocketServers) { 102 | return rSocketServers.stream() 103 | .map(server -> LoadbalanceTarget.from(server.getHost() + server.getPort(), server.constructClientTransport())) 104 | .collect(Collectors.toList()); 105 | } 106 | 107 | private String convertToAppName(String serviceName) { 108 | String appName = serviceName.replaceAll("\\.", "-"); 109 | // service with assigned name 110 | if (serviceName.contains(":")) { 111 | return appName.substring(0, appName.indexOf(":")); 112 | } 113 | if (appName.contains("-")) { 114 | String temp = appName.substring(appName.lastIndexOf("-") + 1); 115 | // if first character is uppercase, and it means service name 116 | if (Character.isUpperCase(temp.toCharArray()[0])) { 117 | appName = appName.substring(0, appName.lastIndexOf("-")); 118 | } 119 | } 120 | return appName; 121 | } 122 | 123 | private RSocketServerInstance convertToRSocketServerInstance(ServiceInstance serviceInstance) { 124 | RSocketServerInstance serverInstance = new RSocketServerInstance(); 125 | serverInstance.setHost(serviceInstance.getHost()); 126 | serverInstance.setSchema(serviceInstance.getMetadata().getOrDefault("rsocketSchema", "tcp")); 127 | if (serverInstance.isWebSocket()) { 128 | serverInstance.setPort(serviceInstance.getPort()); 129 | serverInstance.setPath(serviceInstance.getMetadata().getOrDefault("rsocketPath", "/rsocket")); 130 | } else { 131 | serverInstance.setPort(Integer.parseInt(serviceInstance.getMetadata().getOrDefault("rsocketPort", "42252"))); 132 | } 133 | return serverInstance; 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /rsocket-loadbalance-spring-boot-starter/src/main/java/com/alibaba/rsocket/loadbalance/RSocketServiceRegistry.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.rsocket.loadbalance; 2 | 3 | import io.rsocket.loadbalance.LoadbalanceTarget; 4 | import org.springframework.messaging.rsocket.RSocketRequester; 5 | import reactor.core.publisher.Flux; 6 | 7 | import java.util.Date; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | public interface RSocketServiceRegistry { 12 | 13 | Flux> getServers(String serviceName); 14 | 15 | RSocketRequester buildLoadBalanceRSocket(String serviceName, RSocketRequester.Builder builder); 16 | 17 | Map> getSnapshots(); 18 | 19 | Date getLastRefreshTimestamp(); 20 | } 21 | -------------------------------------------------------------------------------- /rsocket-loadbalance-spring-boot-starter/src/main/java/com/alibaba/rsocket/loadbalance/proxy/DefaultMethodHandler.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.rsocket.loadbalance.proxy; 2 | 3 | import java.lang.invoke.MethodHandle; 4 | import java.lang.invoke.MethodHandles; 5 | import java.lang.invoke.MethodType; 6 | import java.lang.reflect.Constructor; 7 | import java.lang.reflect.Method; 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | /** 12 | * Default Method Handler 13 | * 14 | * 15 | * if (method.isDefault()) { 16 | * return DefaultMethodHandler.getMethodHandle(method, serviceInterface).bindTo(proxy).invokeWithArguments(args); 17 | * } 18 | * 19 | * 20 | * @author linux_china 21 | */ 22 | public class DefaultMethodHandler { 23 | /** 24 | * default method handles 25 | */ 26 | private static final Map methodHandles = new HashMap<>(); 27 | 28 | public static MethodHandle getMethodHandle(Method method, Class serviceInterface) throws Exception { 29 | MethodHandle methodHandle = methodHandles.get(method); 30 | if (methodHandle == null) { 31 | String version = System.getProperty("java.version"); 32 | if (version.startsWith("1.8.")) { 33 | Constructor lookupConstructor = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, Integer.TYPE); 34 | if (!lookupConstructor.isAccessible()) { 35 | lookupConstructor.setAccessible(true); 36 | } 37 | methodHandle = lookupConstructor.newInstance(method.getDeclaringClass(), MethodHandles.Lookup.PRIVATE) 38 | .unreflectSpecial(method, method.getDeclaringClass()); 39 | } else { 40 | methodHandle = MethodHandles.lookup().findSpecial( 41 | method.getDeclaringClass(), 42 | method.getName(), 43 | MethodType.methodType(method.getReturnType(), method.getParameterTypes()), 44 | serviceInterface); 45 | } 46 | methodHandles.put(method, methodHandle); 47 | } 48 | return methodHandle; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /rsocket-loadbalance-spring-boot-starter/src/main/java/com/alibaba/rsocket/loadbalance/proxy/RSocketRemoteCallInvocationHandler.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.rsocket.loadbalance.proxy; 2 | 3 | import org.springframework.messaging.rsocket.RSocketRequester; 4 | import reactor.core.publisher.Flux; 5 | 6 | import java.lang.reflect.InvocationHandler; 7 | import java.lang.reflect.Method; 8 | import java.lang.reflect.ParameterizedType; 9 | import java.lang.reflect.Type; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | public class RSocketRemoteCallInvocationHandler implements InvocationHandler { 14 | private final RSocketRequester rsocketRequester; 15 | private final Class serviceInterface; 16 | private final String serviceName; 17 | private final static Map> methodReturnTypeMap = new HashMap<>(); 18 | 19 | public RSocketRemoteCallInvocationHandler(RSocketRequester rsocketRequester, String serviceName, Class serviceInterface) { 20 | this.rsocketRequester = rsocketRequester; 21 | this.serviceName = serviceName; 22 | this.serviceInterface = serviceInterface; 23 | } 24 | 25 | @Override 26 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 27 | if (method.isDefault()) { 28 | return DefaultMethodHandler.getMethodHandle(method, serviceInterface).bindTo(proxy).invokeWithArguments(args); 29 | } 30 | String methodName = method.getName(); 31 | Object arg = null; 32 | if (args != null && args.length > 0) { 33 | arg = args[0]; 34 | } 35 | Class returnType = methodReturnTypeMap.get(method); 36 | if (returnType == null) { 37 | returnType = parseInferredClass(method.getGenericReturnType()); 38 | methodReturnTypeMap.put(method, returnType); 39 | } 40 | RSocketRequester.RequestSpec requestSpec = rsocketRequester.route(serviceName + "." + methodName); 41 | RSocketRequester.RetrieveSpec retrieveSpec; 42 | if (arg != null) { 43 | retrieveSpec = requestSpec.data(arg); 44 | } else { 45 | retrieveSpec = requestSpec; 46 | } 47 | // Flux return type: request/stream or channel 48 | if (method.getReturnType().isAssignableFrom(Flux.class)) { 49 | return retrieveSpec.retrieveFlux(returnType); 50 | } else { //Mono return type 51 | // Void return type: fireAndForget 52 | if (returnType.equals(Void.class)) { 53 | return retrieveSpec.send(); 54 | } else { // request/response 55 | return requestSpec.retrieveMono(returnType); 56 | } 57 | } 58 | } 59 | 60 | 61 | public static Class parseInferredClass(Type genericType) { 62 | Class inferredClass = null; 63 | if (genericType instanceof ParameterizedType) { 64 | ParameterizedType type = (ParameterizedType) genericType; 65 | Type[] typeArguments = type.getActualTypeArguments(); 66 | if (typeArguments.length > 0) { 67 | final Type typeArgument = typeArguments[0]; 68 | if (typeArgument instanceof ParameterizedType) { 69 | inferredClass = (Class) ((ParameterizedType) typeArgument).getActualTypeArguments()[0]; 70 | } else if (typeArgument instanceof Class) { 71 | inferredClass = (Class) typeArgument; 72 | } else { 73 | String typeName = typeArgument.getTypeName(); 74 | if (typeName.contains(" ")) { 75 | typeName = typeName.substring(typeName.lastIndexOf(" ") + 1); 76 | } 77 | if (typeName.contains("<")) { 78 | typeName = typeName.substring(0, typeName.indexOf("<")); 79 | } 80 | try { 81 | inferredClass = Class.forName(typeName); 82 | } catch (Exception e) { 83 | e.printStackTrace(); 84 | } 85 | } 86 | } 87 | } 88 | if (inferredClass == null && genericType instanceof Class) { 89 | inferredClass = (Class) genericType; 90 | } 91 | return inferredClass; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /rsocket-loadbalance-spring-boot-starter/src/main/java/com/alibaba/rsocket/loadbalance/proxy/RSocketRemoteServiceBuilder.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.rsocket.loadbalance.proxy; 2 | 3 | import org.springframework.messaging.rsocket.RSocketRequester; 4 | 5 | import java.lang.reflect.Proxy; 6 | 7 | public class RSocketRemoteServiceBuilder { 8 | private String serviceName; 9 | private Class serviceInterface; 10 | private RSocketRequester rsocketRequester; 11 | 12 | public RSocketRemoteServiceBuilder serviceInterface(Class serviceInterface) { 13 | this.serviceInterface = serviceInterface; 14 | return this; 15 | } 16 | 17 | public RSocketRemoteServiceBuilder serviceName(String serviceName) { 18 | this.serviceName = serviceName; 19 | return this; 20 | } 21 | 22 | public RSocketRemoteServiceBuilder rsocketRequester(RSocketRequester rsocketRequester) { 23 | this.rsocketRequester = rsocketRequester; 24 | return this; 25 | } 26 | 27 | @SuppressWarnings("unchecked") 28 | public T build() { 29 | RSocketRemoteCallInvocationHandler handler = new RSocketRemoteCallInvocationHandler(rsocketRequester, serviceName, serviceInterface); 30 | return (T) Proxy.newProxyInstance( 31 | serviceInterface.getClassLoader(), 32 | new Class[]{serviceInterface}, 33 | handler); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /rsocket-loadbalance-spring-boot-starter/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.alibaba.rsocket.loadbalance.RSocketLoadBalanceConfiguration -------------------------------------------------------------------------------- /server-app/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | 8 | rsocket-load-balance-parent 9 | com.alibaba.rsocket 10 | 1.0.0-SNAPSHOT 11 | 12 | 13 | server-app 14 | 15 | 16 | 17 | com.alibaba.rsocket 18 | calculator-api 19 | ${project.version} 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-webflux 24 | 25 | 26 | org.springframework.boot 27 | spring-boot-starter-rsocket 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-actuator 32 | 33 | 34 | org.springframework.cloud 35 | spring-cloud-starter-consul-discovery 36 | 37 | 38 | 39 | 40 | 41 | 42 | org.springframework.boot 43 | spring-boot-maven-plugin 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /server-app/src/main/java/com/alibaba/calculator/RSocketServerApplication.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.calculator; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | 7 | @SpringBootApplication 8 | @EnableDiscoveryClient 9 | public class RSocketServerApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(RSocketServerApplication.class, args); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /server-app/src/main/java/com/alibaba/calculator/annotations/RSocketHandler.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.calculator.annotations; 2 | 3 | import org.springframework.core.annotation.AliasFor; 4 | import org.springframework.messaging.handler.annotation.MessageMapping; 5 | 6 | import java.lang.annotation.*; 7 | 8 | @Target(ElementType.METHOD) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | @Documented 11 | @MessageMapping 12 | public @interface RSocketHandler { 13 | @AliasFor(annotation = MessageMapping.class) 14 | String[] value() default {}; 15 | } 16 | -------------------------------------------------------------------------------- /server-app/src/main/java/com/alibaba/calculator/annotations/SpringRSocketService.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.calculator.annotations; 2 | 3 | import org.springframework.core.annotation.AliasFor; 4 | import org.springframework.messaging.handler.annotation.MessageMapping; 5 | import org.springframework.stereotype.Controller; 6 | 7 | import java.lang.annotation.*; 8 | 9 | @Target(ElementType.TYPE) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Documented 12 | @Controller 13 | @MessageMapping 14 | public @interface SpringRSocketService { 15 | @AliasFor(annotation = MessageMapping.class) 16 | String[] value() default {}; 17 | } 18 | -------------------------------------------------------------------------------- /server-app/src/main/java/com/alibaba/calculator/impl/ExchangeCalculatorImpl.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.calculator.impl; 2 | 3 | import com.alibaba.calculator.ExchangeCalculatorService; 4 | import com.alibaba.calculator.ExchangeRequest; 5 | import com.alibaba.calculator.annotations.RSocketHandler; 6 | import com.alibaba.calculator.annotations.SpringRSocketService; 7 | import reactor.core.publisher.Mono; 8 | 9 | @SpringRSocketService(ExchangeCalculatorService.RSOCKET_SERVICE_NAME) 10 | public class ExchangeCalculatorImpl implements ExchangeCalculatorService { 11 | @Override 12 | @RSocketHandler("exchange") 13 | public Mono exchange(ExchangeRequest request) { 14 | if (request.getSource().equals("USD") && request.getTarget().equals("CNY")) { 15 | return Mono.just(request.getAmount() * 6.4); 16 | } 17 | return Mono.just(0.0); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server-app/src/main/java/com/alibaba/calculator/impl/MathCalculatorImpl.java: -------------------------------------------------------------------------------- 1 | package com.alibaba.calculator.impl; 2 | 3 | import com.alibaba.calculator.MathCalculatorService; 4 | import com.alibaba.calculator.annotations.RSocketHandler; 5 | import com.alibaba.calculator.annotations.SpringRSocketService; 6 | import reactor.core.publisher.Mono; 7 | 8 | @SpringRSocketService(MathCalculatorService.RSOCKET_SERVICE_NAME) 9 | public class MathCalculatorImpl implements MathCalculatorService { 10 | 11 | @RSocketHandler("square") 12 | public Mono square(Integer input) { 13 | System.out.println("received: " + input); 14 | return Mono.just(input * input); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /server-app/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=com-alibaba-calculator 2 | server.port=0 3 | spring.rsocket.server.port=6565 4 | # Consul configuration 5 | spring.cloud.consul.discovery.instance-id=com-alibaba-calculator-${random.uuid} 6 | spring.cloud.consul.discovery.prefer-ip-address=true 7 | spring.cloud.consul.discovery.metadata.rsocketPort=${spring.rsocket.server.port} 8 | spring.cloud.consul.discovery.tags=com.alibaba.calculator.ExchangeCalculatorService,com.alibaba.calculator.MathCalculatorService 9 | # rsocket websocket configuration 10 | #spring.rsocket.server.transport=websocket 11 | #spring.rsocket.server.mapping-path=/rsocket 12 | #spring.cloud.consul.discovery.metadata.rsocketSchema=ws 13 | #spring.cloud.consul.discovery.metadata.rsocketPath=/rsocket 14 | ### management 15 | management.endpoints.web.exposure.include=* 16 | management.endpoint.health.show-components=always 17 | management.endpoint.health.show-details=always -------------------------------------------------------------------------------- /server-app/src/main/resources/bootstrap.properties: -------------------------------------------------------------------------------- 1 | spring.cloud.consul.host=localhost 2 | spring.cloud.consul.port=8500 3 | --------------------------------------------------------------------------------