├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------