├── .gitignore ├── README.md ├── ccx-rpc-common ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── ccx │ │ └── rpc │ │ └── common │ │ ├── consts │ │ ├── RegistryConst.java │ │ ├── RpcException.java │ │ └── URLKeyConst.java │ │ ├── extension │ │ ├── Adaptive.java │ │ ├── AdaptiveInvocationHandler.java │ │ ├── ExtensionLoader.java │ │ ├── Holder.java │ │ └── SPI.java │ │ └── url │ │ ├── URL.java │ │ ├── URLBuilder.java │ │ └── URLParser.java │ └── test │ ├── java │ └── com │ │ └── ccx │ │ └── rpc │ │ └── common │ │ └── test │ │ ├── extension │ │ ├── DefaultExtension.java │ │ ├── Extension.java │ │ ├── ExtensionLoaderTest.java │ │ ├── ExtensionNotDefault.java │ │ ├── ExtensionNotFile.java │ │ ├── ExtensionNotInterface.java │ │ ├── OtherExtension.java │ │ └── adaptive │ │ │ ├── AdaptiveTest.java │ │ │ ├── Ext.java │ │ │ ├── ExtFactory.java │ │ │ ├── ExtFactoryImpl1.java │ │ │ ├── ExtFactoryImpl2.java │ │ │ ├── ExtImpl1.java │ │ │ └── ExtImpl2.java │ │ └── url │ │ └── URLParserTest.java │ └── resources │ └── META-INF │ └── ccx-rpc │ ├── com.ccx.rpc.common.test.extension.Extension │ └── com.ccx.rpc.common.test.extension.adaptive.ExtFactory ├── ccx-rpc-core ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── ccx │ │ │ └── rpc │ │ │ └── core │ │ │ ├── annotation │ │ │ ├── Config.java │ │ │ ├── RpcReference.java │ │ │ ├── RpcScan.java │ │ │ └── RpcService.java │ │ │ ├── compress │ │ │ ├── Compressor.java │ │ │ ├── DummyCompressor.java │ │ │ └── GzipCompressor.java │ │ │ ├── config │ │ │ ├── ClusterConfig.java │ │ │ ├── ConfigManager.java │ │ │ ├── ProtocolConfig.java │ │ │ ├── RegistryConfig.java │ │ │ ├── ServiceConfig.java │ │ │ └── loader │ │ │ │ ├── ConfigLoader.java │ │ │ │ ├── PropertiesConfigLoader.java │ │ │ │ └── SystemPropertyLoader.java │ │ │ ├── consts │ │ │ ├── CompressType.java │ │ │ ├── MessageFormatConst.java │ │ │ ├── MessageType.java │ │ │ └── SerializeType.java │ │ │ ├── dto │ │ │ ├── AsyncResult.java │ │ │ ├── RpcMessage.java │ │ │ ├── RpcRequest.java │ │ │ ├── RpcResponse.java │ │ │ ├── RpcResponseCode.java │ │ │ └── RpcResult.java │ │ │ ├── faulttolerant │ │ │ ├── AbstractFaultTolerantInvoker.java │ │ │ ├── FailFastInvoker.java │ │ │ ├── FaultTolerantInvoker.java │ │ │ └── RetryInvoker.java │ │ │ ├── invoke │ │ │ ├── AbstractInvoker.java │ │ │ ├── Invoker.java │ │ │ └── NettyInvoker.java │ │ │ ├── loadbalance │ │ │ ├── AbstractLoadBalance.java │ │ │ ├── LoadBalance.java │ │ │ ├── RandomLoadBalance.java │ │ │ └── RoundRobinLoadBalance.java │ │ │ ├── proxy │ │ │ ├── RpcClientProxy.java │ │ │ └── RpcServiceCache.java │ │ │ ├── registry │ │ │ ├── AbstractRegistry.java │ │ │ ├── Registry.java │ │ │ ├── RegistryEvent.java │ │ │ ├── RegistryFactory.java │ │ │ ├── local │ │ │ │ ├── LocalRegistry.java │ │ │ │ └── LocalRegistryFactory.java │ │ │ └── zk │ │ │ │ ├── CuratorZkClient.java │ │ │ │ ├── ZkRegistry.java │ │ │ │ └── ZkRegistryFactory.java │ │ │ ├── remoting │ │ │ ├── client │ │ │ │ └── netty │ │ │ │ │ ├── NettyClient.java │ │ │ │ │ ├── NettyClientHandler.java │ │ │ │ │ └── UnprocessedRequests.java │ │ │ ├── codec │ │ │ │ ├── RpcMessageDecoder.java │ │ │ │ └── RpcMessageEncoder.java │ │ │ └── server │ │ │ │ ├── ShutdownHook.java │ │ │ │ └── netty │ │ │ │ ├── NettyServerBootstrap.java │ │ │ │ └── NettyServerHandler.java │ │ │ ├── serialize │ │ │ ├── Serializer.java │ │ │ └── protostuff │ │ │ │ └── ProtostuffSerializer.java │ │ │ └── spring │ │ │ ├── RpcScanner.java │ │ │ ├── RpcScannerRegistrar.java │ │ │ └── ServiceBeanPostProcessor.java │ └── resources │ │ └── META-INF │ │ └── ccx-rpc │ │ ├── com.ccx.rpc.core.compress.Compressor │ │ ├── com.ccx.rpc.core.config.loader.ConfigLoader │ │ ├── com.ccx.rpc.core.faulttolerant.FaultTolerantInvoker │ │ ├── com.ccx.rpc.core.invoke.Invoker │ │ ├── com.ccx.rpc.core.loadbalance.LoadBalance │ │ ├── com.ccx.rpc.core.registry.RegistryFactory │ │ └── com.ccx.rpc.core.serialize.Serializer │ └── test │ ├── java │ └── com │ │ └── ccx │ │ └── rpc │ │ └── core │ │ └── test │ │ ├── config │ │ └── ConfigManagerTest.java │ │ └── registry │ │ ├── LoadBalanceTest.java │ │ └── ZkRegistryTest.java │ └── resources │ └── ccx-rpc.properties ├── ccx-rpc-demo ├── ccx-rpc-demo-client │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── com │ │ │ └── ccx │ │ │ └── rpc │ │ │ └── demo │ │ │ └── client │ │ │ ├── ClientBootstrap.java │ │ │ ├── UserController.java │ │ │ └── spi │ │ │ ├── JSONSerializer.java │ │ │ ├── ProtostuffSerializer.java │ │ │ ├── SPILoaderTest.java │ │ │ └── Serializer.java │ │ └── resources │ │ ├── META-INF │ │ ├── ccx-rpc │ │ │ └── com.ccx.rpc.demo.client.spi.Serializer │ │ └── services │ │ │ └── com.ccx.rpc.demo.client.spi.Serializer │ │ ├── application.properties │ │ └── ccx-rpc.properties ├── ccx-rpc-demo-service │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── com │ │ │ └── ccx │ │ │ └── rpc │ │ │ └── demo │ │ │ └── service │ │ │ ├── ServiceBootstrap.java │ │ │ ├── api │ │ │ ├── UserService.java │ │ │ └── impl │ │ │ │ ├── UserServiceImpl.java │ │ │ │ └── UserServiceImplV2.java │ │ │ └── bean │ │ │ └── UserInfo.java │ │ └── resources │ │ └── ccx-rpc.properties └── pom.xml └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # maven ignore 2 | target/ 3 | *.jar 4 | !.mvn/wrapper/* 5 | *.war 6 | *.zip 7 | *.tar 8 | *.tar.gz 9 | .flattened-pom.xml 10 | 11 | # eclipse ignore 12 | .settings/ 13 | .project 14 | .classpath 15 | 16 | # idea ignore 17 | .idea/ 18 | *.ipr 19 | *.iml 20 | *.iws 21 | 22 | # visual-studio-code ignore 23 | .vscode/ 24 | 25 | # temp ignore 26 | *.log 27 | *.cache 28 | *.diff 29 | *.patch 30 | *.tmp 31 | 32 | # system ignore 33 | .DS_Store 34 | Thumbs.db 35 | *.orig 36 | 37 | # license check result 38 | license-list 39 | 40 | # grpc compiler 41 | compiler/gradle.properties 42 | compiler/build/* 43 | compiler/.gradle/* 44 | 45 | # protobuf 46 | dubbo-serialization/dubbo-serialization-protobuf/build/* 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ccx-rpc 2 | 3 | #### 介绍 4 | 这是一个基于 Netty + Zookeeper + Protostuff 的简易 RPC 框架。 5 | 造轮子主要是为了学习,因为我觉得"会用"、"会读源码"、"会写出来"是完全不一样的水平。 6 | 7 | Github: https://github.com/chenchuxin/ccx-rpc 8 | 9 | Gitee: https://gitee.com/imccx/ccx-rpc 10 | 11 | 更多详细教程请看:https://www.cnblogs.com/chenchuxin/category/2010813.html 12 | 13 | #### 目录 14 | 以下是重要的包的简介: 15 | ``` 16 | |- ccx-rpc-common:基础的代码 17 | |- extendsion:扩展,主要实现了一套自己的 SPI,参考 dubbo,做了简化 18 | |- url: 同样也是参考 dubbo 的 URL,一般是构建参数用的 19 | |- ccx-rpc-core: rpc 核心逻辑 20 | |- annotation:里面包含了一些自定义的注解,例如 @RpcService(服务提供)、@RpcReference(服务引用) 21 | |- compress: 压缩,网络传输需要压缩数据 22 | |- config: 定义了一套配置的接口,例如配置服务绑定的端口,zk 的地址等 23 | |- faulttolerant: 集群容错,例如快速失败、重试等 24 | |- loadbalance: 负载均衡,多个服务应该如何选择。有随机策略、轮询策略等 25 | |- proxy: 代理,用于客户端代理,客户端调用服务接口,实际上是一个网络请求的过程 26 | |- registry: 注册中心,例如 zk 注册中心 27 | |- remoting: 网络相关的东西,例如自定义协议、Netty 收发请求等 28 | |- serialize: 序列化,网络传输,序列化是必不可少了 29 | |- spring: 一些 spring 相关的东西,例如扫描器、bean 的处理 30 | |- ccx-rpc-demo: 框架的使用例子 31 | |- ccx-rpc-demo-client: 客户端,服务引用方 32 | |- ccx-rpc-demo-service: 服务提供方 33 | ``` 34 | 35 | #### 功能列表 36 | - [x] 自定义 SPI 扩展 37 | - [x] 动态代理 38 | - [x] JDK Proxy 39 | - [ ] Javassist 生成代码,直接调用 40 | - [x] 注册中心 41 | - [x] Zookeeper 42 | - [ ] Eureka 43 | - [ ] Nacos 44 | - [ ] Consul 45 | - [ ] ... 46 | - [x] 序列化 47 | - [x] Protostuff 48 | - [ ] Kryo 49 | - [ ] ... 50 | - [x] 压缩 51 | - [x] gzip 52 | - [ ] ... 53 | - [x] 远程通信 54 | - [x] 自定义通信协议 55 | - [x] 使用 Netty 框架 56 | - [x] 配置 57 | - [x] JVM 参数配置 58 | - [x] properties 文件配置 59 | - [ ] Apollo 动态配置 60 | - [x] 负载均衡 61 | - [x] 随机策略 62 | - [x] 轮询策略 63 | - [ ] 一致性哈希 64 | - [x] 多版本 65 | - [x] 集群容错 66 | - [x] 重试策略 67 | - [x] 快速失败策略 68 | - [ ] 优雅停机 69 | - [ ] 监控后台 70 | - [ ] 线程模型 71 | - [ ] 服务分组 72 | - [ ] 过滤器 73 | 74 | #### 运行 75 | 1. 环境要求:JDK8 以上、Lombok 插件 76 | 2. 需要安装 `Zookeeper` 并运行 77 | 3. 修改配置文件 `ccx-rpc.properties` 中的 zk 地址、监听端口,启动服务 `com.ccx.rpc.demo.service.ServiceBootstrap` 78 | 4. 修改配置文件 `ccx-rpc.properties` 中的 zk 地址,启动客户端 `com.ccx.rpc.demo.client.ClientBootstrap` 79 | 5. 访问客户端地址 `http://localhost:8864/user/1` 就可以啦. `http://localhost:8864/user/v2/1` 可以访问另一个实现,这是多版本功能。 80 | 81 | #### 参与贡献 82 | 1. Fork 本仓库 83 | 2. 新建分支 84 | 3. 提交代码 85 | 4. 新建 Pull Request 86 | -------------------------------------------------------------------------------- /ccx-rpc-common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | ccx-rpc 7 | com.ccx 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | ccx-rpc-common 13 | 14 | 15 | 16 | junit 17 | junit 18 | test 19 | 20 | 21 | org.projectlombok 22 | lombok 23 | 24 | 25 | cn.hutool 26 | hutool-all 27 | 28 | 29 | org.springframework 30 | spring-context 31 | 32 | 33 | org.slf4j 34 | slf4j-api 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/main/java/com/ccx/rpc/common/consts/RegistryConst.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.consts; 2 | 3 | /** 4 | * 注册中心常量 5 | * 6 | * @author chenchuxin 7 | * @date 2021/7/18 8 | */ 9 | public interface RegistryConst { 10 | /** 11 | * 服务提供方目录 12 | */ 13 | String PROVIDERS_CATEGORY = "providers"; 14 | 15 | /** 16 | * 消费者目录 17 | */ 18 | String CONSUMERS_CATEGORY = "consumers"; 19 | } 20 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/main/java/com/ccx/rpc/common/consts/RpcException.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.consts; 2 | 3 | /** 4 | * @author chenchuxin 5 | * @date 2021/8/1 6 | */ 7 | public class RpcException extends RuntimeException { 8 | public RpcException(String message) { 9 | super(message); 10 | } 11 | 12 | public RpcException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/main/java/com/ccx/rpc/common/consts/URLKeyConst.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.consts; 2 | 3 | /** 4 | * URL 上面的 Key 常量 5 | * 6 | * @author chenchuxin 7 | * @date 2021/7/18 8 | */ 9 | public interface URLKeyConst { 10 | 11 | String TIMEOUT = "timeout"; 12 | 13 | String DEFAULT_PREFIX = "default."; 14 | 15 | String DEFAULT = "default"; 16 | 17 | String INTERFACE = "interface"; 18 | 19 | String PROTOCOL = "protocol"; 20 | 21 | String VERSION = "version"; 22 | 23 | String ANY_HOST = "anyHost"; 24 | 25 | String CCX_RPC_PROTOCOL = "ccx-rpc"; 26 | } 27 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/main/java/com/ccx/rpc/common/extension/Adaptive.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.extension; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 与 {@link SPI} 联合使用,在方法上面标记。
7 | * 使用的方法中必须要有 URL 参数
8 | * 代理生成的扩展类会自动读取 URL 参数上的 {@link Adaptive#value()} 参数,再根据这个参数类型,获取对应的扩展类 9 | * 10 | * @author chenchuxin 11 | * @date 2021/7/19 12 | * @see SPI 13 | */ 14 | @Documented 15 | @Target(ElementType.METHOD) 16 | @Retention(RetentionPolicy.RUNTIME) 17 | public @interface Adaptive { 18 | String value() default ""; 19 | } 20 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/main/java/com/ccx/rpc/common/extension/AdaptiveInvocationHandler.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.extension; 2 | 3 | import com.ccx.rpc.common.consts.URLKeyConst; 4 | import com.ccx.rpc.common.url.URL; 5 | 6 | import java.lang.reflect.InvocationHandler; 7 | import java.lang.reflect.Method; 8 | 9 | /** 10 | * @author chenchuxin 11 | * @date 2021/7/20 12 | */ 13 | public class AdaptiveInvocationHandler implements InvocationHandler { 14 | 15 | private final Class clazz; 16 | 17 | public AdaptiveInvocationHandler(Class tClass) { 18 | clazz = tClass; 19 | } 20 | 21 | @Override 22 | public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 23 | if (args.length == 0) { 24 | return method.invoke(proxy, args); 25 | } 26 | // 找 URL 参数 27 | URL url = null; 28 | for (Object arg : args) { 29 | if (arg instanceof URL) { 30 | url = (URL) arg; 31 | break; 32 | } 33 | } 34 | if (url == null) { 35 | return method.invoke(proxy, args); 36 | } 37 | 38 | Adaptive adaptive = method.getAnnotation(Adaptive.class); 39 | if (adaptive == null) { 40 | return method.invoke(proxy, args); 41 | } 42 | 43 | String extendNameKey = adaptive.value(); 44 | String extendName; 45 | if (URLKeyConst.PROTOCOL.equals(extendNameKey)) { 46 | extendName = url.getProtocol(); 47 | } else { 48 | extendName = url.getParam(extendNameKey, method.getDeclaringClass() + "." + method.getName()); 49 | } 50 | 51 | ExtensionLoader extensionLoader = ExtensionLoader.getLoader(clazz); 52 | T extension = extensionLoader.getExtension(extendName); 53 | return method.invoke(extension, args); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/main/java/com/ccx/rpc/common/extension/ExtensionLoader.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.extension; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.IOException; 7 | import java.io.InputStreamReader; 8 | import java.lang.reflect.InvocationHandler; 9 | import java.lang.reflect.Proxy; 10 | import java.net.URL; 11 | import java.nio.charset.StandardCharsets; 12 | import java.util.Enumeration; 13 | import java.util.Map; 14 | import java.util.concurrent.ConcurrentHashMap; 15 | 16 | /** 17 | *

扩展类加载器。

18 | *

扩展类的配置写到 {@code META-INF/extensions/ccx-rpc} 目录下,文件名为接口全名。

19 | *

文件格式为:扩展名=扩展类全名。例如:{@code zk=com.ccx.core.registry.ZkRegistry}

20 | *

获取扩展类实例的代码示例如下:

21 | *
{@code
 22 |  *     ExtensionLoader loader = ExtensionLoader.getExtensionLoader(Registry.class)
 23 |  *     Registry registry = loader.getExtension("zk");
 24 |  * }
25 | * 26 | * @author chenchuxin 27 | * @date 2021/7/12 28 | */ 29 | public class ExtensionLoader { 30 | 31 | /** 32 | * 扩展类实例缓存 {name: 扩展类实例} 33 | */ 34 | private final Map extensionsCache = new ConcurrentHashMap<>(); 35 | 36 | /** 37 | * 扩展加载器实例缓存 {类型:加载器实例} 38 | */ 39 | private static final Map, ExtensionLoader> extensionLoaderCache = new ConcurrentHashMap<>(); 40 | 41 | /** 42 | * 扩展类配置列表缓存 {type: {name, 扩展类}} 43 | */ 44 | private final Holder>> extensionClassesCache = new Holder<>(); 45 | 46 | /** 47 | * 创建扩展实例类的锁缓存 {name: synchronized 持有的锁} 48 | */ 49 | private final Map createExtensionLockMap = new ConcurrentHashMap<>(); 50 | 51 | /** 52 | * 扩展类加载器的类型 53 | */ 54 | private final Class type; 55 | 56 | /** 57 | * 扩展类存放的目录地址 58 | */ 59 | private static final String EXTENSION_PATH = "META-INF/ccx-rpc/"; 60 | 61 | /** 62 | * 默认扩展名缓存 63 | */ 64 | private final String defaultNameCache; 65 | 66 | /** 67 | * 构造函数 68 | * 69 | * @param type 扩展类加载器的类型 70 | */ 71 | private ExtensionLoader(Class type) { 72 | this.type = type; 73 | SPI annotation = type.getAnnotation(SPI.class); 74 | defaultNameCache = annotation.value(); 75 | } 76 | 77 | /** 78 | * 获取对应类型的扩展加载器实例 79 | * 80 | * @param type 扩展类加载器的类型 81 | * @return 扩展类加载器实例 82 | */ 83 | public static ExtensionLoader getLoader(Class type) { 84 | // 扩展类型必须是接口 85 | if (!type.isInterface()) { 86 | throw new IllegalStateException(type.getName() + " is not interface"); 87 | } 88 | SPI annotation = type.getAnnotation(SPI.class); 89 | if (annotation == null) { 90 | throw new IllegalStateException(type.getName() + " has not @SPI annotation."); 91 | } 92 | ExtensionLoader extensionLoader = extensionLoaderCache.get(type); 93 | if (extensionLoader != null) { 94 | //noinspection unchecked 95 | return (ExtensionLoader) extensionLoader; 96 | } 97 | extensionLoader = new ExtensionLoader<>(type); 98 | extensionLoaderCache.putIfAbsent(type, extensionLoader); 99 | //noinspection unchecked 100 | return (ExtensionLoader) extensionLoader; 101 | } 102 | 103 | /** 104 | * 获取默认的扩展类实例,会自动加载 @SPI 注解中的 value 指定的类实例 105 | * 106 | * @return 返回该类的注解 @SPI.value 指定的类实例 107 | */ 108 | public T getDefaultExtension() { 109 | return getExtension(defaultNameCache); 110 | } 111 | 112 | /** 113 | * 根据名字获取扩展类实例(单例) 114 | * 115 | * @param name 扩展类在配置文件中配置的名字. 如果名字是空的或者空白的,则返回默认扩展 116 | * @return 单例扩展类实例,如果找不到,则抛出异常 117 | */ 118 | public T getExtension(String name) { 119 | if (StrUtil.isBlank(name)) { 120 | return getDefaultExtension(); 121 | } 122 | // 从缓存中获取单例 123 | T extension = extensionsCache.get(name); 124 | if (extension == null) { 125 | Object lock = createExtensionLockMap.computeIfAbsent(name, k -> new Object()); 126 | //noinspection SynchronizationOnLocalVariableOrMethodParameter 127 | synchronized (lock) { 128 | extension = extensionsCache.get(name); 129 | if (extension == null) { 130 | extension = createExtension(name); 131 | extensionsCache.put(name, extension); 132 | } 133 | } 134 | } 135 | return extension; 136 | } 137 | 138 | /** 139 | * 获取自适应扩展类 140 | * 141 | * @return 动态代理自适应类 142 | */ 143 | public T getAdaptiveExtension() { 144 | InvocationHandler handler = new AdaptiveInvocationHandler<>(type); 145 | //noinspection unchecked 146 | return (T) Proxy.newProxyInstance(ExtensionLoader.class.getClassLoader(), 147 | new Class[]{type}, handler); 148 | } 149 | 150 | /** 151 | * 创建对应名字的扩展类实例 152 | * 153 | * @param name 扩展名 154 | * @return 扩展类实例 155 | */ 156 | private T createExtension(String name) { 157 | // 获取当前类型所有扩展类 158 | Map> extensionClasses = getAllExtensionClasses(); 159 | // 再根据名字找到对应的扩展类 160 | Class clazz = extensionClasses.get(name); 161 | if (clazz == null) { 162 | throw new IllegalStateException("Extension not found. name=" + name + ", type=" + type.getName()); 163 | } 164 | try { 165 | //noinspection unchecked 166 | return (T) clazz.newInstance(); 167 | } catch (InstantiationException | IllegalAccessException e) { 168 | throw new IllegalStateException("Extension not found. name=" + name + ", type=" + type.getName() + ". " + e.toString()); 169 | } 170 | } 171 | 172 | /** 173 | * 获取当前类型{@link #type}的所有扩展类 174 | * 175 | * @return {name: clazz} 176 | */ 177 | private Map> getAllExtensionClasses() { 178 | Map> extensionClasses = extensionClassesCache.get(); 179 | if (extensionClasses != null) { 180 | return extensionClasses; 181 | } 182 | synchronized (extensionClassesCache) { 183 | extensionClasses = extensionClassesCache.get(); 184 | if (extensionClasses == null) { 185 | extensionClasses = loadClassesFromResources(); 186 | extensionClassesCache.set(extensionClasses); 187 | } 188 | } 189 | return extensionClasses; 190 | } 191 | 192 | /** 193 | * 从资源文件中加载所有扩展类 194 | * 195 | * @return {name: 扩展类} 196 | */ 197 | private Map> loadClassesFromResources() { 198 | Map> extensionClasses = new ConcurrentHashMap<>(); 199 | // 扩展配置文件名 200 | String fileName = EXTENSION_PATH + type.getName(); 201 | // 拿到资源文件夹 202 | ClassLoader classLoader = ExtensionLoader.class.getClassLoader(); 203 | try { 204 | Enumeration resources = classLoader.getResources(fileName); 205 | while (resources.hasMoreElements()) { 206 | URL url = resources.nextElement(); 207 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) { 208 | // 开始读文件 209 | while (true) { 210 | String line = reader.readLine(); 211 | if (line == null) { 212 | break; 213 | } 214 | parseLine(line, extensionClasses); 215 | } 216 | } 217 | } 218 | } catch (IOException | ClassNotFoundException e) { 219 | throw new IllegalStateException("Parse file fail. " + e.toString()); 220 | } 221 | return extensionClasses; 222 | } 223 | 224 | /** 225 | * 解析行,并且把解析到的类,放到 extensionClasses 中 226 | * 227 | * @param line 行 228 | * @param extensionClasses 扩展类列表 229 | * @throws ClassNotFoundException 找不到类 230 | */ 231 | private void parseLine(String line, Map> extensionClasses) throws ClassNotFoundException { 232 | line = line.trim(); 233 | // 忽略#号开头的注释 234 | if (line.startsWith("#")) { 235 | return; 236 | } 237 | String[] kv = line.split("="); 238 | if (kv.length != 2 || kv[0].length() == 0 || kv[1].length() == 0) { 239 | throw new IllegalStateException("Extension file parsing error. Invalid format!"); 240 | } 241 | if (extensionClasses.containsKey(kv[0])) { 242 | throw new IllegalStateException(kv[0] + " is already exists!"); 243 | } 244 | Class clazz = ExtensionLoader.class.getClassLoader().loadClass(kv[1]); 245 | extensionClasses.put(kv[0], clazz); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/main/java/com/ccx/rpc/common/extension/Holder.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.extension; 2 | 3 | /** 4 | * 持有者。用于单个变量既可以做锁又可以做值 5 | * 6 | * @author chenchuxin 7 | * @date 2021/7/17 8 | */ 9 | public class Holder { 10 | private T value; 11 | 12 | public T get() { 13 | return value; 14 | } 15 | 16 | public void set(T value) { 17 | this.value = value; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/main/java/com/ccx/rpc/common/extension/SPI.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.extension; 2 | 3 | import com.ccx.rpc.common.consts.URLKeyConst; 4 | 5 | import java.lang.annotation.*; 6 | 7 | /** 8 | * 被此注解标记的类,表示是一个扩展接口 9 | * 10 | * @author chenchuxin 11 | * @date 2021/7/12 12 | */ 13 | @Documented 14 | @Target(ElementType.TYPE) 15 | @Retention(RetentionPolicy.RUNTIME) 16 | public @interface SPI { 17 | /** 18 | * 默认扩展类全路径 19 | * 20 | * @return 默认不填是 default 21 | */ 22 | String value() default URLKeyConst.DEFAULT; 23 | } 24 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/main/java/com/ccx/rpc/common/url/URL.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.url; 2 | 3 | import cn.hutool.core.map.MapUtil; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | 7 | import java.util.Collections; 8 | import java.util.Map; 9 | 10 | /** 11 | * url 12 | * 13 | * @author chenchuxin 14 | * @date 2021/7/18 15 | */ 16 | @Builder 17 | @Getter 18 | public class URL { 19 | /** 20 | * 协议 21 | */ 22 | private final String protocol; 23 | 24 | /** 25 | * 域名 26 | */ 27 | private final String host; 28 | 29 | /** 30 | * 端口 31 | */ 32 | private final int port; 33 | 34 | /** 35 | * 用户名 36 | */ 37 | private final String username; 38 | 39 | /** 40 | * 密码 41 | */ 42 | private final String password; 43 | 44 | /** 45 | * 路径 46 | */ 47 | private final String path; 48 | 49 | /** 50 | * 参数 51 | */ 52 | private final Map params; 53 | 54 | // ====================== cache 55 | private String fullString; 56 | // ====================== end cache 57 | 58 | public Map getParams() { 59 | if (params == null) { 60 | return Collections.emptyMap(); 61 | } 62 | return params; 63 | } 64 | 65 | /** 66 | * 获取地址 host:port 67 | * 68 | * @return host:port 69 | */ 70 | public String getAddress() { 71 | return host + ":" + port; 72 | } 73 | 74 | /** 75 | * 获取参数 76 | * 77 | * @param key 参数 key 78 | * @param defaultVal 默认值,如果获取不到,则用这个值 79 | * @return 参数,如果获取不到使用默认值 80 | */ 81 | public String getParam(String key, String defaultVal) { 82 | return params.getOrDefault(key, defaultVal); 83 | } 84 | 85 | /** 86 | * 获取 int 类型的参数 87 | * 88 | * @param key 参数 key 89 | * @param defaultVal 默认值,如果获取不到,则用这个值 90 | * @return 参数,如果获取不到使用默认值 91 | */ 92 | public int getIntParam(String key, int defaultVal) { 93 | if (MapUtil.isEmpty(params)) { 94 | return defaultVal; 95 | } 96 | String val = params.get(key); 97 | return val != null ? Integer.parseInt(val) : defaultVal; 98 | } 99 | 100 | public String toFullString() { 101 | if (fullString != null) { 102 | return fullString; 103 | } 104 | return fullString = URLParser.parseToStr(this, true, true); 105 | } 106 | 107 | public static URL valueOf(String url) { 108 | return URLParser.toURL(url); 109 | } 110 | 111 | @Override 112 | public String toString() { 113 | return toFullString(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/main/java/com/ccx/rpc/common/url/URLBuilder.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.url; 2 | 3 | import cn.hutool.core.map.MapUtil; 4 | import com.ccx.rpc.common.consts.URLKeyConst; 5 | 6 | import java.util.Map; 7 | 8 | /** 9 | * @author chenchuxin 10 | * @date 2021/8/1 11 | */ 12 | public class URLBuilder { 13 | 14 | /** 15 | * 获取url上相关服务的参数 16 | * 17 | * @return 参数 18 | */ 19 | public static Map getServiceParam(String interfaceName, String rpcVersion) { 20 | return MapUtil.builder() 21 | .put(URLKeyConst.INTERFACE, interfaceName) 22 | .put(URLKeyConst.VERSION, rpcVersion).build(); 23 | } 24 | 25 | /** 26 | * 获取url上相关服务的参数 27 | * 28 | * @return 参数 29 | */ 30 | public static Map getServiceParam(Class interfaceClass, String rpcVersion) { 31 | return getServiceParam(interfaceClass.getCanonicalName(), rpcVersion); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/main/java/com/ccx/rpc/common/url/URLParser.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.url; 2 | 3 | import cn.hutool.core.util.ArrayUtil; 4 | import cn.hutool.core.util.StrUtil; 5 | import java.util.*; 6 | 7 | import static com.ccx.rpc.common.consts.URLKeyConst.DEFAULT_PREFIX; 8 | 9 | /** 10 | * @author chenchuxin 11 | * @date 2021/7/18 12 | */ 13 | public class URLParser { 14 | 15 | /** 16 | * 转成字符串 17 | * 18 | * @param url url对象 19 | * @param appendUser 是否加上用户密码 20 | * @param appendParameter 是否加上参数 21 | * @param parameters 参数,appendParameter=true 的时候有用 22 | * @return url 格式的字符串 23 | */ 24 | public static String parseToStr(URL url, boolean appendUser, boolean appendParameter, String... parameters) { 25 | StringBuilder buf = new StringBuilder(); 26 | if (StrUtil.isNotEmpty(url.getProtocol())) { 27 | buf.append(url.getProtocol()); 28 | buf.append("://"); 29 | } 30 | if (appendUser && StrUtil.isNotEmpty(url.getUsername())) { 31 | buf.append(url.getUsername()); 32 | if (StrUtil.isNotEmpty(url.getPassword())) { 33 | buf.append(":"); 34 | buf.append(url.getPassword()); 35 | } 36 | buf.append("@"); 37 | } 38 | if (StrUtil.isNotEmpty(url.getHost())) { 39 | buf.append(url.getHost()); 40 | if (url.getPort() > 0) { 41 | buf.append(":"); 42 | buf.append(url.getPort()); 43 | } 44 | } 45 | if (StrUtil.isNotEmpty(url.getPath())) { 46 | buf.append("/"); 47 | buf.append(url.getPath()); 48 | } 49 | 50 | if (appendParameter) { 51 | buildParameters(url, buf, parameters); 52 | } 53 | return buf.toString(); 54 | } 55 | 56 | private static void buildParameters(URL url, StringBuilder buf, String[] parameters) { 57 | List includes = (ArrayUtil.isEmpty(parameters) ? null : Arrays.asList(parameters)); 58 | boolean first = true; 59 | for (Map.Entry entry : new TreeMap<>(url.getParams()).entrySet()) { 60 | if (StrUtil.isNotEmpty(entry.getKey()) 61 | && (includes == null || includes.contains(entry.getKey()))) { 62 | if (first) { 63 | buf.append("?"); 64 | first = false; 65 | } else { 66 | buf.append("&"); 67 | } 68 | buf.append(entry.getKey()); 69 | buf.append("="); 70 | buf.append(entry.getValue() == null ? "" : entry.getValue().trim()); 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * 从字符串解析出 URL 对象。参考 dubbo 77 | * 78 | * @param url URL string 79 | * @return URL instance 80 | * @see URL 81 | */ 82 | public static URL toURL(String url) { 83 | if (url == null || (url = url.trim()).length() == 0) { 84 | throw new IllegalArgumentException("url == null"); 85 | } 86 | String protocol = null; 87 | String username = null; 88 | String password = null; 89 | String host = null; 90 | int port = 0; 91 | String path = null; 92 | Map parameters = null; 93 | int i = url.indexOf('?'); // separator between body and parameters 94 | if (i >= 0) { 95 | String[] parts = url.substring(i + 1).split("&"); 96 | parameters = new HashMap<>(); 97 | for (String part : parts) { 98 | part = part.trim(); 99 | if (part.length() > 0) { 100 | int j = part.indexOf('='); 101 | if (j >= 0) { 102 | String key = part.substring(0, j); 103 | String value = part.substring(j + 1); 104 | parameters.put(key, value); 105 | // compatible with lower versions registering "default." keys 106 | if (key.startsWith(DEFAULT_PREFIX)) { 107 | parameters.putIfAbsent(key.substring(DEFAULT_PREFIX.length()), value); 108 | } 109 | } else { 110 | parameters.put(part, part); 111 | } 112 | } 113 | } 114 | url = url.substring(0, i); 115 | } 116 | i = url.indexOf("://"); 117 | if (i >= 0) { 118 | if (i == 0) { 119 | throw new IllegalStateException("url missing protocol: \"" + url + "\""); 120 | } 121 | protocol = url.substring(0, i); 122 | url = url.substring(i + 3); 123 | } else { 124 | // case: file:/path/to/file.txt 125 | i = url.indexOf(":/"); 126 | if (i >= 0) { 127 | if (i == 0) { 128 | throw new IllegalStateException("url missing protocol: \"" + url + "\""); 129 | } 130 | protocol = url.substring(0, i); 131 | url = url.substring(i + 1); 132 | } 133 | } 134 | 135 | i = url.indexOf('/'); 136 | if (i >= 0) { 137 | path = url.substring(i + 1); 138 | url = url.substring(0, i); 139 | } 140 | i = url.lastIndexOf('@'); 141 | if (i >= 0) { 142 | username = url.substring(0, i); 143 | int j = username.indexOf(':'); 144 | if (j >= 0) { 145 | password = username.substring(j + 1); 146 | username = username.substring(0, j); 147 | } 148 | url = url.substring(i + 1); 149 | } 150 | i = url.lastIndexOf(':'); 151 | if (i >= 0 && i < url.length() - 1) { 152 | //noinspection StatementWithEmptyBody 153 | if (url.lastIndexOf('%') > i) { 154 | // ipv6 address with scope id 155 | // e.g. fe80:0:0:0:894:aeec:f37d:23e1%en0 156 | // see https://howdoesinternetwork.com/2013/ipv6-zone-id 157 | // ignore 158 | } else { 159 | port = Integer.parseInt(url.substring(i + 1)); 160 | url = url.substring(0, i); 161 | } 162 | } 163 | if (url.length() > 0) { 164 | host = url; 165 | } 166 | 167 | return URL.builder().protocol(protocol).username(username).password(password) 168 | .host(host).port(port).path(path).params(parameters).build(); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/test/java/com/ccx/rpc/common/test/extension/DefaultExtension.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.test.extension; 2 | 3 | /** 4 | * 测试用的默认扩展 5 | * 6 | * @author chenchuxin 7 | * @date 2021/7/13 8 | */ 9 | public class DefaultExtension implements Extension { 10 | @Override 11 | public void doSomething() { 12 | System.out.println("DefaultExtensionTest doing..."); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/test/java/com/ccx/rpc/common/test/extension/Extension.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.test.extension; 2 | 3 | import com.ccx.rpc.common.extension.SPI; 4 | 5 | /** 6 | * 测试用的扩展接口 7 | * 8 | * @author chenchuxin 9 | * @date 2021/7/13 10 | */ 11 | @SPI("default") 12 | public interface Extension { 13 | void doSomething(); 14 | } 15 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/test/java/com/ccx/rpc/common/test/extension/ExtensionLoaderTest.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.test.extension; 2 | 3 | import com.ccx.rpc.common.extension.ExtensionLoader; 4 | import org.junit.Assert; 5 | import org.junit.Test; 6 | 7 | /** 8 | * 类加载器测试 9 | * 10 | * @author chenchuxin 11 | * @date 2021/7/13 12 | */ 13 | public class ExtensionLoaderTest { 14 | 15 | @Test 16 | public void getExtensionLoaderTest() { 17 | ExtensionLoader extensionLoader = ExtensionLoader.getLoader(Extension.class); 18 | Assert.assertNotNull(extensionLoader); 19 | } 20 | 21 | @Test 22 | public void getExtensionTest() { 23 | ExtensionLoader extensionLoader = ExtensionLoader.getLoader(Extension.class); 24 | Extension extension = (Extension) extensionLoader.getExtension("other"); 25 | Assert.assertTrue(extension instanceof OtherExtension); 26 | } 27 | 28 | @Test 29 | public void getDefaultExtensionTest() { 30 | ExtensionLoader extensionLoader = ExtensionLoader.getLoader(Extension.class); 31 | Object extension = extensionLoader.getDefaultExtension(); 32 | Assert.assertTrue(extension instanceof DefaultExtension); 33 | } 34 | 35 | @Test(expected = IllegalStateException.class) 36 | public void notInterfaceExtensionLoaderTest() { 37 | ExtensionLoader.getLoader(ExtensionNotInterface.class); 38 | } 39 | 40 | @Test(expected = IllegalStateException.class) 41 | public void notDefaultExtensionLoaderTest() { 42 | ExtensionLoader extensionLoader = ExtensionLoader.getLoader(ExtensionNotDefault.class); 43 | extensionLoader.getDefaultExtension(); 44 | } 45 | 46 | @Test(expected = IllegalStateException.class) 47 | public void notFileExtensionLoaderTest() { 48 | ExtensionLoader extensionLoader = ExtensionLoader.getLoader(ExtensionNotFile.class); 49 | extensionLoader.getDefaultExtension(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/test/java/com/ccx/rpc/common/test/extension/ExtensionNotDefault.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.test.extension; 2 | 3 | /** 4 | * 测试用的扩展接口 5 | * 6 | * @author chenchuxin 7 | * @date 2021/7/13 8 | */ 9 | public interface ExtensionNotDefault { 10 | void doSomething(); 11 | } 12 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/test/java/com/ccx/rpc/common/test/extension/ExtensionNotFile.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.test.extension; 2 | 3 | import com.ccx.rpc.common.extension.SPI; 4 | 5 | /** 6 | * 测试用的扩展接口 7 | * 8 | * @author chenchuxin 9 | * @date 2021/7/13 10 | */ 11 | @SPI("default") 12 | public interface ExtensionNotFile { 13 | void doSomething(); 14 | } 15 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/test/java/com/ccx/rpc/common/test/extension/ExtensionNotInterface.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.test.extension; 2 | 3 | /** 4 | * 测试用的扩展接口 5 | * 6 | * @author chenchuxin 7 | * @date 2021/7/13 8 | */ 9 | public class ExtensionNotInterface { 10 | public void doSomething() { 11 | System.out.println("ExtensionNotInterface doSomething..."); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/test/java/com/ccx/rpc/common/test/extension/OtherExtension.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.test.extension; 2 | 3 | /** 4 | * 测试用的其他扩展 5 | * 6 | * @author chenchuxin 7 | * @date 2021/7/13 8 | */ 9 | public class OtherExtension implements Extension { 10 | @Override 11 | public void doSomething() { 12 | System.out.println("OtherExtensionTest doing..."); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/test/java/com/ccx/rpc/common/test/extension/adaptive/AdaptiveTest.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.test.extension.adaptive; 2 | 3 | import com.ccx.rpc.common.extension.ExtensionLoader; 4 | import com.ccx.rpc.common.url.URL; 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | 8 | /** 9 | * @author chenchuxin 10 | * @date 2021/7/20 11 | */ 12 | public class AdaptiveTest { 13 | 14 | @Test 15 | public void getAdaptiveExtendTest() { 16 | ExtensionLoader extensionLoader = ExtensionLoader.getLoader(ExtFactory.class); 17 | ExtFactory extFactory = extensionLoader.getAdaptiveExtension(); 18 | Ext ext = extFactory.getExt(URL.valueOf("ext2://localhost:123")); 19 | Assert.assertTrue(ext instanceof ExtImpl2); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/test/java/com/ccx/rpc/common/test/extension/adaptive/Ext.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.test.extension.adaptive; 2 | 3 | /** 4 | * @author chenchuxin 5 | * @date 2021/7/20 6 | */ 7 | public interface Ext { 8 | 9 | void doing(); 10 | } 11 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/test/java/com/ccx/rpc/common/test/extension/adaptive/ExtFactory.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.test.extension.adaptive; 2 | 3 | import com.ccx.rpc.common.consts.URLKeyConst; 4 | import com.ccx.rpc.common.extension.Adaptive; 5 | import com.ccx.rpc.common.extension.SPI; 6 | import com.ccx.rpc.common.url.URL; 7 | 8 | /** 9 | * @author chenchuxin 10 | * @date 2021/7/20 11 | */ 12 | @SPI 13 | public interface ExtFactory { 14 | 15 | @Adaptive(URLKeyConst.PROTOCOL) 16 | Ext getExt(URL url); 17 | } 18 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/test/java/com/ccx/rpc/common/test/extension/adaptive/ExtFactoryImpl1.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.test.extension.adaptive; 2 | 3 | import com.ccx.rpc.common.url.URL; 4 | 5 | /** 6 | * @author chenchuxin 7 | * @date 2021/7/20 8 | */ 9 | public class ExtFactoryImpl1 implements ExtFactory { 10 | 11 | public Ext getExt(URL url) { 12 | return new ExtImpl1(url); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/test/java/com/ccx/rpc/common/test/extension/adaptive/ExtFactoryImpl2.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.test.extension.adaptive; 2 | 3 | import com.ccx.rpc.common.url.URL; 4 | 5 | /** 6 | * @author chenchuxin 7 | * @date 2021/7/20 8 | */ 9 | public class ExtFactoryImpl2 implements ExtFactory { 10 | 11 | public Ext getExt(URL url) { 12 | return new ExtImpl2(url); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/test/java/com/ccx/rpc/common/test/extension/adaptive/ExtImpl1.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.test.extension.adaptive; 2 | 3 | import com.ccx.rpc.common.url.URL; 4 | 5 | /** 6 | * @author chenchuxin 7 | * @date 2021/7/20 8 | */ 9 | public class ExtImpl1 implements Ext { 10 | 11 | private URL url; 12 | 13 | public ExtImpl1(URL url) { 14 | this.url = url; 15 | } 16 | 17 | @Override 18 | public void doing() { 19 | System.out.println("ExtImpl1:" + url.toFullString()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/test/java/com/ccx/rpc/common/test/extension/adaptive/ExtImpl2.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.test.extension.adaptive; 2 | 3 | import com.ccx.rpc.common.url.URL; 4 | 5 | /** 6 | * @author chenchuxin 7 | * @date 2021/7/20 8 | */ 9 | public class ExtImpl2 implements Ext { 10 | 11 | private URL url; 12 | 13 | public ExtImpl2(URL url) { 14 | this.url = url; 15 | } 16 | 17 | @Override 18 | public void doing() { 19 | System.out.println("ExtImpl2:" + url.toFullString()); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/test/java/com/ccx/rpc/common/test/url/URLParserTest.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.common.test.url; 2 | 3 | import com.ccx.rpc.common.url.URL; 4 | import com.ccx.rpc.common.url.URLParser; 5 | import org.junit.Assert; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | /** 13 | * @author chenchuxin 14 | * @date 2021/7/18 15 | * @see URLParser 16 | */ 17 | public class URLParserTest { 18 | 19 | private URL url; 20 | 21 | @Before 22 | public void setup() { 23 | Map params = new HashMap<>(); 24 | params.put("timeout", "1000"); 25 | params.put("param2", "value2"); 26 | params.put("key3", "value3"); 27 | url = URL.builder().protocol("zk") 28 | .username("user").password("pwd") 29 | .host("localhost").port(1234) 30 | .path("path").params(params).build(); 31 | } 32 | 33 | @Test 34 | public void parseToStrTest() { 35 | String urlStr = URLParser.parseToStr(url, true, true); 36 | Assert.assertEquals("zk://user:pwd@localhost:1234/path?key3=value3¶m2=value2&timeout=1000", urlStr); 37 | } 38 | 39 | @Test 40 | public void toURLTest() { 41 | String urlStr = "zk://user:pwd@localhost:1234/path?key3=value3¶m2=value2&timeout=1000"; 42 | URL url = URLParser.toURL(urlStr); 43 | Assert.assertEquals(urlStr, url.toFullString()); 44 | Assert.assertEquals("zk", url.getProtocol()); 45 | Assert.assertEquals("user", url.getUsername()); 46 | Assert.assertEquals("pwd", url.getPassword()); 47 | Assert.assertEquals("localhost", url.getHost()); 48 | Assert.assertEquals(1234, url.getPort()); 49 | Assert.assertEquals("path", url.getPath()); 50 | Assert.assertEquals("1000", url.getParam("timeout", null)); 51 | Assert.assertEquals("value2", url.getParam("param2", null)); 52 | Assert.assertEquals("value3", url.getParam("key3", null)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ccx-rpc-common/src/test/resources/META-INF/ccx-rpc/com.ccx.rpc.common.test.extension.Extension: -------------------------------------------------------------------------------- 1 | default=com.ccx.rpc.common.test.extension.DefaultExtension 2 | other=com.ccx.rpc.common.test.extension.OtherExtension -------------------------------------------------------------------------------- /ccx-rpc-common/src/test/resources/META-INF/ccx-rpc/com.ccx.rpc.common.test.extension.adaptive.ExtFactory: -------------------------------------------------------------------------------- 1 | ext1=com.ccx.rpc.common.test.extension.adaptive.ExtFactoryImpl1 2 | ext2=com.ccx.rpc.common.test.extension.adaptive.ExtFactoryImpl2 -------------------------------------------------------------------------------- /ccx-rpc-core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | ccx-rpc 7 | com.ccx 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | ccx-rpc-core 13 | 14 | 15 | 16 | com.ccx 17 | ccx-rpc-common 18 | 19 | 20 | junit 21 | junit 22 | test 23 | 24 | 25 | 26 | org.apache.curator 27 | curator-recipes 28 | 29 | 30 | org.apache.curator 31 | curator-test 32 | test 33 | 34 | 35 | cn.hutool 36 | hutool-all 37 | 38 | 39 | io.protostuff 40 | protostuff-core 41 | 42 | 43 | io.protostuff 44 | protostuff-runtime 45 | 46 | 47 | io.netty 48 | netty-all 49 | 50 | 51 | org.springframework 52 | spring-context 53 | 54 | 55 | org.springframework.boot 56 | spring-boot 57 | 58 | 59 | org.slf4j 60 | slf4j-api 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/annotation/Config.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * 放在配置类上,可以使用 ConfigLoader 加载 7 | * 8 | * @author chenchuxin 9 | * @date 2021/8/1 10 | */ 11 | @Documented 12 | @Target(ElementType.TYPE) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface Config { 15 | 16 | /** 17 | * 前缀名 18 | * 19 | * @return 前缀,不能空 20 | */ 21 | String prefix(); 22 | } 23 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/annotation/RpcReference.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * Rpc 引用,服务调用方用 7 | * 8 | * @author chenchuxin 9 | * @date 2021/7/31 10 | */ 11 | @Documented 12 | @Target(ElementType.FIELD) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface RpcReference { 15 | 16 | /** 17 | * 版本,没有特殊要求不用填写 18 | * 19 | * @return 版本,默认"" 20 | */ 21 | String version() default ""; 22 | 23 | } 24 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/annotation/RpcScan.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.annotation; 2 | 3 | import com.ccx.rpc.core.spring.RpcScannerRegistrar; 4 | import org.springframework.context.annotation.Import; 5 | 6 | import java.lang.annotation.*; 7 | 8 | /** 9 | * @author chenchuxin 10 | * @date 2021/7/30 11 | */ 12 | @Documented 13 | @Target(ElementType.TYPE) 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Import(RpcScannerRegistrar.class) 16 | public @interface RpcScan { 17 | String[] basePackages(); 18 | } 19 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/annotation/RpcService.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | /** 6 | * RPC 服务注解,打上这个注解的,将作为服务注册到注册中心 7 | * 8 | * @author chenchuxin 9 | * @date 2021/7/30 10 | */ 11 | @Inherited 12 | @Documented 13 | @Target(ElementType.TYPE) 14 | @Retention(RetentionPolicy.RUNTIME) 15 | public @interface RpcService { 16 | 17 | /** 18 | * 版本,如果同样签名的接口参数不兼容,可以用版本区分 19 | * 20 | * @return 默认空 21 | */ 22 | String version() default ""; 23 | } 24 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/compress/Compressor.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.compress; 2 | 3 | import com.ccx.rpc.common.extension.SPI; 4 | 5 | /** 6 | * 压缩解压器 7 | * 8 | * @author chenchuxin 9 | * @date 2021/7/24 10 | */ 11 | @SPI("dummy") 12 | public interface Compressor { 13 | 14 | /** 15 | * 压缩 16 | * 17 | * @param bytes 压缩前的字节数组 18 | * @return 压缩后的字节数组 19 | */ 20 | byte[] compress(byte[] bytes); 21 | 22 | /** 23 | * 解压 24 | * 25 | * @param bytes 解压前的字节数组 26 | * @return 解压后的字节数组 27 | */ 28 | byte[] decompress(byte[] bytes); 29 | } 30 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/compress/DummyCompressor.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.compress; 2 | 3 | /** 4 | * 伪压缩器,啥事不干。有一些序列化工具压缩已经做得很好了,无需再压缩 5 | * 6 | * @author chenchuxin 7 | * @date 2021/8/27 8 | */ 9 | public class DummyCompressor implements Compressor { 10 | @Override 11 | public byte[] compress(byte[] bytes) { 12 | return bytes; 13 | } 14 | 15 | @Override 16 | public byte[] decompress(byte[] bytes) { 17 | return bytes; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/compress/GzipCompressor.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.compress; 2 | 3 | import cn.hutool.core.lang.Assert; 4 | import com.ccx.rpc.core.compress.Compressor; 5 | 6 | import java.io.ByteArrayInputStream; 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.IOException; 9 | import java.util.Arrays; 10 | import java.util.zip.GZIPInputStream; 11 | import java.util.zip.GZIPOutputStream; 12 | 13 | /** 14 | * gzip 压缩/解压器 15 | * 16 | * @author chenchuxin 17 | * @date 2021/7/24 18 | */ 19 | public class GzipCompressor implements Compressor { 20 | 21 | /** 22 | * 4k 缓冲区 23 | */ 24 | private static final int BUFFER_SIZE = 4096; 25 | 26 | 27 | @Override 28 | public byte[] compress(byte[] bytes) { 29 | Assert.notNull("bytes should not null"); 30 | try (ByteArrayOutputStream out = new ByteArrayOutputStream(); 31 | GZIPOutputStream gzip = new GZIPOutputStream(out)) { 32 | gzip.write(bytes); 33 | gzip.flush(); 34 | gzip.finish(); 35 | return out.toByteArray(); 36 | } catch (IOException e) { 37 | throw new RuntimeException("gzip compress error", e); 38 | } 39 | } 40 | 41 | @Override 42 | public byte[] decompress(byte[] bytes) { 43 | Assert.notNull("bytes should not null"); 44 | try (ByteArrayOutputStream out = new ByteArrayOutputStream(); 45 | GZIPInputStream gunzip = new GZIPInputStream(new ByteArrayInputStream(bytes))) { 46 | byte[] buffer = new byte[BUFFER_SIZE]; 47 | int n; 48 | while ((n = gunzip.read(buffer)) > -1) { 49 | out.write(buffer, 0, n); 50 | } 51 | return out.toByteArray(); 52 | } catch (IOException e) { 53 | throw new RuntimeException("gzip decompress error", e); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/config/ClusterConfig.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.config; 2 | 3 | import com.ccx.rpc.core.annotation.Config; 4 | import lombok.Data; 5 | 6 | /** 7 | * 集群配置 8 | * 9 | * @author chenchuxin 10 | * @date 2021/8/7 11 | */ 12 | @Data 13 | @Config(prefix = "cluster") 14 | public class ClusterConfig { 15 | /** 16 | * 负载均衡策略 17 | */ 18 | private String loadBalance; 19 | 20 | /** 21 | * 容错策略 22 | */ 23 | private String faultTolerant; 24 | 25 | /** 26 | * 重试次数,只有容错策略是 'retry' 的时候才有效 27 | */ 28 | private Integer retryTimes; 29 | } 30 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/config/ConfigManager.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.config; 2 | 3 | import cn.hutool.core.convert.Convert; 4 | import cn.hutool.core.util.StrUtil; 5 | import com.ccx.rpc.common.extension.ExtensionLoader; 6 | import com.ccx.rpc.core.annotation.Config; 7 | import com.ccx.rpc.core.config.loader.ConfigLoader; 8 | 9 | import java.lang.reflect.Field; 10 | import java.lang.reflect.Modifier; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Map; 14 | import java.util.concurrent.ConcurrentHashMap; 15 | 16 | /** 17 | * 配置管理 18 | * 19 | * @author chenchuxin 20 | * @date 2021/7/31 21 | */ 22 | public class ConfigManager { 23 | 24 | /** 25 | * 按照优先级排序的加载器 26 | */ 27 | private final List configLoaders; 28 | 29 | private final Map, Object> configCache = new ConcurrentHashMap<>(); 30 | 31 | private ConfigManager() { 32 | // 按照优先级放好 33 | String[] configLoaderNames = new String[]{"system-property", "properties"}; 34 | configLoaders = new ArrayList<>(configLoaderNames.length); 35 | for (String loaderName : configLoaderNames) { 36 | ConfigLoader configLoader = ExtensionLoader.getLoader(ConfigLoader.class).getExtension(loaderName); 37 | configLoaders.add(configLoader); 38 | } 39 | } 40 | 41 | private static final ConfigManager instant = new ConfigManager(); 42 | 43 | public static ConfigManager getInstant() { 44 | return instant; 45 | } 46 | 47 | 48 | /** 49 | * 加载配置,有缓存 50 | * 51 | * @param clazz 配置类型 52 | * @param 类型 53 | * @return 配置实体类 54 | */ 55 | @SuppressWarnings("unchecked") 56 | public T loadConfig(Class clazz) { 57 | T config = (T) configCache.get(clazz); 58 | if (config == null) { 59 | config = loadAndCreateConfig(clazz); 60 | configCache.put(clazz, config); 61 | } 62 | return config; 63 | } 64 | 65 | /** 66 | * 获取配置项 67 | * 68 | * @param key 配置项的 key 69 | * @return 如果获取不到,返回 null 70 | */ 71 | public String loadConfigItem(String key) { 72 | // 按照优先级,先获取到就返回 73 | for (ConfigLoader configLoader : configLoaders) { 74 | String value = configLoader.loadConfigItem(key); 75 | if (value != null) { 76 | return value; 77 | } 78 | } 79 | return null; 80 | } 81 | 82 | /** 83 | * 加载并创建配置类 84 | * 85 | * @param clazz 类型 class 86 | * @param 类型 87 | * @return 配置类,load 不到的字段为 null 88 | */ 89 | private T loadAndCreateConfig(Class clazz) { 90 | Config configAnnotation = clazz.getAnnotation(Config.class); 91 | if (configAnnotation == null) { 92 | throw new IllegalStateException("config class " + clazz.getName() + " must has @Config annotation"); 93 | } 94 | String prefix = configAnnotation.prefix(); 95 | if (StrUtil.isBlank(prefix)) { 96 | throw new IllegalArgumentException("config class " + clazz.getName() + "@Config annotation must has prefix"); 97 | } 98 | try { 99 | T configObject = clazz.newInstance(); 100 | for (Field field : clazz.getDeclaredFields()) { 101 | // 忽略掉静态的 102 | if (Modifier.isStatic(field.getModifiers())) { 103 | continue; 104 | } 105 | String configKey = prefix + "." + field.getName(); 106 | String value = loadConfigItem(configKey); 107 | if (value == null) { 108 | continue; 109 | } 110 | 111 | Object convertedValue = Convert.convert(field.getType(), value); 112 | field.setAccessible(true); 113 | field.set(configObject, convertedValue); 114 | } 115 | return configObject; 116 | } catch (Exception e) { 117 | throw new IllegalStateException(e.getMessage(), e); 118 | } 119 | } 120 | 121 | /** 122 | * 获取注册中心的配置 123 | */ 124 | public RegistryConfig getRegistryConfig() { 125 | return loadConfig(RegistryConfig.class); 126 | } 127 | 128 | public ServiceConfig getServiceConfig() { 129 | return loadConfig(ServiceConfig.class); 130 | } 131 | 132 | public ProtocolConfig getProtocolConfig() { 133 | return loadConfig(ProtocolConfig.class); 134 | } 135 | 136 | public ClusterConfig getClusterConfig() { 137 | return loadConfig(ClusterConfig.class); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/config/ProtocolConfig.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.config; 2 | 3 | import com.ccx.rpc.core.annotation.Config; 4 | import com.ccx.rpc.core.consts.CompressType; 5 | import com.ccx.rpc.core.consts.SerializeType; 6 | import lombok.Data; 7 | 8 | /** 9 | * 协议相关配置 10 | * 11 | * @author chenchuxin 12 | * @date 2021/8/1 13 | */ 14 | @Data 15 | @Config(prefix = "protocol") 16 | public class ProtocolConfig { 17 | 18 | /** 19 | * 序列化类型 {@link SerializeType} 20 | */ 21 | private String serializeType; 22 | 23 | /** 24 | * 压缩类型 {@link CompressType} 25 | */ 26 | private String compressType; 27 | 28 | public String getSerializeType() { 29 | return serializeType != null ? serializeType : SerializeType.PROTOSTUFF.getName(); 30 | } 31 | 32 | public String getCompressType() { 33 | return compressType != null ? compressType : CompressType.DUMMY.getName(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/config/RegistryConfig.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.config; 2 | 3 | import com.ccx.rpc.common.url.URL; 4 | import com.ccx.rpc.core.annotation.Config; 5 | import lombok.Data; 6 | 7 | /** 8 | * 注册中心配置 9 | * 10 | * @author chenchuxin 11 | * @date 2021/7/31 12 | */ 13 | @Data 14 | @Config(prefix = "registry") 15 | public class RegistryConfig { 16 | 17 | /** 18 | * 注册中心地址。zk://10.20.153.10:6379?backup=10.20.153.11:6379,10.20.153.12:6379 19 | */ 20 | private String address; 21 | 22 | public URL toURL() { 23 | return URL.valueOf(address); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/config/ServiceConfig.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.config; 2 | 3 | import com.ccx.rpc.core.annotation.Config; 4 | import lombok.Data; 5 | 6 | /** 7 | * 服务启动配置 8 | * 9 | * @author chenchuxin 10 | * @date 2021/7/31 11 | */ 12 | @Data 13 | @Config(prefix = "service") 14 | public class ServiceConfig { 15 | /** 16 | * 默认服务端口 17 | */ 18 | private static final Integer DEFAULT_SERVICE_PORT = 5525; 19 | 20 | /** 21 | * 监听的端口 22 | */ 23 | private Integer port; 24 | 25 | public Integer getPort() { 26 | return (port != null && port > 0) ? port : DEFAULT_SERVICE_PORT; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/config/loader/ConfigLoader.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.config.loader; 2 | 3 | import com.ccx.rpc.common.extension.SPI; 4 | 5 | /** 6 | * 配置加载器 7 | * 8 | * @author chenchuxin 9 | * @date 2021/8/1 10 | */ 11 | @SPI 12 | public interface ConfigLoader { 13 | 14 | /** 15 | * 加载配置项 16 | * 17 | * @param key 配置的 key 18 | * @return 配置项的值,如果不存在,返回 null 19 | */ 20 | String loadConfigItem(String key); 21 | 22 | } 23 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/config/loader/PropertiesConfigLoader.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.config.loader; 2 | 3 | import cn.hutool.core.io.resource.NoResourceException; 4 | import cn.hutool.setting.Setting; 5 | import cn.hutool.setting.SettingUtil; 6 | import lombok.extern.slf4j.Slf4j; 7 | 8 | /** 9 | * ccx-rpc.properties 文件配置。prefix.configField=xxx 10 | * 11 | * @author chenchuxin 12 | * @date 2021/8/22 13 | */ 14 | @Slf4j 15 | public class PropertiesConfigLoader implements ConfigLoader { 16 | 17 | private Setting setting = null; 18 | 19 | public PropertiesConfigLoader() { 20 | try { 21 | setting = SettingUtil.get("ccx-rpc.properties"); 22 | } catch (NoResourceException ex) { 23 | log.info("Config file 'ccx-rpc.properties' not exist!"); 24 | } 25 | } 26 | 27 | 28 | @Override 29 | public String loadConfigItem(String key) { 30 | if (setting == null) { 31 | return null; 32 | } 33 | return setting.getStr(key); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/config/loader/SystemPropertyLoader.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.config.loader; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | 5 | /** 6 | * java 参数配置 -Dprefix.configField=xxx 7 | * 8 | * @author chenchuxin 9 | * @date 2021/8/1 10 | */ 11 | @Slf4j 12 | public class SystemPropertyLoader implements ConfigLoader { 13 | 14 | @Override 15 | public String loadConfigItem(String key) { 16 | return System.getProperty(key); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/consts/CompressType.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.consts; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | /** 7 | * 压缩类型 8 | * 9 | * @author chenchuxin 10 | * @date 2021/7/25 11 | */ 12 | @Getter 13 | @AllArgsConstructor 14 | public enum CompressType { 15 | /** 16 | * 伪压缩器,啥事不干。有一些序列化工具压缩已经做得很好了,无需再压缩 17 | */ 18 | DUMMY((byte) 0, "dummy"), 19 | 20 | GZIP((byte) 1, "gzip"); 21 | 22 | private final byte value; 23 | private final String name; 24 | 25 | /** 26 | * 通过值获取压缩类型枚举 27 | * 28 | * @param value 值 29 | * @return 如果获取不到,返回 DUMMY 30 | */ 31 | public static CompressType fromValue(byte value) { 32 | for (CompressType codecType : CompressType.values()) { 33 | if (codecType.getValue() == value) { 34 | return codecType; 35 | } 36 | } 37 | return DUMMY; 38 | } 39 | 40 | /** 41 | * 通过名字获取压缩类型枚举 42 | * 43 | * @param name 名字 44 | * @return 如果获取不到,返回 DUMMY 45 | */ 46 | public static CompressType fromName(String name) { 47 | for (CompressType codecType : CompressType.values()) { 48 | if (codecType.getName().equals(name)) { 49 | return codecType; 50 | } 51 | } 52 | return DUMMY; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/consts/MessageFormatConst.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.consts; 2 | 3 | import cn.hutool.core.util.ByteUtil; 4 | 5 | import java.util.concurrent.atomic.AtomicLong; 6 | 7 | /** 8 | * 消息格式常量 9 | * 10 | * @author chenchuxin 11 | * @date 2021/7/25 12 | */ 13 | public interface MessageFormatConst { 14 | 15 | /** 16 | * 魔数 17 | */ 18 | byte[] MAGIC = ByteUtil.numberToBytes((short) 0x52ff); 19 | 20 | /** 21 | * 版本 22 | */ 23 | byte VERSION = 1; 24 | 25 | /** 26 | * 请求 Id 27 | */ 28 | AtomicLong REQUEST_ID = new AtomicLong(0); 29 | 30 | String PING_DATA = "ping"; 31 | String PONG_DATA = "pong"; 32 | 33 | /** 34 | * 魔法数字长度 35 | */ 36 | int MAGIC_LENGTH = 2; 37 | 38 | /** 39 | * 版本长度 40 | */ 41 | int VERSION_LENGTH = 1; 42 | 43 | /** 44 | * 总长度字段的长度 45 | */ 46 | int FULL_LENGTH_LENGTH = 4; 47 | 48 | /** 49 | * 消息类型长度 50 | */ 51 | int MESSAGE_TYPE_LENGTH = 1; 52 | 53 | /** 54 | * 编解码类型长度 55 | */ 56 | int CODEC_LENGTH = 1; 57 | 58 | /** 59 | * 压缩器类型长度 60 | */ 61 | int COMPRESS_LENGTH = 1; 62 | 63 | /** 64 | * 请求id 长度 65 | */ 66 | int REQUEST_ID_LENGTH = 8; 67 | 68 | /** 69 | * 请求头长度 70 | */ 71 | int HEADER_LENGTH = MAGIC_LENGTH + VERSION_LENGTH + FULL_LENGTH_LENGTH + MESSAGE_TYPE_LENGTH + CODEC_LENGTH + CODEC_LENGTH + REQUEST_ID_LENGTH; 72 | 73 | /** 74 | * 协议最大长度 75 | */ 76 | int MAX_FRAME_LENGTH = 8 * 1024 * 1024; 77 | 78 | } 79 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/consts/MessageType.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.consts; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | /** 7 | * 消息类型 8 | * 9 | * @author chenchuxin 10 | * @date 2021/7/24 11 | */ 12 | @Getter 13 | @AllArgsConstructor 14 | public enum MessageType { 15 | /** 16 | * 普通请求 17 | */ 18 | REQUEST((byte) 1), 19 | 20 | /** 21 | * 普通响应 22 | */ 23 | RESPONSE((byte) 2), 24 | 25 | /** 26 | * 心跳 27 | */ 28 | HEARTBEAT((byte) 3), 29 | ; 30 | private final byte value; 31 | } 32 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/consts/SerializeType.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.consts; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | /** 7 | * 序列化类型 8 | * 9 | * @author chenchuxin 10 | * @date 2021/7/25 11 | */ 12 | @Getter 13 | @AllArgsConstructor 14 | public enum SerializeType { 15 | PROTOSTUFF((byte) 1, "protostuff"); 16 | 17 | private final byte value; 18 | private final String name; 19 | 20 | public static SerializeType fromValue(byte value) { 21 | for (SerializeType serializeType : SerializeType.values()) { 22 | if (serializeType.getValue() == value) { 23 | return serializeType; 24 | } 25 | } 26 | return null; 27 | } 28 | 29 | public static SerializeType fromName(String name) { 30 | for (SerializeType serializeType : SerializeType.values()) { 31 | if (serializeType.getName().equals(name)) { 32 | return serializeType; 33 | } 34 | } 35 | return null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/dto/AsyncResult.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.dto; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | 5 | import java.util.concurrent.CompletableFuture; 6 | import java.util.concurrent.ExecutionException; 7 | 8 | /** 9 | * 异步结果 10 | * 11 | * @author chenchuxin 12 | * @date 2021/8/8 13 | */ 14 | @Slf4j 15 | public class AsyncResult implements RpcResult { 16 | 17 | private final CompletableFuture future; 18 | 19 | public AsyncResult(CompletableFuture future) { 20 | this.future = future; 21 | } 22 | 23 | @Override 24 | public boolean isSuccess() { 25 | return !future.isCompletedExceptionally(); 26 | } 27 | 28 | @Override 29 | public Object getData() { 30 | try { 31 | return future.get(); 32 | } catch (InterruptedException | ExecutionException e) { 33 | log.error("getData error.", e); 34 | } 35 | return null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/dto/RpcMessage.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.dto; 2 | 3 | import com.ccx.rpc.core.consts.CompressType; 4 | import com.ccx.rpc.core.consts.MessageType; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | 10 | /** 11 | * @author chenchuxin 12 | * @date 2021/7/24 13 | */ 14 | @Data 15 | @Builder 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class RpcMessage { 19 | /** 20 | * 消息类型 {@link MessageType#getValue()} 21 | */ 22 | private byte messageType; 23 | 24 | /** 25 | * 压缩类型 {@link CompressType#getValue()} 26 | */ 27 | private byte compressTye; 28 | 29 | /** 30 | * 序列化类型 31 | */ 32 | private byte serializeType; 33 | 34 | private long requestId; 35 | 36 | private Object data; 37 | } 38 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/dto/RpcRequest.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.dto; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import lombok.*; 5 | 6 | /** 7 | * RPC 请求实体 8 | * 9 | * @author chenchuxin 10 | * @date 2021/7/25 11 | */ 12 | @Data 13 | @Builder 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class RpcRequest { 17 | /** 18 | * 接口名 19 | */ 20 | private String interfaceName; 21 | /** 22 | * 方法名 23 | */ 24 | private String methodName; 25 | /** 26 | * 参数列表 27 | */ 28 | private Object[] params; 29 | /** 30 | * 参数类型列表 31 | */ 32 | private Class[] paramTypes; 33 | /** 34 | * 接口版本 35 | */ 36 | private String version; 37 | 38 | public String getRpcServiceForCache() { 39 | if (StrUtil.isBlank(version)) { 40 | return interfaceName; 41 | } 42 | return interfaceName + "_" + version; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/dto/RpcResponse.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * @author chenchuxin 10 | * @date 2021/7/25 11 | */ 12 | @Data 13 | @Builder 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | public class RpcResponse { 17 | /** 18 | * 请求id 19 | */ 20 | private long requestId; 21 | /** 22 | * 响应码 23 | */ 24 | private Integer code; 25 | /** 26 | * 提示消息 27 | */ 28 | private String message; 29 | /** 30 | * 响应数据 31 | */ 32 | private T data; 33 | 34 | public static RpcResponse success(T data, long requestId) { 35 | return RpcResponse.builder() 36 | .code(RpcResponseCode.SUCCESS.getCode()) 37 | .message(RpcResponseCode.SUCCESS.getMessage()) 38 | .data(data).requestId(requestId) 39 | .build(); 40 | } 41 | 42 | public static RpcResponse fail() { 43 | return RpcResponse.builder() 44 | .code(RpcResponseCode.FAIL.getCode()) 45 | .message(RpcResponseCode.FAIL.getMessage()) 46 | .build(); 47 | } 48 | 49 | public static RpcResponse fail(RpcResponseCode code) { 50 | return RpcResponse.builder() 51 | .code(code.getCode()) 52 | .message(code.getMessage()) 53 | .build(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/dto/RpcResponseCode.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | /** 7 | * 返回码 8 | * 9 | * @author chenchuxin 10 | * @date 2021/7/26 11 | */ 12 | @Getter 13 | @AllArgsConstructor 14 | public enum RpcResponseCode { 15 | SUCCESS(200, "success"), 16 | FAIL(500, "fail"); 17 | 18 | private final int code; 19 | private final String message; 20 | } 21 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/dto/RpcResult.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.dto; 2 | 3 | /** 4 | * @author chenchuxin 5 | * @date 2021/8/8 6 | */ 7 | public interface RpcResult { 8 | 9 | boolean isSuccess(); 10 | 11 | Object getData(); 12 | } 13 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/faulttolerant/AbstractFaultTolerantInvoker.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.faulttolerant; 2 | 3 | import com.ccx.rpc.common.consts.RpcException; 4 | import com.ccx.rpc.common.extension.ExtensionLoader; 5 | import com.ccx.rpc.core.config.ClusterConfig; 6 | import com.ccx.rpc.core.config.ConfigManager; 7 | import com.ccx.rpc.core.dto.RpcRequest; 8 | import com.ccx.rpc.core.dto.RpcResult; 9 | import com.ccx.rpc.core.invoke.Invoker; 10 | import com.ccx.rpc.core.loadbalance.LoadBalance; 11 | 12 | /** 13 | * 容错执行者 14 | * 15 | * @author chenchuxin 16 | * @date 2021/8/8 17 | */ 18 | public abstract class AbstractFaultTolerantInvoker implements FaultTolerantInvoker { 19 | 20 | protected final ClusterConfig clusterConfig = ConfigManager.getInstant().getClusterConfig(); 21 | 22 | private final LoadBalance loadBalance = ExtensionLoader.getLoader(LoadBalance.class) 23 | .getExtension(ConfigManager.getInstant().getClusterConfig().getLoadBalance()); 24 | private final Invoker invoker = ExtensionLoader.getLoader(Invoker.class).getDefaultExtension(); 25 | 26 | @Override 27 | public RpcResult invoke(RpcRequest request) throws RpcException { 28 | return doInvoke(request, invoker, loadBalance); 29 | } 30 | 31 | /** 32 | * 执行 33 | * 34 | * @param request 请求 35 | * @param invoker 具体执行者 36 | * @param loadBalance 负载 37 | * @return 结果 38 | * @throws RpcException 请求失败会抛出异常 39 | */ 40 | protected abstract RpcResult doInvoke(RpcRequest request, Invoker invoker, LoadBalance loadBalance) throws RpcException; 41 | } 42 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/faulttolerant/FailFastInvoker.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.faulttolerant; 2 | 3 | import com.ccx.rpc.common.consts.RpcException; 4 | import com.ccx.rpc.core.dto.RpcRequest; 5 | import com.ccx.rpc.core.dto.RpcResult; 6 | import com.ccx.rpc.core.invoke.Invoker; 7 | import com.ccx.rpc.core.loadbalance.LoadBalance; 8 | 9 | /** 10 | * 快速失败 11 | * 12 | * @author chenchuxin 13 | * @date 2021/8/8 14 | */ 15 | public class FailFastInvoker extends AbstractFaultTolerantInvoker { 16 | 17 | @Override 18 | protected RpcResult doInvoke(RpcRequest request, Invoker invoker, LoadBalance loadBalance) throws RpcException { 19 | return invoker.invoke(request); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/faulttolerant/FaultTolerantInvoker.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.faulttolerant; 2 | 3 | import com.ccx.rpc.common.extension.SPI; 4 | import com.ccx.rpc.core.invoke.Invoker; 5 | 6 | /** 7 | * 容错 8 | * 9 | * @author chenchuxin 10 | * @date 2021/8/8 11 | */ 12 | @SPI("fail-fast") 13 | public interface FaultTolerantInvoker extends Invoker { 14 | } 15 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/faulttolerant/RetryInvoker.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.faulttolerant; 2 | 3 | import com.ccx.rpc.common.consts.RpcException; 4 | import com.ccx.rpc.core.dto.RpcRequest; 5 | import com.ccx.rpc.core.dto.RpcResult; 6 | import com.ccx.rpc.core.invoke.Invoker; 7 | import com.ccx.rpc.core.loadbalance.LoadBalance; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | import java.util.Optional; 11 | 12 | /** 13 | * 重试执行,只有幂等的才能重试 14 | * 15 | * @author chenchuxin 16 | * @date 2021/8/8 17 | */ 18 | @Slf4j 19 | public class RetryInvoker extends AbstractFaultTolerantInvoker { 20 | 21 | /** 22 | * 默认重试次数 23 | */ 24 | private static final Integer DEFAULT_RETRY_TIMES = 3; 25 | 26 | @Override 27 | protected RpcResult doInvoke(RpcRequest request, Invoker invoker, LoadBalance loadBalance) throws RpcException { 28 | int retryTimes = Optional.ofNullable(clusterConfig.getRetryTimes()).orElse(DEFAULT_RETRY_TIMES); 29 | RpcException rpcException = null; 30 | for (int i = 0; i < retryTimes; i++) { 31 | try { 32 | RpcResult result = invoker.invoke(request); 33 | if (result.isSuccess()) { 34 | return result; 35 | } 36 | } catch (RpcException ex) { 37 | log.error("invoke error. retry times=" + i, ex); 38 | rpcException = ex; 39 | } 40 | } 41 | if (rpcException == null) { 42 | rpcException = new RpcException("invoker error. request=" + request); 43 | } 44 | throw rpcException; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/invoke/AbstractInvoker.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.invoke; 2 | 3 | import cn.hutool.core.collection.CollectionUtil; 4 | import com.ccx.rpc.common.consts.RpcException; 5 | import com.ccx.rpc.common.consts.URLKeyConst; 6 | import com.ccx.rpc.common.extension.ExtensionLoader; 7 | import com.ccx.rpc.common.url.URL; 8 | import com.ccx.rpc.common.url.URLBuilder; 9 | import com.ccx.rpc.core.config.ConfigManager; 10 | import com.ccx.rpc.core.dto.RpcRequest; 11 | import com.ccx.rpc.core.dto.RpcResult; 12 | import com.ccx.rpc.core.loadbalance.LoadBalance; 13 | import com.ccx.rpc.core.registry.Registry; 14 | import com.ccx.rpc.core.registry.RegistryFactory; 15 | 16 | import java.util.Collections; 17 | import java.util.List; 18 | import java.util.Map; 19 | 20 | /** 21 | * 抽象执行者 22 | * 23 | * @author chenchuxin 24 | * @date 2021/8/8 25 | */ 26 | public abstract class AbstractInvoker implements Invoker { 27 | 28 | private final RegistryFactory registryFactory = ExtensionLoader.getLoader(RegistryFactory.class).getAdaptiveExtension(); 29 | private final Registry registry = registryFactory.getRegistry(ConfigManager.getInstant().getRegistryConfig().toURL()); 30 | private final LoadBalance loadBalance = ExtensionLoader.getLoader(LoadBalance.class) 31 | .getExtension(ConfigManager.getInstant().getClusterConfig().getLoadBalance()); 32 | 33 | @Override 34 | public RpcResult invoke(RpcRequest request) throws RpcException { 35 | Map serviceParam = URLBuilder.getServiceParam(request.getInterfaceName(), request.getVersion()); 36 | URL url = URL.builder().protocol(URLKeyConst.CCX_RPC_PROTOCOL).host(URLKeyConst.ANY_HOST).params(serviceParam).build(); 37 | // 注册中心拿出所有服务的信息 38 | List urls = registry.lookup(url); 39 | if (CollectionUtil.isEmpty(urls)) { 40 | throw new RpcException("Not service Providers registered." + serviceParam); 41 | } 42 | URL selected = loadBalance.select(urls, request); 43 | return doInvoke(request, selected); 44 | } 45 | 46 | protected abstract RpcResult doInvoke(RpcRequest rpcRequest, URL selected) throws RpcException; 47 | } 48 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/invoke/Invoker.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.invoke; 2 | 3 | import com.ccx.rpc.common.consts.RpcException; 4 | import com.ccx.rpc.common.extension.SPI; 5 | import com.ccx.rpc.core.dto.RpcRequest; 6 | import com.ccx.rpc.core.dto.RpcResult; 7 | 8 | /** 9 | * 执行者 10 | * 11 | * @author chenchuxin 12 | * @date 2021/8/8 13 | */ 14 | @SPI("netty") 15 | public interface Invoker { 16 | 17 | /** 18 | * 执行 19 | * 20 | * @param request 请求 21 | * @return result 22 | * @throws RpcException 执行异常会抛出 23 | */ 24 | RpcResult invoke(RpcRequest request) throws RpcException; 25 | } 26 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/invoke/NettyInvoker.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.invoke; 2 | 3 | import com.ccx.rpc.common.consts.RpcException; 4 | import com.ccx.rpc.common.url.URL; 5 | import com.ccx.rpc.core.config.ConfigManager; 6 | import com.ccx.rpc.core.config.ProtocolConfig; 7 | import com.ccx.rpc.core.consts.CompressType; 8 | import com.ccx.rpc.core.consts.MessageFormatConst; 9 | import com.ccx.rpc.core.consts.MessageType; 10 | import com.ccx.rpc.core.consts.SerializeType; 11 | import com.ccx.rpc.core.dto.*; 12 | import com.ccx.rpc.core.remoting.client.netty.NettyClient; 13 | import com.ccx.rpc.core.remoting.client.netty.UnprocessedRequests; 14 | import io.netty.channel.Channel; 15 | import io.netty.channel.ChannelFutureListener; 16 | import lombok.extern.slf4j.Slf4j; 17 | 18 | import java.net.InetSocketAddress; 19 | import java.util.concurrent.CompletableFuture; 20 | 21 | /** 22 | * netty 执行者,相当于发请求 23 | * 24 | * @author chenchuxin 25 | * @date 2021/8/8 26 | */ 27 | @Slf4j 28 | public class NettyInvoker extends AbstractInvoker { 29 | 30 | private final NettyClient nettyClient = NettyClient.getInstance(); 31 | 32 | @Override 33 | protected RpcResult doInvoke(RpcRequest request, URL selected) throws RpcException { 34 | InetSocketAddress socketAddress = new InetSocketAddress(selected.getHost(), selected.getPort()); 35 | Channel channel = nettyClient.getChannel(socketAddress); 36 | if (channel.isActive()) { 37 | CompletableFuture> resultFuture = new CompletableFuture<>(); 38 | // 构建 RPC 消息,此处会构建 requestId 39 | RpcMessage rpcMessage = buildRpcMessage(request); 40 | UnprocessedRequests.put(rpcMessage.getRequestId(), resultFuture); 41 | channel.writeAndFlush(rpcMessage).addListener((ChannelFutureListener) future -> { 42 | if (future.isSuccess()) { 43 | log.info("client send message: [{}]", rpcMessage); 44 | } else { 45 | future.channel().close(); 46 | resultFuture.completeExceptionally(future.cause()); 47 | log.error("send failed:", future.cause()); 48 | } 49 | }); 50 | return new AsyncResult(resultFuture); 51 | } else { 52 | throw new RpcException("channel is not active. address=" + socketAddress); 53 | } 54 | } 55 | 56 | /** 57 | * 根据请求数据,构建 Rpc 通用信息结构 58 | * 59 | * @param request 请求 60 | * @return RpcMessage 61 | */ 62 | private RpcMessage buildRpcMessage(RpcRequest request) { 63 | ProtocolConfig protocolConfig = ConfigManager.getInstant().getProtocolConfig(); 64 | 65 | // 压缩类型 66 | String compressTypeName = protocolConfig.getCompressType(); 67 | CompressType compressType = CompressType.fromName(compressTypeName); 68 | 69 | // 序列化类型 70 | String serializeTypeName = protocolConfig.getSerializeType(); 71 | SerializeType serializeType = SerializeType.fromName(serializeTypeName); 72 | if (serializeType == null) { 73 | throw new IllegalStateException("serializeType " + serializeTypeName + " not support."); 74 | } 75 | 76 | return RpcMessage.builder() 77 | .messageType(MessageType.REQUEST.getValue()) 78 | .compressTye(compressType.getValue()) 79 | .serializeType(serializeType.getValue()) 80 | .requestId(MessageFormatConst.REQUEST_ID.getAndIncrement()) 81 | .data(request) 82 | .build(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/loadbalance/AbstractLoadBalance.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.loadbalance; 2 | 3 | import cn.hutool.core.collection.CollectionUtil; 4 | import com.ccx.rpc.common.url.URL; 5 | import com.ccx.rpc.core.dto.RpcRequest; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * 抽象的负载均衡 11 | * 12 | * @author chenchuxin 13 | * @date 2021/8/7 14 | */ 15 | public abstract class AbstractLoadBalance implements LoadBalance { 16 | 17 | @Override 18 | public URL select(List candidateUrls, RpcRequest request) { 19 | if (CollectionUtil.isEmpty(candidateUrls)) { 20 | return null; 21 | } 22 | if (candidateUrls.size() == 1) { 23 | return candidateUrls.get(0); 24 | } 25 | return doSelect(candidateUrls, request); 26 | } 27 | 28 | protected abstract URL doSelect(List candidateUrls, RpcRequest request); 29 | } 30 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/loadbalance/LoadBalance.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.loadbalance; 2 | 3 | import com.ccx.rpc.common.extension.SPI; 4 | import com.ccx.rpc.common.url.URL; 5 | import com.ccx.rpc.core.dto.RpcRequest; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * 负载均衡 11 | * 12 | * @author chenchuxin 13 | * @date 2021/8/7 14 | */ 15 | @SPI("round-robin") 16 | public interface LoadBalance { 17 | 18 | /** 19 | * 选择 20 | * 21 | * @param candidateUrls 候选的 URL 22 | * @param request 请求 23 | * @return 选择的 URL 24 | */ 25 | URL select(List candidateUrls, RpcRequest request); 26 | } 27 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/loadbalance/RandomLoadBalance.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.loadbalance; 2 | 3 | import cn.hutool.core.util.RandomUtil; 4 | import com.ccx.rpc.common.url.URL; 5 | import com.ccx.rpc.core.dto.RpcRequest; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * 随机负载均衡 11 | * 12 | * @author chenchuxin 13 | * @date 2021/8/7 14 | */ 15 | public class RandomLoadBalance extends AbstractLoadBalance { 16 | 17 | @Override 18 | protected URL doSelect(List candidateUrls, RpcRequest request) { 19 | int size = candidateUrls.size(); 20 | int index = RandomUtil.randomInt(size); 21 | return candidateUrls.get(index); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/loadbalance/RoundRobinLoadBalance.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.loadbalance; 2 | 3 | import com.ccx.rpc.common.url.URL; 4 | import com.ccx.rpc.core.dto.RpcRequest; 5 | 6 | import java.util.List; 7 | import java.util.concurrent.atomic.LongAdder; 8 | 9 | /** 10 | * 轮询 11 | * 12 | * @author chenchuxin 13 | * @date 2021/8/7 14 | */ 15 | public class RoundRobinLoadBalance extends AbstractLoadBalance { 16 | 17 | private final LongAdder curIndex = new LongAdder(); 18 | 19 | @Override 20 | protected URL doSelect(List candidateUrls, RpcRequest request) { 21 | int index = (int) (curIndex.longValue() % candidateUrls.size()); 22 | curIndex.increment(); 23 | return candidateUrls.get(index); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/proxy/RpcClientProxy.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.proxy; 2 | 3 | import com.ccx.rpc.common.extension.ExtensionLoader; 4 | import com.ccx.rpc.core.annotation.RpcReference; 5 | import com.ccx.rpc.core.config.ConfigManager; 6 | import com.ccx.rpc.core.dto.RpcRequest; 7 | import com.ccx.rpc.core.dto.RpcResponse; 8 | import com.ccx.rpc.core.dto.RpcResult; 9 | import com.ccx.rpc.core.faulttolerant.FaultTolerantInvoker; 10 | import lombok.SneakyThrows; 11 | 12 | import java.lang.reflect.InvocationHandler; 13 | import java.lang.reflect.Method; 14 | import java.lang.reflect.Proxy; 15 | 16 | /** 17 | * Rpc 调用方的代理 18 | * 19 | * @author chenchuxin 20 | * @date 2021/7/31 21 | */ 22 | public class RpcClientProxy implements InvocationHandler { 23 | 24 | private final RpcReference rpcReference; 25 | 26 | public RpcClientProxy(RpcReference rpcReference) { 27 | this.rpcReference = rpcReference; 28 | } 29 | 30 | /** 31 | * 获取代理类 32 | * 33 | * @param clazz 需要代理的接口类 34 | * @param 类型 35 | * @return 代理类 36 | */ 37 | @SuppressWarnings("unchecked") 38 | public T getProxy(Class clazz) { 39 | // TODO: 缓存,不然会生成很多代理类 40 | return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, this); 41 | } 42 | 43 | @Override 44 | @SneakyThrows 45 | public Object invoke(Object proxy, Method method, Object[] args) { 46 | // 调用 nettyClient 发请求 47 | RpcRequest request = RpcRequest.builder() 48 | .interfaceName(method.getDeclaringClass().getCanonicalName()) 49 | .methodName(method.getName()) 50 | .params(args) 51 | .paramTypes(method.getParameterTypes()) 52 | .version(rpcReference.version()) 53 | .build(); 54 | ExtensionLoader loader = ExtensionLoader.getLoader(FaultTolerantInvoker.class); 55 | FaultTolerantInvoker invoker = loader.getExtension(ConfigManager.getInstant().getClusterConfig().getFaultTolerant()); 56 | RpcResult rpcResult = invoker.invoke(request); 57 | return ((RpcResponse) rpcResult.getData()).getData(); 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/proxy/RpcServiceCache.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.proxy; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.ccx.rpc.common.consts.RpcException; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | import java.util.Map; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | 10 | /** 11 | * 服务缓存 12 | * 13 | * @author chenchuxin 14 | * @date 2021/8/1 15 | */ 16 | @Slf4j 17 | public class RpcServiceCache { 18 | 19 | private static final Map serviceMap = new ConcurrentHashMap<>(); 20 | 21 | public static void addService(String version, Object service) { 22 | Class[] interfaces = service.getClass().getInterfaces(); 23 | if (interfaces.length == 0) { 24 | throw new RpcException("add service error. service not implements interface. service=" + service.getClass().getName()); 25 | } 26 | String rpcServiceName; 27 | if (StrUtil.isBlank(version)) { 28 | rpcServiceName = interfaces[0].getCanonicalName(); 29 | } else { 30 | rpcServiceName = interfaces[0].getCanonicalName() + "_" + version; 31 | } 32 | serviceMap.putIfAbsent(rpcServiceName, service); 33 | log.info(StrUtil.format("add service. rpcServiceName={}, class={}", rpcServiceName, service.getClass())); 34 | } 35 | 36 | public static Object getService(String rpcServiceName) { 37 | Object service = serviceMap.get(rpcServiceName); 38 | if (service == null) { 39 | throw new RpcException("rpcService not found." + rpcServiceName); 40 | } 41 | return service; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/registry/AbstractRegistry.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.registry; 2 | 3 | import cn.hutool.core.collection.ConcurrentHashSet; 4 | import cn.hutool.core.lang.Assert; 5 | import com.ccx.rpc.common.consts.URLKeyConst; 6 | import com.ccx.rpc.common.url.URL; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | import java.util.List; 10 | import java.util.Map; 11 | import java.util.Set; 12 | import java.util.concurrent.ConcurrentHashMap; 13 | import java.util.stream.Collectors; 14 | 15 | import static com.ccx.rpc.core.registry.RegistryEvent.Type.*; 16 | 17 | /** 18 | * 抽象的注册中心 19 | * 20 | * @author chenchuxin 21 | * @date 2021/7/24 22 | */ 23 | @Slf4j 24 | public abstract class AbstractRegistry implements Registry { 25 | 26 | /** 27 | * 已注册的服务的本地缓存。{serviceName: [URL]} 28 | */ 29 | private final Map> registered = new ConcurrentHashMap<>(); 30 | 31 | /** 32 | * 本机注册的服务 33 | */ 34 | private static final Set myServiceURLs = new ConcurrentHashSet<>(); 35 | 36 | /** 37 | * 向注册中心注册服务 38 | * 39 | * @param url 注册者的信息 40 | */ 41 | protected abstract void doRegister(URL url); 42 | 43 | /** 44 | * 向注册中心取消注册服务 45 | * 46 | * @param url 注册者的信息 47 | */ 48 | protected abstract void doUnregister(URL url); 49 | 50 | /** 51 | * 查找注册的服务 52 | * 53 | * @param condition 查询条件 54 | * @return 符合查询条件的所有注册者 55 | */ 56 | protected abstract List doLookup(URL condition); 57 | 58 | /** 59 | * 向注册中心注册服务 60 | * 61 | * @param url 注册者的信息 62 | */ 63 | @Override 64 | public void register(URL url) { 65 | Assert.notNull(url, "register url == null"); 66 | doRegister(url); 67 | addToLocalCache(url); 68 | myServiceURLs.add(url); 69 | log.info("register: {}", url); 70 | } 71 | 72 | /** 73 | * 向注册中心取消注册服务 74 | * 75 | * @param url 注册者的信息 76 | */ 77 | @Override 78 | public void unregister(URL url) { 79 | Assert.notNull(url, "register url == null"); 80 | doUnregister(url); 81 | removeFromLocalCache(url); 82 | myServiceURLs.remove(url); 83 | log.info("unregister: {}", url); 84 | } 85 | 86 | /** 87 | * 查找注册的服务 88 | * 89 | * @param condition 查询条件,包含接口类型 90 | * @return 符合查询条件的所有注册者 91 | */ 92 | @Override 93 | public List lookup(URL condition) { 94 | String serviceName = getServiceNameFromUrl(condition); 95 | if (registered.containsKey(serviceName)) { 96 | return registered.get(serviceName).stream().map(URL::valueOf).collect(Collectors.toList()); 97 | } 98 | List urls = reset(condition); 99 | log.info("lookup: {}", urls); 100 | return urls; 101 | } 102 | 103 | /** 104 | * 取消所有本机的服务,用于关机的时候 105 | */ 106 | @Override 107 | public void unregisterAllMyService() { 108 | log.info("unregisterAllMyService. myServiceURLs:{}", myServiceURLs); 109 | for (URL url : myServiceURLs) { 110 | unregister(url); 111 | } 112 | } 113 | 114 | /** 115 | * 重置。真实拿出注册信息,然后加到缓存中。 116 | * 117 | * @param condition 118 | * @return 119 | */ 120 | public List reset(URL condition) { 121 | // 获取服务名 122 | String serviceName = getServiceNameFromUrl(condition); 123 | // 将原来注册信息本地缓存删掉 124 | registered.remove(serviceName); 125 | // 重新从注册中心获取 126 | List urls = doLookup(condition); 127 | for (URL url : urls) { 128 | // 将所有 Provider 添加到本地缓存 129 | addToLocalCache(url); 130 | } 131 | log.info("reset: {}", urls); 132 | return urls; 133 | } 134 | 135 | /** 136 | * 触发事件 137 | * 138 | * @param event 事件 139 | */ 140 | public final void triggerEvent(RegistryEvent event) { 141 | RegistryEvent.Type type = event.getType(); 142 | String data = event.getData(); 143 | String oldData = event.getOldData(); 144 | log.info("triggerEvent. event={}", event); 145 | if (type == CREATED) { 146 | // 新增节点 147 | if (data != null) { 148 | addToLocalCache(URL.valueOf(data)); 149 | } 150 | } else if (type == DELETED) { 151 | if (oldData != null) { 152 | // 删除节点 153 | removeFromLocalCache(URL.valueOf(oldData)); 154 | } 155 | } else if (type == CHANGED) { 156 | // 修改节点 157 | if (oldData != null) { 158 | removeFromLocalCache(URL.valueOf(oldData)); 159 | } 160 | if (data != null) { 161 | addToLocalCache(URL.valueOf(data)); 162 | } 163 | } 164 | } 165 | 166 | /** 167 | * 从 URL 中获取服务名 168 | */ 169 | public String getServiceNameFromUrl(URL url) { 170 | return url.getParam(URLKeyConst.INTERFACE, url.getPath()); 171 | } 172 | 173 | /** 174 | * 添加到本地缓存 175 | */ 176 | private void addToLocalCache(URL url) { 177 | String serviceName = getServiceNameFromUrl(url); 178 | if (!registered.containsKey(serviceName)) { 179 | registered.put(serviceName, new ConcurrentHashSet<>()); 180 | } 181 | registered.get(serviceName).add(url.toFullString()); 182 | } 183 | 184 | /** 185 | * 从本地缓存中删除 186 | */ 187 | private void removeFromLocalCache(URL url) { 188 | String serviceName = getServiceNameFromUrl(url); 189 | if (registered.containsKey(serviceName)) { 190 | registered.get(serviceName).remove(url.toFullString()); 191 | } 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/registry/Registry.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.registry; 2 | 3 | import com.ccx.rpc.common.url.URL; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * 注册中心 9 | * 10 | * @author chenchuxin 11 | * @date 2021/7/18 12 | */ 13 | public interface Registry { 14 | 15 | /** 16 | * 向注册中心注册服务 17 | * 18 | * @param url 注册者的信息 19 | */ 20 | void register(URL url); 21 | 22 | /** 23 | * 向注册中心取消注册服务 24 | * 25 | * @param url 注册者的信息 26 | */ 27 | void unregister(URL url); 28 | 29 | /** 30 | * 查找注册的服务 31 | * 32 | * @param condition 查询条件 33 | * @return 符合查询条件的所有注册者 34 | */ 35 | List lookup(URL condition); 36 | 37 | /** 38 | * 取消所有本机的服务,用于关机的时候 39 | */ 40 | void unregisterAllMyService(); 41 | } 42 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/registry/RegistryEvent.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.registry; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | 6 | /** 7 | * 注册中心事件 8 | * 9 | * @author chenchuxin 10 | * @date 2021/8/22 11 | */ 12 | @Data 13 | @AllArgsConstructor 14 | public class RegistryEvent { 15 | 16 | /** 17 | * 事件类型 18 | */ 19 | public enum Type { 20 | /** 21 | * 节点已创建 22 | */ 23 | CREATED, 24 | /** 25 | * 节点已删除 26 | */ 27 | DELETED, 28 | /** 29 | * 节点已更改 30 | */ 31 | CHANGED 32 | } 33 | 34 | /** 35 | * 事件类型 36 | */ 37 | private Type type; 38 | 39 | /** 40 | * 旧数据 41 | */ 42 | private String oldData; 43 | 44 | /** 45 | * 当前数据 46 | */ 47 | private String data; 48 | } 49 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/registry/RegistryFactory.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.registry; 2 | 3 | import com.ccx.rpc.common.consts.URLKeyConst; 4 | import com.ccx.rpc.common.extension.Adaptive; 5 | import com.ccx.rpc.common.extension.SPI; 6 | import com.ccx.rpc.common.url.URL; 7 | 8 | /** 9 | * 注册中心工厂 10 | * 11 | * @author chenchuxin 12 | * @date 2021/7/18 13 | */ 14 | @SPI 15 | public interface RegistryFactory { 16 | 17 | /** 18 | * 获取注册中心 19 | * 20 | * @param url 注册中心的配置,例如注册中心的地址。会自动根据协议获取注册中心实例 21 | * @return 如果协议类型跟注册中心匹配上了,返回对应的配置中心实例 22 | */ 23 | @Adaptive(URLKeyConst.PROTOCOL) 24 | Registry getRegistry(URL url); 25 | } 26 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/registry/local/LocalRegistry.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.registry.local; 2 | 3 | import cn.hutool.core.collection.ConcurrentHashSet; 4 | import com.ccx.rpc.common.url.URL; 5 | import com.ccx.rpc.core.registry.AbstractRegistry; 6 | import lombok.extern.slf4j.Slf4j; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Set; 11 | 12 | /** 13 | * 本地注册中心,测试用 14 | * 15 | * @author chenchuxin 16 | * @date 2021/7/18 17 | */ 18 | @Slf4j 19 | public class LocalRegistry extends AbstractRegistry { 20 | 21 | private static final Set urls = new ConcurrentHashSet<>(); 22 | 23 | @Override 24 | public void doRegister(URL url) { 25 | urls.add(url); 26 | log.info("doRegister:" + url); 27 | } 28 | 29 | @Override 30 | public void doUnregister(URL url) { 31 | urls.remove(url); 32 | log.info("doUnregister:" + url); 33 | } 34 | 35 | @Override 36 | public List doLookup(URL condition) { 37 | log.info("doLookup:" + urls); 38 | return new ArrayList<>(urls); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/registry/local/LocalRegistryFactory.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.registry.local; 2 | 3 | import com.ccx.rpc.core.registry.Registry; 4 | import com.ccx.rpc.core.registry.RegistryFactory; 5 | import com.ccx.rpc.common.url.URL; 6 | 7 | /** 8 | * @author chenchuxin 9 | * @date 2021/7/18 10 | */ 11 | public class LocalRegistryFactory implements RegistryFactory { 12 | 13 | @Override 14 | public Registry getRegistry(URL url) { 15 | return new LocalRegistry(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/registry/zk/CuratorZkClient.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.registry.zk; 2 | 3 | import com.ccx.rpc.common.consts.URLKeyConst; 4 | import com.ccx.rpc.common.url.URL; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.apache.commons.lang.StringUtils; 7 | import org.apache.curator.framework.CuratorFramework; 8 | import org.apache.curator.framework.CuratorFrameworkFactory; 9 | import org.apache.curator.framework.recipes.cache.CuratorCache; 10 | import org.apache.curator.framework.recipes.cache.CuratorCacheListener; 11 | import org.apache.curator.retry.RetryNTimes; 12 | import org.apache.zookeeper.CreateMode; 13 | import org.apache.zookeeper.KeeperException; 14 | 15 | import java.util.Collections; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.concurrent.ConcurrentHashMap; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | /** 22 | * curator zk 客户端 23 | * 24 | * @author chenchuxin 25 | * @date 2021/7/18 26 | */ 27 | @Slf4j 28 | public class CuratorZkClient { 29 | /** 30 | * 默认连接超时毫秒数 31 | */ 32 | private static final int DEFAULT_CONNECTION_TIMEOUT_MS = 5_000; 33 | /** 34 | * 默认 session 超时毫秒数 35 | */ 36 | private static final int DEFAULT_SESSION_TIMEOUT_MS = 60_000; 37 | /** 38 | * session 超时时间 39 | */ 40 | private static final String SESSION_TIMEOUT_KEY = "zk.sessionTimeoutMs"; 41 | /** 42 | * 连接重试次数 43 | */ 44 | private static final int RETRY_TIMES = 3; 45 | /** 46 | * 连接重试睡眠毫秒数 47 | */ 48 | private static final int RETRY_SLEEP_MS = 1000; 49 | /** 50 | * 根目录 51 | */ 52 | private static final String ROOT_PATH = "/ccx-rpc"; 53 | /** 54 | * zk 客户端 55 | */ 56 | private final CuratorFramework client; 57 | /** 58 | * 监听器 {path: 监听器} 59 | */ 60 | private static final Map LISTENER_MAP = new ConcurrentHashMap<>(); 61 | 62 | public CuratorZkClient(URL url) { 63 | int timeout = url.getIntParam(URLKeyConst.TIMEOUT, DEFAULT_CONNECTION_TIMEOUT_MS); 64 | int sessionTimeout = url.getIntParam(SESSION_TIMEOUT_KEY, DEFAULT_SESSION_TIMEOUT_MS); 65 | CuratorFrameworkFactory.Builder builder = CuratorFrameworkFactory.builder() 66 | .connectString(url.getAddress()) 67 | .retryPolicy(new RetryNTimes(RETRY_TIMES, RETRY_SLEEP_MS)) 68 | .connectionTimeoutMs(timeout) 69 | .sessionTimeoutMs(sessionTimeout); 70 | if (StringUtils.isNotEmpty(url.getUsername()) || StringUtils.isNotEmpty(url.getPassword())) { 71 | String authority = StringUtils.defaultIfEmpty(url.getUsername(), "") 72 | + ":" + StringUtils.defaultIfEmpty(url.getPassword(), ""); 73 | builder.authorization("digest", authority.getBytes()); 74 | } 75 | client = builder.build(); 76 | client.start(); 77 | try { 78 | client.blockUntilConnected(timeout, TimeUnit.MILLISECONDS); 79 | } catch (InterruptedException e) { 80 | throw new RuntimeException("Time out waiting to connect to zookeeper! Please check the zookeeper config."); 81 | } 82 | } 83 | 84 | /** 85 | * 创建节点 86 | * 87 | * @param path 路径,如果没有加上根目录,会自动加上根目录 88 | * @param createMode 节点模式 89 | */ 90 | public void createNode(String path, CreateMode createMode) { 91 | try { 92 | client.create().creatingParentsIfNeeded().withMode(createMode).forPath(buildPath(path)); 93 | } catch (KeeperException.NodeExistsException e) { 94 | log.warn("ZNode " + path + " already exists."); 95 | } catch (Exception e) { 96 | throw new IllegalStateException(e.getMessage(), e); 97 | } 98 | } 99 | 100 | /** 101 | * 创建永久节点 102 | * 103 | * @param path 路径,如果没有加上根目录,会自动加上根目录 104 | */ 105 | public void createPersistentNode(String path) { 106 | createNode(path, CreateMode.PERSISTENT); 107 | } 108 | 109 | /** 110 | * 创建临时节点 111 | * 112 | * @param path 路径,如果没有加上根目录,会自动加上根目录 113 | */ 114 | public void createEphemeralNode(String path) { 115 | createNode(path, CreateMode.EPHEMERAL); 116 | } 117 | 118 | /** 119 | * 删除节点 120 | * 121 | * @param path 路径,如果没有加上根目录,会自动加上根目录 122 | */ 123 | public void removeNode(String path) { 124 | try { 125 | client.delete().forPath(buildPath(path)); 126 | } catch (KeeperException.NoNodeException ignored) { 127 | } catch (Exception exception) { 128 | exception.printStackTrace(); 129 | } 130 | } 131 | 132 | /** 133 | * 获取路径下的所有子节点 134 | * 135 | * @param path 要搜索的路径 136 | * @return 节点不存在返回空列表 137 | */ 138 | public List getChildren(String path) { 139 | try { 140 | return client.getChildren().forPath(buildPath(path)); 141 | } catch (KeeperException.NoNodeException e) { 142 | return Collections.emptyList(); 143 | } catch (Exception e) { 144 | throw new IllegalStateException(e.getMessage(), e); 145 | } 146 | } 147 | 148 | /** 149 | * 添加监听者 150 | * 151 | * @param path 相对路径 152 | * @param listener 监听者 153 | */ 154 | public void addListener(String path, CuratorCacheListener listener) { 155 | String fullPath = buildPath(path); 156 | if (LISTENER_MAP.containsKey(fullPath)) { 157 | return; 158 | } 159 | CuratorCache curatorCache = CuratorCache.build(client, fullPath); 160 | LISTENER_MAP.put(fullPath, curatorCache); 161 | curatorCache.listenable().addListener(listener); 162 | curatorCache.start(); 163 | } 164 | 165 | /** 166 | * 构建完整的路径,用于存 zk 167 | * 168 | * @param path 相对或者路径 169 | * @return 如果路径不包含根目录,加上根目录 170 | */ 171 | private String buildPath(String path) { 172 | if (path.startsWith(ROOT_PATH)) { 173 | return path; 174 | } 175 | if (path.startsWith("/")) { 176 | return ROOT_PATH + path; 177 | } 178 | return ROOT_PATH + "/" + path; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/registry/zk/ZkRegistry.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.registry.zk; 2 | 3 | import cn.hutool.core.net.URLDecoder; 4 | import cn.hutool.core.net.URLEncoder; 5 | import com.ccx.rpc.common.consts.RegistryConst; 6 | import com.ccx.rpc.common.url.URL; 7 | import com.ccx.rpc.common.url.URLParser; 8 | import com.ccx.rpc.core.registry.AbstractRegistry; 9 | import lombok.extern.slf4j.Slf4j; 10 | 11 | import java.nio.charset.Charset; 12 | import java.util.List; 13 | import java.util.stream.Collectors; 14 | 15 | /** 16 | * zk 注册中心。
17 | * 这里面会引入 curator,最好是新建个 Module 的,不过现在代码还太简单,不需要 18 | * 19 | * @author chenchuxin 20 | * @date 2021/7/18 21 | */ 22 | @Slf4j 23 | public class ZkRegistry extends AbstractRegistry { 24 | 25 | private final CuratorZkClient zkClient; 26 | private static final URLEncoder urlEncoder = URLEncoder.createPathSegment(); 27 | private static final Charset charset = Charset.defaultCharset(); 28 | 29 | public ZkRegistry(URL url) { 30 | zkClient = new CuratorZkClient(url); 31 | } 32 | 33 | @Override 34 | public void doRegister(URL url) { 35 | zkClient.createEphemeralNode(toUrlPath(url)); 36 | watch(url); 37 | } 38 | 39 | @Override 40 | public void doUnregister(URL url) { 41 | zkClient.removeNode(toUrlPath(url)); 42 | watch(url); 43 | } 44 | 45 | @Override 46 | public List doLookup(URL condition) { 47 | List children = zkClient.getChildren(toServicePath(condition)); 48 | List urls = children.stream() 49 | .map(s -> URLParser.toURL(URLDecoder.decode(s, charset))) 50 | .collect(Collectors.toList()); 51 | // 获取到的每个都添加监听 52 | for (URL url : urls) { 53 | watch(url); 54 | } 55 | return urls; 56 | } 57 | 58 | /** 59 | * 转成全路径,包括节点内容 60 | */ 61 | private String toUrlPath(URL url) { 62 | return toServicePath(url) + "/" + urlEncoder.encode(url.toFullString(), charset); 63 | } 64 | 65 | /** 66 | * 转成服务的路径,例如:/ccx-rpc/com.ccx.rpc.demo.service.api.UserService/providers 67 | */ 68 | private String toServicePath(URL url) { 69 | return getServiceNameFromUrl(url) + "/" + RegistryConst.PROVIDERS_CATEGORY; 70 | } 71 | 72 | /** 73 | * 监听 74 | */ 75 | private void watch(URL url) { 76 | String path = toServicePath(url); 77 | zkClient.addListener(path, (type, oldData, data) -> { 78 | log.info("watch event. type={}, oldData={}, data={}", type, oldData, data); 79 | reset(url); 80 | }); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/registry/zk/ZkRegistryFactory.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.registry.zk; 2 | 3 | import com.ccx.rpc.core.registry.Registry; 4 | import com.ccx.rpc.core.registry.RegistryFactory; 5 | import com.ccx.rpc.common.url.URL; 6 | 7 | import java.util.Map; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | 10 | /** 11 | * zk 注册中心工厂 12 | * 13 | * @author chenchuxin 14 | * @date 2021/7/18 15 | */ 16 | public class ZkRegistryFactory implements RegistryFactory { 17 | 18 | private static final Map cache = new ConcurrentHashMap<>(); 19 | 20 | @Override 21 | public Registry getRegistry(URL url) { 22 | if (cache.containsKey(url)) { 23 | return cache.get(url); 24 | } 25 | ZkRegistry zkRegistry = new ZkRegistry(url); 26 | cache.putIfAbsent(url, zkRegistry); 27 | return cache.get(url); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/remoting/client/netty/NettyClient.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.remoting.client.netty; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.ccx.rpc.common.consts.RpcException; 5 | import com.ccx.rpc.core.remoting.codec.RpcMessageDecoder; 6 | import com.ccx.rpc.core.remoting.codec.RpcMessageEncoder; 7 | import io.netty.bootstrap.Bootstrap; 8 | import io.netty.channel.*; 9 | import io.netty.channel.epoll.Epoll; 10 | import io.netty.channel.epoll.EpollSocketChannel; 11 | import io.netty.channel.nio.NioEventLoopGroup; 12 | import io.netty.channel.socket.SocketChannel; 13 | import io.netty.channel.socket.nio.NioSocketChannel; 14 | import io.netty.handler.logging.LogLevel; 15 | import io.netty.handler.logging.LoggingHandler; 16 | import io.netty.handler.timeout.IdleStateHandler; 17 | import lombok.extern.slf4j.Slf4j; 18 | 19 | import java.net.SocketAddress; 20 | import java.util.Map; 21 | import java.util.concurrent.CompletableFuture; 22 | import java.util.concurrent.ConcurrentHashMap; 23 | import java.util.concurrent.TimeUnit; 24 | 25 | /** 26 | * @author chenchuxin 27 | * @date 2021/7/31 28 | */ 29 | @Slf4j 30 | public class NettyClient { 31 | 32 | private final Bootstrap bootstrap; 33 | 34 | /** 35 | * {地址:连接的channel} 36 | */ 37 | private static final Map CHANNEL_MAP = new ConcurrentHashMap<>(); 38 | 39 | private static NettyClient instance = null; 40 | 41 | public static NettyClient getInstance() { 42 | if (instance == null) { 43 | synchronized (NettyClient.class) { 44 | if (instance == null) { 45 | instance = new NettyClient(); 46 | } 47 | } 48 | } 49 | return instance; 50 | } 51 | 52 | private NettyClient() { 53 | bootstrap = new Bootstrap() 54 | .group(new NioEventLoopGroup()) 55 | .channel(Epoll.isAvailable() ? EpollSocketChannel.class : NioSocketChannel.class) 56 | .handler(new LoggingHandler(LogLevel.INFO)) 57 | .option(ChannelOption.SO_KEEPALIVE, Boolean.TRUE) 58 | .option(ChannelOption.TCP_NODELAY, Boolean.TRUE) 59 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) 60 | .handler(new ChannelInitializer() { 61 | @Override 62 | protected void initChannel(SocketChannel ch) { 63 | ChannelPipeline p = ch.pipeline(); 64 | // 设定 IdleStateHandler 心跳检测每 5 秒进行一次写检测 65 | // write()方法超过 5 秒没调用,就调用 userEventTrigger 66 | p.addLast(new IdleStateHandler(0, 5, 0, TimeUnit.SECONDS)); 67 | p.addLast(new RpcMessageEncoder()); 68 | p.addLast(new RpcMessageDecoder()); 69 | p.addLast(new NettyClientHandler()); 70 | } 71 | }); 72 | } 73 | 74 | /** 75 | * 获取和指定地址连接的 channel,如果获取不到,则连接 76 | * 77 | * @param address 指定要连接的地址 78 | * @return channel 79 | */ 80 | public Channel getChannel(SocketAddress address) { 81 | Channel channel = CHANNEL_MAP.get(address); 82 | if (channel == null || !channel.isActive()) { 83 | channel = connect(address); 84 | CHANNEL_MAP.put(address, channel); 85 | } 86 | return channel; 87 | } 88 | 89 | /** 90 | * 连接地址 91 | * 92 | * @param address 地址 93 | * @return channel 94 | */ 95 | private Channel connect(SocketAddress address) { 96 | try { 97 | log.info("Try to connect server [{}]", address); 98 | CompletableFuture completableFuture = new CompletableFuture<>(); 99 | ChannelFuture connect = bootstrap.connect(address); 100 | connect.addListener((ChannelFutureListener) future -> { 101 | if (future.isSuccess()) { 102 | completableFuture.complete(future.channel()); 103 | } else { 104 | throw new IllegalStateException(StrUtil.format("connect fail. address:", address)); 105 | } 106 | }); 107 | return completableFuture.get(10, TimeUnit.SECONDS); 108 | } catch (Exception ex) { 109 | throw new RpcException(address + " connect fail.", ex); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/remoting/client/netty/NettyClientHandler.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.remoting.client.netty; 2 | 3 | import com.ccx.rpc.core.consts.SerializeType; 4 | import com.ccx.rpc.core.consts.CompressType; 5 | import com.ccx.rpc.core.consts.MessageFormatConst; 6 | import com.ccx.rpc.core.consts.MessageType; 7 | import com.ccx.rpc.core.dto.RpcMessage; 8 | import com.ccx.rpc.core.dto.RpcResponse; 9 | import io.netty.channel.Channel; 10 | import io.netty.channel.ChannelFutureListener; 11 | import io.netty.channel.ChannelHandlerContext; 12 | import io.netty.channel.SimpleChannelInboundHandler; 13 | import io.netty.handler.timeout.IdleState; 14 | import io.netty.handler.timeout.IdleStateEvent; 15 | import io.netty.util.ReferenceCountUtil; 16 | import lombok.extern.slf4j.Slf4j; 17 | 18 | /** 19 | * @author chenchuxin 20 | * @date 2021/7/31 21 | */ 22 | @Slf4j 23 | public class NettyClientHandler extends SimpleChannelInboundHandler { 24 | @Override 25 | protected void channelRead0(ChannelHandlerContext context, RpcMessage requestMsg) { 26 | try { 27 | log.info("client receive msg: [{}]", requestMsg); 28 | if (requestMsg.getMessageType() == MessageType.RESPONSE.getValue()) { 29 | RpcResponse response = (RpcResponse) requestMsg.getData(); 30 | UnprocessedRequests.complete(response); 31 | } 32 | } finally { 33 | ReferenceCountUtil.release(requestMsg); 34 | } 35 | } 36 | 37 | @Override 38 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { 39 | if (evt instanceof IdleStateEvent) { 40 | // 心跳 41 | IdleState state = ((IdleStateEvent) evt).state(); 42 | if (state == IdleState.WRITER_IDLE) { 43 | log.info("write idle happen [{}]", ctx.channel().remoteAddress()); 44 | Channel channel = ctx.channel(); 45 | RpcMessage rpcMessage = new RpcMessage(); 46 | rpcMessage.setSerializeType(SerializeType.PROTOSTUFF.getValue()); 47 | rpcMessage.setCompressTye(CompressType.DUMMY.getValue()); 48 | rpcMessage.setMessageType(MessageType.HEARTBEAT.getValue()); 49 | channel.writeAndFlush(rpcMessage).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); 50 | } 51 | } else { 52 | super.userEventTriggered(ctx, evt); 53 | } 54 | } 55 | 56 | /** 57 | * Called when an exception occurs in processing a client message 58 | */ 59 | @Override 60 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 61 | log.error("client catch exception:", cause); 62 | cause.printStackTrace(); 63 | ctx.close(); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/remoting/client/netty/UnprocessedRequests.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.remoting.client.netty; 2 | 3 | import cn.hutool.json.JSONUtil; 4 | import com.ccx.rpc.core.dto.RpcResponse; 5 | 6 | import java.util.Map; 7 | import java.util.concurrent.CompletableFuture; 8 | import java.util.concurrent.ConcurrentHashMap; 9 | 10 | /** 11 | * 未处理的请求 12 | * 13 | * @author chenchuxin 14 | * @date 2021/7/31 15 | */ 16 | public class UnprocessedRequests { 17 | private static final Map>> FUTURE_MAP = new ConcurrentHashMap<>(); 18 | 19 | public static void put(long requestId, CompletableFuture> future) { 20 | FUTURE_MAP.put(requestId, future); 21 | } 22 | 23 | /** 24 | * 完成响应 25 | * 26 | * @param rpcResponse 响应内容 27 | */ 28 | public static void complete(RpcResponse rpcResponse) { 29 | CompletableFuture> future = FUTURE_MAP.remove(rpcResponse.getRequestId()); 30 | if (future != null) { 31 | future.complete(rpcResponse); 32 | } else { 33 | throw new IllegalStateException("future is null. rpcResponse=" + JSONUtil.toJsonStr(rpcResponse)); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/remoting/codec/RpcMessageDecoder.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.remoting.codec; 2 | 3 | import com.ccx.rpc.common.extension.ExtensionLoader; 4 | import com.ccx.rpc.core.compress.Compressor; 5 | import com.ccx.rpc.core.consts.SerializeType; 6 | import com.ccx.rpc.core.consts.CompressType; 7 | import com.ccx.rpc.core.consts.MessageType; 8 | import com.ccx.rpc.core.dto.RpcMessage; 9 | import com.ccx.rpc.core.dto.RpcRequest; 10 | import com.ccx.rpc.core.dto.RpcResponse; 11 | import com.ccx.rpc.core.serialize.Serializer; 12 | import io.netty.buffer.ByteBuf; 13 | import io.netty.channel.ChannelHandlerContext; 14 | import io.netty.handler.codec.LengthFieldBasedFrameDecoder; 15 | import lombok.extern.slf4j.Slf4j; 16 | 17 | import java.util.Arrays; 18 | 19 | import static com.ccx.rpc.core.consts.MessageFormatConst.*; 20 | 21 | /** 22 | *

23 | * 自定义协议解码器 24 | *

25 | *

 26 |  *   0   1   2       3   4   5   6   7           8        9        10   11  12  13  14  15  16  17  18
 27 |  *   +---+---+-------+---+---+---+---+-----------+---------+--------+---+---+---+---+---+---+---+---+
 28 |  *   | magic |version|  full length  |messageType|serialize|compress|           RequestId           |
 29 |  *   +---+---+-------+---+---+---+---+-----------+---------+--------+---+---+---+---+---+---+---+---+
 30 |  *   |                                                                                              |
 31 |  *   |                                         body                                                 |
 32 |  *   |                                                                                              |
 33 |  *   |                                        ... ...                                               |
 34 |  *   +----------------------------------------------------------------------------------------------+
 35 |  *   2B magic(魔法数)
 36 |  *   1B version(版本)
 37 |  *   4B full length(消息长度)
 38 |  *   1B messageType(消息类型)
 39 |  *   1B serialize(序列化类型)
 40 |  *   1B compress(压缩类型)
 41 |  *   8B requestId(请求的Id)
 42 |  *   body(object类型数据)
 43 |  * 
44 | * 45 | * @author chenchuxin 46 | * @date 2021/7/25 47 | */ 48 | @Slf4j 49 | public class RpcMessageDecoder extends LengthFieldBasedFrameDecoder { 50 | public RpcMessageDecoder() { 51 | super( 52 | // 最大的长度,如果超过,会直接丢弃 53 | MAX_FRAME_LENGTH, 54 | // 描述长度的字段[4B full length(消息长度)]在哪个位置:在 [2B magic(魔数)]、[1B version(版本)] 后面 55 | MAGIC_LENGTH + VERSION_LENGTH, 56 | // 描述长度的字段[4B full length(消息长度)]本身的长度,也就是 4B 啦 57 | FULL_LENGTH_LENGTH, 58 | // LengthFieldBasedFrameDecoder 拿到消息长度之后,还会加上 [4B full length(消息长度)] 字段前面的长度 59 | // 因为我们的消息长度包含了这部分了,所以需要减回去 60 | -(MAGIC_LENGTH + VERSION_LENGTH + FULL_LENGTH_LENGTH), 61 | // initialBytesToStrip: 去除哪个位置前面的数据。因为我们还需要检测 魔数 和 版本号,所以不能去除 62 | 0); 63 | } 64 | 65 | @Override 66 | protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { 67 | Object decoded = super.decode(ctx, in); 68 | if (decoded instanceof ByteBuf) { 69 | ByteBuf frame = (ByteBuf) decoded; 70 | if (frame.readableBytes() >= HEADER_LENGTH) { 71 | try { 72 | return decodeFrame(frame); 73 | } catch (Exception ex) { 74 | log.error("Decode frame error.", ex); 75 | } finally { 76 | frame.release(); 77 | } 78 | } 79 | } 80 | return decoded; 81 | } 82 | 83 | /** 84 | * 业务解码 85 | */ 86 | private RpcMessage decodeFrame(ByteBuf in) { 87 | readAndCheckMagic(in); 88 | readAndCheckVersion(in); 89 | int fullLength = in.readInt(); 90 | byte messageType = in.readByte(); 91 | byte codec = in.readByte(); 92 | byte compress = in.readByte(); 93 | long requestId = in.readLong(); 94 | 95 | RpcMessage rpcMessage = RpcMessage.builder() 96 | .serializeType(codec) 97 | .compressTye(compress) 98 | .requestId(requestId) 99 | .messageType(messageType) 100 | .build(); 101 | 102 | if (messageType == MessageType.HEARTBEAT.getValue()) { 103 | return rpcMessage; 104 | } 105 | 106 | int bodyLength = fullLength - HEADER_LENGTH; 107 | if (bodyLength == 0) { 108 | return rpcMessage; 109 | } 110 | 111 | byte[] bodyBytes = new byte[bodyLength]; 112 | in.readBytes(bodyBytes); 113 | // 解压 114 | CompressType compressType = CompressType.fromValue(compress); 115 | Compressor compressor = ExtensionLoader.getLoader(Compressor.class).getExtension(compressType.getName()); 116 | byte[] decompressedBytes = compressor.decompress(bodyBytes); 117 | 118 | // 反序列化 119 | SerializeType serializeType = SerializeType.fromValue(codec); 120 | if (serializeType == null) { 121 | throw new IllegalArgumentException("unknown codec type:" + codec); 122 | } 123 | Serializer serializer = ExtensionLoader.getLoader(Serializer.class).getExtension(serializeType.getName()); 124 | Class clazz = messageType == MessageType.REQUEST.getValue() ? RpcRequest.class : RpcResponse.class; 125 | Object object = serializer.deserialize(decompressedBytes, clazz); 126 | rpcMessage.setData(object); 127 | return rpcMessage; 128 | } 129 | 130 | /** 131 | * 读取并检查版本 132 | */ 133 | private void readAndCheckVersion(ByteBuf in) { 134 | byte version = in.readByte(); 135 | if (version != VERSION) { 136 | throw new IllegalArgumentException("Unknown version: " + version); 137 | } 138 | } 139 | 140 | /** 141 | * 读取并检查魔数 142 | */ 143 | private void readAndCheckMagic(ByteBuf in) { 144 | byte[] bytes = new byte[MAGIC_LENGTH]; 145 | in.readBytes(bytes); 146 | for (int i = 0; i < bytes.length; i++) { 147 | if (bytes[i] != MAGIC[i]) { 148 | throw new IllegalArgumentException("Unknown magic: " + Arrays.toString(bytes)); 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/remoting/codec/RpcMessageEncoder.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.remoting.codec; 2 | 3 | import com.ccx.rpc.common.extension.ExtensionLoader; 4 | import com.ccx.rpc.core.compress.Compressor; 5 | import com.ccx.rpc.core.consts.SerializeType; 6 | import com.ccx.rpc.core.consts.CompressType; 7 | import com.ccx.rpc.core.consts.MessageFormatConst; 8 | import com.ccx.rpc.core.consts.MessageType; 9 | import com.ccx.rpc.core.dto.RpcMessage; 10 | import com.ccx.rpc.core.serialize.Serializer; 11 | import io.netty.buffer.ByteBuf; 12 | import io.netty.channel.ChannelHandlerContext; 13 | import io.netty.handler.codec.MessageToByteEncoder; 14 | 15 | /** 16 | *

17 | * 自定义协议编码器 18 | *

19 | *

 20 |  *   0     1     2       3    4    5    6    7           8        9        10   11   12   13   14   15   16   17   18
 21 |  *   +-----+-----+-------+----+----+----+----+-----------+---------+--------+----+----+----+----+----+----+----+---+
 22 |  *   |   magic   |version|    full length    |messageType|serialize|compress|              RequestId               |
 23 |  *   +-----+-----+-------+----+----+----+----+-----------+----- ---+--------+----+----+----+----+----+----+----+---+
 24 |  *   |                                                                                                             |
 25 |  *   |                                         body                                                                |
 26 |  *   |                                                                                                             |
 27 |  *   |                                        ... ...                                                              |
 28 |  *   +-------------------------------------------------------------------------------------------------------------+
 29 |  *   2B magic(魔数)
 30 |  *   1B version(版本)
 31 |  *   4B full length(消息长度)
 32 |  *   1B messageType(消息类型)
 33 |  *   1B serialize(序列化类型)
 34 |  *   1B compress(压缩类型)
 35 |  *   8B requestId(请求的Id)
 36 |  *   body(object类型数据)
 37 |  * 
38 | * 39 | * @author chenchuxin 40 | * @date 2021/7/25 41 | */ 42 | public class RpcMessageEncoder extends MessageToByteEncoder { 43 | 44 | @Override 45 | protected void encode(ChannelHandlerContext ctx, RpcMessage rpcMessage, ByteBuf out) { 46 | // 2B magic code(魔数) 47 | out.writeBytes(MessageFormatConst.MAGIC); 48 | // 1B version(版本) 49 | out.writeByte(MessageFormatConst.VERSION); 50 | // 4B full length(消息长度). 总长度先空着,后面填。 51 | out.writerIndex(out.writerIndex() + MessageFormatConst.FULL_LENGTH_LENGTH); 52 | // 1B messageType(消息类型) 53 | out.writeByte(rpcMessage.getMessageType()); 54 | // 1B codec(序列化类型) 55 | out.writeByte(rpcMessage.getSerializeType()); 56 | // 1B compress(压缩类型) 57 | out.writeByte(rpcMessage.getCompressTye()); 58 | // 8B requestId(请求的Id) 59 | out.writeLong(rpcMessage.getRequestId()); 60 | // 写 body,返回 body 长度 61 | int bodyLength = writeBody(rpcMessage, out); 62 | 63 | // 当前写指针 64 | int writerIndex = out.writerIndex(); 65 | out.writerIndex(MessageFormatConst.MAGIC_LENGTH + MessageFormatConst.VERSION_LENGTH); 66 | // 4B full length(消息长度) 67 | out.writeInt(MessageFormatConst.HEADER_LENGTH + bodyLength); 68 | // 写指针复原 69 | out.writerIndex(writerIndex); 70 | } 71 | 72 | /** 73 | * 写 body 74 | * 75 | * @return body 长度 76 | */ 77 | private int writeBody(RpcMessage rpcMessage, ByteBuf out) { 78 | byte messageType = rpcMessage.getMessageType(); 79 | // 如果是 ping、pong 心跳类型的,没有 body,直接返回头部长度 80 | if (messageType == MessageType.HEARTBEAT.getValue()) { 81 | return 0; 82 | } 83 | 84 | // 序列化器 85 | SerializeType serializeType = SerializeType.fromValue(rpcMessage.getSerializeType()); 86 | if (serializeType == null) { 87 | throw new IllegalArgumentException("codec type not found"); 88 | } 89 | Serializer serializer = ExtensionLoader.getLoader(Serializer.class).getExtension(serializeType.getName()); 90 | 91 | // 压缩器 92 | CompressType compressType = CompressType.fromValue(rpcMessage.getCompressTye()); 93 | Compressor compressor = ExtensionLoader.getLoader(Compressor.class).getExtension(compressType.getName()); 94 | 95 | // 序列化 96 | byte[] notCompressBytes = serializer.serialize(rpcMessage.getData()); 97 | // 压缩 98 | byte[] compressedBytes = compressor.compress(notCompressBytes); 99 | 100 | // 写 body 101 | out.writeBytes(compressedBytes); 102 | return compressedBytes.length; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/remoting/server/ShutdownHook.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.remoting.server; 2 | 3 | import com.ccx.rpc.common.extension.ExtensionLoader; 4 | import com.ccx.rpc.core.config.ConfigManager; 5 | import com.ccx.rpc.core.config.RegistryConfig; 6 | import com.ccx.rpc.core.registry.Registry; 7 | import com.ccx.rpc.core.registry.RegistryFactory; 8 | import lombok.extern.slf4j.Slf4j; 9 | 10 | /** 11 | * 关闭的钩子 12 | * 13 | * @author chenchuxin 14 | * @date 2021/7/24 15 | */ 16 | @Slf4j 17 | public class ShutdownHook { 18 | 19 | public static void addShutdownHook() { 20 | log.info("addShutdownHook for clearAll"); 21 | Runtime.getRuntime().addShutdownHook(new Thread(() -> { 22 | RegistryFactory registryFactory = ExtensionLoader.getLoader(RegistryFactory.class).getAdaptiveExtension(); 23 | RegistryConfig registryConfig = ConfigManager.getInstant().getRegistryConfig(); 24 | Registry registry = registryFactory.getRegistry(registryConfig.toURL()); 25 | registry.unregisterAllMyService(); 26 | })); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/remoting/server/netty/NettyServerBootstrap.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.remoting.server.netty; 2 | 3 | import cn.hutool.core.net.NetUtil; 4 | import cn.hutool.core.thread.ThreadUtil; 5 | import cn.hutool.core.util.RuntimeUtil; 6 | import com.ccx.rpc.core.config.ConfigManager; 7 | import com.ccx.rpc.core.config.ServiceConfig; 8 | import com.ccx.rpc.core.remoting.codec.RpcMessageDecoder; 9 | import com.ccx.rpc.core.remoting.codec.RpcMessageEncoder; 10 | import com.ccx.rpc.core.remoting.server.ShutdownHook; 11 | import io.netty.bootstrap.ServerBootstrap; 12 | import io.netty.channel.*; 13 | import io.netty.channel.epoll.Epoll; 14 | import io.netty.channel.epoll.EpollServerSocketChannel; 15 | import io.netty.channel.nio.NioEventLoopGroup; 16 | import io.netty.channel.socket.SocketChannel; 17 | import io.netty.channel.socket.nio.NioServerSocketChannel; 18 | import io.netty.handler.logging.LogLevel; 19 | import io.netty.handler.logging.LoggingHandler; 20 | import io.netty.handler.timeout.IdleStateHandler; 21 | import io.netty.util.concurrent.DefaultEventExecutorGroup; 22 | import lombok.extern.slf4j.Slf4j; 23 | import org.springframework.stereotype.Component; 24 | 25 | import java.util.concurrent.TimeUnit; 26 | 27 | /** 28 | * netty 服务端 29 | * 30 | * @author chenchuxin 31 | * @date 2021/7/24 32 | */ 33 | @Slf4j 34 | @Component 35 | public class NettyServerBootstrap { 36 | 37 | public void start() { 38 | ShutdownHook.addShutdownHook(); 39 | EventLoopGroup bossGroup = new NioEventLoopGroup(1); 40 | EventLoopGroup workerGroup = new NioEventLoopGroup(); 41 | DefaultEventExecutorGroup serviceHandlerGroup = new DefaultEventExecutorGroup( 42 | RuntimeUtil.getProcessorCount() * 2, 43 | ThreadUtil.newNamedThreadFactory("service-handler-group", false) 44 | ); 45 | try { 46 | ServerBootstrap bootstrap = new ServerBootstrap() 47 | .group(bossGroup, workerGroup) 48 | .channel(Epoll.isAvailable() ? EpollServerSocketChannel.class : NioServerSocketChannel.class) 49 | // 系统用于临时存放已完成三次握手的请求的队列的最大长度。如果连接建立频繁,服务器处理创建新连接较慢,可以适当调大这个参数 50 | .option(ChannelOption.SO_BACKLOG, 128) 51 | // 程序进程非正常退出,内核需要一定的时间才能够释放此端口,不设置 SO_REUSEADDR 就无法正常使用该端口。 52 | .option(ChannelOption.SO_REUSEADDR, Boolean.TRUE) 53 | // TCP/IP协议中针对TCP默认开启了Nagle 算法。 54 | // Nagle 算法通过减少需要传输的数据包,来优化网络。在内核实现中,数据包的发送和接受会先做缓存,分别对应于写缓存和读缓存。 55 | // 启动 TCP_NODELAY,就意味着禁用了 Nagle 算法,允许小包的发送。 56 | // 对于延时敏感型,同时数据传输量比较小的应用,开启TCP_NODELAY选项无疑是一个正确的选择 57 | .childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE) 58 | // 是否开启 TCP 底层心跳机制 59 | .childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE) 60 | .handler(new LoggingHandler(LogLevel.INFO)) 61 | .childHandler(new ChannelInitializer() { 62 | @Override 63 | protected void initChannel(SocketChannel ch) { 64 | ChannelPipeline p = ch.pipeline(); 65 | // 30 秒之内没有收到客户端请求的话就关闭连接 66 | p.addLast(new IdleStateHandler(30, 0, 0, TimeUnit.SECONDS)); 67 | // 编解码器 68 | p.addLast(new RpcMessageEncoder()); 69 | p.addLast(new RpcMessageDecoder()); 70 | // RPC 消息处理器 71 | p.addLast(serviceHandlerGroup, new NettyServerHandler()); 72 | } 73 | }); 74 | // 绑定端口,同步等待绑定成功 75 | ServiceConfig serviceConfig = ConfigManager.getInstant().getServiceConfig(); 76 | ChannelFuture channelFuture = bootstrap.bind(NetUtil.getLocalHostName(), serviceConfig.getPort()).sync(); 77 | log.info("server start success. port=" + serviceConfig.getPort()); 78 | // 等待服务端监听端口关闭 79 | channelFuture.channel().closeFuture().sync(); 80 | } catch (Exception ex) { 81 | log.error("shutdown bossGroup and workerGroup", ex); 82 | bossGroup.shutdownGracefully(); 83 | workerGroup.shutdownGracefully(); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/remoting/server/netty/NettyServerHandler.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.remoting.server.netty; 2 | 3 | import com.ccx.rpc.common.consts.RpcException; 4 | import com.ccx.rpc.core.consts.MessageFormatConst; 5 | import com.ccx.rpc.core.consts.MessageType; 6 | import com.ccx.rpc.core.proxy.RpcServiceCache; 7 | import com.ccx.rpc.core.dto.RpcMessage; 8 | import com.ccx.rpc.core.dto.RpcRequest; 9 | import com.ccx.rpc.core.dto.RpcResponse; 10 | import io.netty.channel.ChannelFutureListener; 11 | import io.netty.channel.ChannelHandlerContext; 12 | import io.netty.channel.SimpleChannelInboundHandler; 13 | import io.netty.handler.timeout.IdleState; 14 | import io.netty.handler.timeout.IdleStateEvent; 15 | import io.netty.util.ReferenceCountUtil; 16 | import lombok.extern.slf4j.Slf4j; 17 | 18 | import java.lang.reflect.Method; 19 | 20 | /** 21 | * @author chenchuxin 22 | * @date 2021/7/25 23 | */ 24 | @Slf4j 25 | public class NettyServerHandler extends SimpleChannelInboundHandler { 26 | 27 | @Override 28 | protected void channelRead0(ChannelHandlerContext ctx, RpcMessage requestMsg) { 29 | try { 30 | // 不理心跳消息 31 | if (requestMsg.getMessageType() != MessageType.REQUEST.getValue()) { 32 | return; 33 | } 34 | RpcMessage.RpcMessageBuilder responseMsgBuilder = RpcMessage.builder() 35 | .serializeType(requestMsg.getSerializeType()) 36 | .compressTye(requestMsg.getCompressTye()) 37 | .requestId(requestMsg.getRequestId()); 38 | RpcRequest rpcRequest = (RpcRequest) requestMsg.getData(); 39 | Object result; 40 | try { 41 | // 根据请求的接口名和版本,获取服务。这个服务是在bean初始化的时候加上的 42 | Object service = RpcServiceCache.getService(rpcRequest.getRpcServiceForCache()); 43 | Method method = service.getClass().getMethod(rpcRequest.getMethodName(), rpcRequest.getParamTypes()); 44 | // 开始执行 45 | result = method.invoke(service, rpcRequest.getParams()); 46 | log.info("service:[{}] successful invoke method:[{}]", rpcRequest.getInterfaceName(), rpcRequest.getMethodName()); 47 | } catch (Exception e) { 48 | throw new RpcException(e.getMessage(), e); 49 | } 50 | responseMsgBuilder.messageType(MessageType.RESPONSE.getValue()); 51 | if (ctx.channel().isActive() && ctx.channel().isWritable()) { 52 | RpcResponse response = RpcResponse.success(result, requestMsg.getRequestId()); 53 | responseMsgBuilder.data(response); 54 | } else { 55 | responseMsgBuilder.data(RpcResponse.fail()); 56 | log.error("not writable now, message dropped"); 57 | } 58 | ctx.writeAndFlush(responseMsgBuilder.build()).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); 59 | } finally { 60 | ReferenceCountUtil.release(requestMsg); 61 | } 62 | } 63 | 64 | @Override 65 | public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { 66 | // 处理空闲状态的 67 | if (evt instanceof IdleStateEvent) { 68 | IdleState state = ((IdleStateEvent) evt).state(); 69 | if (state == IdleState.READER_IDLE) { 70 | log.info("idle check happen, so close the connection"); 71 | ctx.close(); 72 | } 73 | } else { 74 | super.userEventTriggered(ctx, evt); 75 | } 76 | } 77 | 78 | @Override 79 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { 80 | log.error("server catch exception", cause); 81 | ctx.close(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/serialize/Serializer.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.serialize; 2 | 3 | import com.ccx.rpc.common.extension.SPI; 4 | 5 | /** 6 | * 序列化器 7 | * 8 | * @author chenchuxin 9 | * @date 2021/7/20 10 | */ 11 | @SPI("protostuff") 12 | public interface Serializer { 13 | 14 | /** 15 | * 序列化 16 | * 17 | * @param object 要序列化的对象 18 | * @return 字节数组 19 | */ 20 | byte[] serialize(Object object); 21 | 22 | /** 23 | * 反序列化 24 | * 25 | * @param bytes 字节数组 26 | * @param clazz 要反序列化的类 27 | * @param 类型 28 | * @return 反序列化的对象 29 | */ 30 | T deserialize(byte[] bytes, Class clazz); 31 | } 32 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/serialize/protostuff/ProtostuffSerializer.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.serialize.protostuff; 2 | 3 | import com.ccx.rpc.core.serialize.Serializer; 4 | import io.protostuff.LinkedBuffer; 5 | import io.protostuff.ProtostuffIOUtil; 6 | import io.protostuff.Schema; 7 | import io.protostuff.runtime.RuntimeSchema; 8 | 9 | /** 10 | * https://github.com/protostuff/protostuff 11 | * 12 | * @author chenchuxin 13 | * @date 2021/7/22 14 | */ 15 | public class ProtostuffSerializer implements Serializer { 16 | 17 | private static final LinkedBuffer BUFFER = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE); 18 | 19 | @Override 20 | public byte[] serialize(Object object) { 21 | Schema schema = RuntimeSchema.getSchema(object.getClass()); 22 | try { 23 | return ProtostuffIOUtil.toByteArray(object, schema, BUFFER); 24 | } finally { 25 | BUFFER.clear(); 26 | } 27 | } 28 | 29 | @Override 30 | public T deserialize(byte[] bytes, Class clazz) { 31 | Schema schema = RuntimeSchema.getSchema(clazz); 32 | T obj = schema.newMessage(); 33 | ProtostuffIOUtil.mergeFrom(bytes, obj, schema); 34 | return obj; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/spring/RpcScanner.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.spring; 2 | 3 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 4 | import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; 5 | import org.springframework.core.type.filter.AnnotationTypeFilter; 6 | 7 | import java.lang.annotation.Annotation; 8 | 9 | /** 10 | * rpc 类扫描 11 | * 12 | * @author chenchuxin 13 | * @date 2021/7/30 14 | */ 15 | public class RpcScanner extends ClassPathBeanDefinitionScanner { 16 | 17 | public RpcScanner(BeanDefinitionRegistry registry, Class... annotationTypes) { 18 | super(registry); 19 | for (Class annotationType : annotationTypes) { 20 | super.addIncludeFilter(new AnnotationTypeFilter(annotationType)); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/spring/RpcScannerRegistrar.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.spring; 2 | 3 | import cn.hutool.core.util.StrUtil; 4 | import com.ccx.rpc.core.annotation.RpcScan; 5 | import com.ccx.rpc.core.annotation.RpcService; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.beans.factory.support.BeanDefinitionRegistry; 8 | import org.springframework.context.ResourceLoaderAware; 9 | import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; 10 | import org.springframework.core.io.ResourceLoader; 11 | import org.springframework.core.type.AnnotationMetadata; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.stereotype.Service; 14 | 15 | import javax.annotation.Resource; 16 | import java.util.Map; 17 | 18 | /** 19 | * rpc 扫描注册 20 | * 21 | * @author chenchuxin 22 | * @date 2021/7/30 23 | */ 24 | @Slf4j 25 | public class RpcScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware { 26 | 27 | /** 28 | * 服务扫描的基础包,是 @RpcScan 的哪个属性 29 | */ 30 | private static final String SERVER_SCANNER_BASE_PACKAGE_FIELD = "basePackages"; 31 | 32 | /** 33 | * 内部扫描的基础包列表 34 | */ 35 | private static final String[] INNER_SCANNER_BASE_PACKAGES = {"com.ccx.rpc.common", "com.ccx.rpc.core"}; 36 | 37 | private ResourceLoader resourceLoader; 38 | 39 | @Override 40 | public void setResourceLoader(ResourceLoader resourceLoader) { 41 | this.resourceLoader = resourceLoader; 42 | } 43 | 44 | public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { 45 | //扫描注解 46 | Map annotationAttributes = importingClassMetadata 47 | .getAnnotationAttributes(RpcScan.class.getName()); 48 | RpcScanner serviceScanner = new RpcScanner(registry, RpcService.class); 49 | // ccx-rpc 内部的类 50 | RpcScanner innerScanner = new RpcScanner(registry, Component.class, Service.class, Resource.class); 51 | if (resourceLoader != null) { 52 | serviceScanner.setResourceLoader(resourceLoader); 53 | innerScanner.setResourceLoader(resourceLoader); 54 | } 55 | String[] serviceBasePackages = (String[]) annotationAttributes.get(SERVER_SCANNER_BASE_PACKAGE_FIELD); 56 | int serviceCount = serviceScanner.scan(serviceBasePackages); 57 | log.info(StrUtil.format("serviceScanner. packages={}, count={}", serviceBasePackages, serviceCount)); 58 | int innerCount = innerScanner.scan(INNER_SCANNER_BASE_PACKAGES); 59 | log.info(StrUtil.format("innerScanner. packages={}, count={}", INNER_SCANNER_BASE_PACKAGES, innerCount)); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/java/com/ccx/rpc/core/spring/ServiceBeanPostProcessor.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.spring; 2 | 3 | import cn.hutool.core.map.MapUtil; 4 | import cn.hutool.core.net.NetUtil; 5 | import com.ccx.rpc.core.annotation.RpcReference; 6 | import com.ccx.rpc.core.annotation.RpcService; 7 | import com.ccx.rpc.common.consts.URLKeyConst; 8 | import com.ccx.rpc.common.extension.ExtensionLoader; 9 | import com.ccx.rpc.common.url.URL; 10 | import com.ccx.rpc.core.config.ConfigManager; 11 | import com.ccx.rpc.core.config.RegistryConfig; 12 | import com.ccx.rpc.core.config.ServiceConfig; 13 | import com.ccx.rpc.core.proxy.RpcClientProxy; 14 | import com.ccx.rpc.core.proxy.RpcServiceCache; 15 | import com.ccx.rpc.core.registry.Registry; 16 | import com.ccx.rpc.core.registry.RegistryFactory; 17 | import lombok.extern.slf4j.Slf4j; 18 | import org.springframework.beans.BeansException; 19 | import org.springframework.beans.factory.config.BeanPostProcessor; 20 | import org.springframework.stereotype.Component; 21 | 22 | import java.lang.reflect.Field; 23 | import java.util.Map; 24 | 25 | /** 26 | * 实例化时。 27 | * 服务提供方:注册到注册中心 28 | * 服务调用方:生成 RPC 代理类 29 | * 30 | * @author chenchuxin 31 | * @date 2021/7/31 32 | */ 33 | @Slf4j 34 | @Component 35 | public class ServiceBeanPostProcessor implements BeanPostProcessor { 36 | 37 | @Override 38 | public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { 39 | RpcService rpcService = bean.getClass().getAnnotation(RpcService.class); 40 | // rpc 服务需要发布到注册中心 41 | if (rpcService != null) { 42 | RegistryFactory registryFactory = ExtensionLoader.getLoader(RegistryFactory.class).getAdaptiveExtension(); 43 | RegistryConfig registryConfig = ConfigManager.getInstant().getRegistryConfig(); 44 | Registry registry = registryFactory.getRegistry(registryConfig.toURL()); 45 | registry.register(buildServiceURL(bean, rpcService)); 46 | // 然后把服务放到缓存中,方便后续通过 rpcServiceName 获取服务 47 | RpcServiceCache.addService(rpcService.version(), bean); 48 | } 49 | return bean; 50 | } 51 | 52 | @Override 53 | public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { 54 | Field[] fields = bean.getClass().getDeclaredFields(); 55 | for (Field field : fields) { 56 | RpcReference rpcReference = field.getAnnotation(RpcReference.class); 57 | if (rpcReference != null) { 58 | // 生成代理对象 59 | RpcClientProxy rpcClientProxy = new RpcClientProxy(rpcReference); 60 | Object proxy = rpcClientProxy.getProxy(field.getType()); 61 | field.setAccessible(true); 62 | try { 63 | // 设置字段 64 | field.set(bean, proxy); 65 | } catch (IllegalAccessException e) { 66 | log.error("field.set error. bean={}, field={}", bean.getClass(), field.getName(), e); 67 | } 68 | } 69 | } 70 | return bean; 71 | } 72 | 73 | /** 74 | * 构建服务的 URL 75 | * 76 | * @param bean 类实例 77 | * @param rpcService 类上面的 @RpcService 注解 78 | * @return 带有服务信息的 URL 79 | */ 80 | private URL buildServiceURL(Object bean, RpcService rpcService) { 81 | Map param = MapUtil.builder() 82 | .put(URLKeyConst.INTERFACE, bean.getClass().getInterfaces()[0].getCanonicalName()) 83 | .put(URLKeyConst.VERSION, rpcService.version()).build(); 84 | ServiceConfig serviceConfig = ConfigManager.getInstant().getServiceConfig(); 85 | return URL.builder().protocol("ccx-rpc") 86 | .host(NetUtil.getLocalhostStr()) 87 | .port(serviceConfig.getPort()) 88 | .params(param).build(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/resources/META-INF/ccx-rpc/com.ccx.rpc.core.compress.Compressor: -------------------------------------------------------------------------------- 1 | dummy=com.ccx.rpc.core.compress.DummyCompressor 2 | gzip=com.ccx.rpc.core.compress.GzipCompressor -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/resources/META-INF/ccx-rpc/com.ccx.rpc.core.config.loader.ConfigLoader: -------------------------------------------------------------------------------- 1 | system-property=com.ccx.rpc.core.config.loader.SystemPropertyLoader 2 | properties=com.ccx.rpc.core.config.loader.PropertiesConfigLoader -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/resources/META-INF/ccx-rpc/com.ccx.rpc.core.faulttolerant.FaultTolerantInvoker: -------------------------------------------------------------------------------- 1 | retry=com.ccx.rpc.core.faulttolerant.RetryInvoker 2 | fail-fast=com.ccx.rpc.core.faulttolerant.FailFastInvoker -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/resources/META-INF/ccx-rpc/com.ccx.rpc.core.invoke.Invoker: -------------------------------------------------------------------------------- 1 | netty=com.ccx.rpc.core.invoke.NettyInvoker 2 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/resources/META-INF/ccx-rpc/com.ccx.rpc.core.loadbalance.LoadBalance: -------------------------------------------------------------------------------- 1 | random=com.ccx.rpc.core.loadbalance.RandomLoadBalance 2 | round-robin=com.ccx.rpc.core.loadbalance.RoundRobinLoadBalance -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/resources/META-INF/ccx-rpc/com.ccx.rpc.core.registry.RegistryFactory: -------------------------------------------------------------------------------- 1 | zk=com.ccx.rpc.core.registry.zk.ZkRegistryFactory 2 | local=com.ccx.rpc.core.registry.local.LocalRegistryFactory -------------------------------------------------------------------------------- /ccx-rpc-core/src/main/resources/META-INF/ccx-rpc/com.ccx.rpc.core.serialize.Serializer: -------------------------------------------------------------------------------- 1 | protostuff=com.ccx.rpc.core.serialize.protostuff.ProtostuffSerializer -------------------------------------------------------------------------------- /ccx-rpc-core/src/test/java/com/ccx/rpc/core/test/config/ConfigManagerTest.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.test.config; 2 | 3 | import com.ccx.rpc.core.config.ConfigManager; 4 | import com.ccx.rpc.core.config.RegistryConfig; 5 | import org.junit.Assert; 6 | import org.junit.Test; 7 | 8 | /** 9 | * @author chenchuxin 10 | * @date 2021/8/22 11 | */ 12 | public class ConfigManagerTest { 13 | 14 | private static final ConfigManager CONFIG_MANAGER = ConfigManager.getInstant(); 15 | 16 | @Test 17 | public void systemPropertyLoaderTest() { 18 | String address = "zk://localhost:2181"; 19 | System.setProperty("registry.address", address); 20 | RegistryConfig registryConfig = CONFIG_MANAGER.getRegistryConfig(); 21 | Assert.assertEquals(address, registryConfig.getAddress()); 22 | } 23 | 24 | @Test 25 | public void propertiesLoaderTest() { 26 | String address = "zk://localhost:2181"; 27 | RegistryConfig registryConfig = CONFIG_MANAGER.getRegistryConfig(); 28 | Assert.assertEquals(address, registryConfig.getAddress()); 29 | } 30 | 31 | @Test 32 | public void loaderPriorityTest() { 33 | String systemPropertyAddress = "zk://localhost2:2181"; 34 | System.setProperty("registry.address", systemPropertyAddress); 35 | RegistryConfig registryConfig = CONFIG_MANAGER.getRegistryConfig(); 36 | Assert.assertEquals(systemPropertyAddress, registryConfig.getAddress()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/test/java/com/ccx/rpc/core/test/registry/LoadBalanceTest.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.test.registry; 2 | 3 | import cn.hutool.core.collection.ListUtil; 4 | import com.ccx.rpc.common.extension.ExtensionLoader; 5 | import com.ccx.rpc.common.url.URL; 6 | import com.ccx.rpc.core.loadbalance.LoadBalance; 7 | import com.ccx.rpc.core.dto.RpcRequest; 8 | import org.junit.Assert; 9 | import org.junit.Test; 10 | 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | /** 16 | * 负载均衡测试 17 | * 18 | * @author chenchuxin 19 | * @date 2021/8/7 20 | */ 21 | public class LoadBalanceTest { 22 | private static final LoadBalance RANDOM_LOAD_BALANCE = ExtensionLoader.getLoader(LoadBalance.class).getExtension("random"); 23 | private static final LoadBalance ROUND_ROBIN_LOAD_BALANCE = ExtensionLoader.getLoader(LoadBalance.class).getExtension("round-robin"); 24 | private static final List candidateUrls = ListUtil.toList( 25 | URL.valueOf("zk://127.0.0.1:1000"), 26 | URL.valueOf("zk://127.0.0.1:2000"), 27 | URL.valueOf("zk://127.0.0.1:3000") 28 | ); 29 | private static final RpcRequest RPC_REQUEST = RpcRequest.builder().build(); 30 | 31 | @Test 32 | public void randomTest() { 33 | Map counter = new HashMap<>(4); 34 | int runs = 50000; 35 | for (int i = 0; i < runs; i++) { 36 | URL url = RANDOM_LOAD_BALANCE.select(candidateUrls, RPC_REQUEST); 37 | counter.put(url, counter.getOrDefault(url, 0) + 1); 38 | } 39 | double avg = (double) runs / (double) counter.size(); 40 | for (Integer count : counter.values()) { 41 | // count 必须处于 0 到 2 * avg 之间 42 | Assert.assertTrue(count < 2 * avg); 43 | } 44 | } 45 | 46 | @Test 47 | public void roundRobinTest() { 48 | for (int i = 0; i < 500; i++) { 49 | URL url = ROUND_ROBIN_LOAD_BALANCE.select(candidateUrls, RPC_REQUEST); 50 | Assert.assertEquals(candidateUrls.get(i % candidateUrls.size()), url); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/test/java/com/ccx/rpc/core/test/registry/ZkRegistryTest.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.core.test.registry; 2 | 3 | import cn.hutool.core.net.NetUtil; 4 | import com.ccx.rpc.common.extension.ExtensionLoader; 5 | import com.ccx.rpc.core.registry.Registry; 6 | import com.ccx.rpc.core.registry.RegistryFactory; 7 | import com.ccx.rpc.common.url.URL; 8 | import com.ccx.rpc.common.url.URLParser; 9 | import org.apache.curator.test.TestingServer; 10 | import org.junit.After; 11 | import org.junit.Assert; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | 15 | import java.util.List; 16 | 17 | /** 18 | * zk 注册中心测试 19 | * 20 | * @author chenchuxin 21 | * @date 2021/7/18 22 | */ 23 | public class ZkRegistryTest { 24 | 25 | private Registry registry; 26 | 27 | private TestingServer zkServer; 28 | 29 | private final URL serviceUrl = URLParser.toURL("zk://host:123/com.ccx.rpc.core.test.registry.ZkRegistryTest?notify=false&methods=test1,test2"); 30 | private final URL serviceUrl2 = URLParser.toURL("zk://host2:123/com.ccx.rpc.core.test.registry.ZkRegistryTest?notify=false&methods=test1,test2"); 31 | 32 | 33 | @Before 34 | public void setup() throws Exception { 35 | // 获取随机端口,绑定测试用的 zk 服务端 36 | int port = NetUtil.getUsableLocalPort(); 37 | zkServer = new TestingServer(port, true); 38 | zkServer.start(); 39 | URL url = URLParser.toURL("zk://localhost:" + port); 40 | RegistryFactory zkRegistryFactory = ExtensionLoader.getLoader(RegistryFactory.class).getAdaptiveExtension(); 41 | registry = zkRegistryFactory.getRegistry(url); 42 | } 43 | 44 | @After 45 | public void stop() throws Exception { 46 | zkServer.stop(); 47 | } 48 | 49 | @Test 50 | public void testRegister() { 51 | registry.register(serviceUrl); 52 | List lookup = registry.lookup(serviceUrl); 53 | Assert.assertEquals(1, lookup.size()); 54 | Assert.assertEquals(serviceUrl.toFullString(), lookup.get(0).toFullString()); 55 | 56 | registry.register(serviceUrl); 57 | lookup = registry.lookup(serviceUrl); 58 | Assert.assertEquals(1, lookup.size()); 59 | 60 | registry.register(serviceUrl2); 61 | lookup = registry.lookup(serviceUrl); 62 | Assert.assertEquals(2, lookup.size()); 63 | } 64 | 65 | @Test 66 | public void testUnregister() { 67 | { 68 | registry.register(serviceUrl); 69 | List lookup = registry.lookup(serviceUrl); 70 | Assert.assertEquals(1, lookup.size()); 71 | } 72 | { 73 | registry.unregister(serviceUrl); 74 | List lookup = registry.lookup(serviceUrl); 75 | Assert.assertEquals(0, lookup.size()); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /ccx-rpc-core/src/test/resources/ccx-rpc.properties: -------------------------------------------------------------------------------- 1 | registry.address=zk://localhost:2181 -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | ccx-rpc-demo 7 | com.ccx 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | ccx-rpc-demo-client 13 | 14 | 15 | 16 | com.ccx 17 | ccx-rpc-demo-service 18 | 1.0-SNAPSHOT 19 | 20 | 21 | org.springframework.boot 22 | spring-boot-starter-web 23 | ${spring-boot.version} 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-client/src/main/java/com/ccx/rpc/demo/client/ClientBootstrap.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.demo.client; 2 | 3 | import com.ccx.rpc.core.annotation.RpcScan; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | /** 8 | * 启动类 9 | * 10 | * @author chenchuxin 11 | * @date 2021/8/1 12 | */ 13 | @SpringBootApplication 14 | @RpcScan(basePackages = {"com.ccx.rpc.demo.client"}) 15 | public class ClientBootstrap { 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(ClientBootstrap.class, args); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-client/src/main/java/com/ccx/rpc/demo/client/UserController.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.demo.client; 2 | 3 | import com.ccx.rpc.core.annotation.RpcReference; 4 | import com.ccx.rpc.demo.service.api.UserService; 5 | import com.ccx.rpc.demo.service.bean.UserInfo; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | /** 12 | * @author chenchuxin 13 | * @date 2021/8/3 14 | */ 15 | @RestController 16 | @RequestMapping("/user") 17 | public class UserController { 18 | 19 | @RpcReference 20 | private UserService userService; 21 | 22 | @RpcReference(version = "v2") 23 | private UserService userService2; 24 | 25 | @GetMapping("/{uid}") 26 | public UserInfo getUser(@PathVariable("uid") long uid) { 27 | return userService.getUser(uid); 28 | } 29 | 30 | @GetMapping("/v2/{uid}") 31 | public UserInfo getUserV2(@PathVariable("uid") long uid) { 32 | return userService2.getUser(uid); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-client/src/main/java/com/ccx/rpc/demo/client/spi/JSONSerializer.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.demo.client.spi; 2 | 3 | import cn.hutool.json.JSONUtil; 4 | 5 | /** 6 | * @author chenchuxin 7 | * @date 2021/8/10 8 | */ 9 | public class JSONSerializer implements Serializer { 10 | @Override 11 | public byte[] serialize(Object object) { 12 | return JSONUtil.toJsonStr(object).getBytes(); 13 | } 14 | } -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-client/src/main/java/com/ccx/rpc/demo/client/spi/ProtostuffSerializer.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.demo.client.spi; 2 | 3 | import io.protostuff.LinkedBuffer; 4 | import io.protostuff.ProtostuffIOUtil; 5 | import io.protostuff.Schema; 6 | import io.protostuff.runtime.RuntimeSchema; 7 | 8 | /** 9 | * @author chenchuxin 10 | * @date 2021/8/10 11 | */ 12 | public class ProtostuffSerializer implements Serializer { 13 | private static final LinkedBuffer BUFFER = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE); 14 | @Override 15 | public byte[] serialize(Object object) { 16 | Schema schema = RuntimeSchema.getSchema(object.getClass()); 17 | return ProtostuffIOUtil.toByteArray(object, schema, BUFFER); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-client/src/main/java/com/ccx/rpc/demo/client/spi/SPILoaderTest.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.demo.client.spi; 2 | 3 | import com.ccx.rpc.common.extension.ExtensionLoader; 4 | 5 | import java.util.Iterator; 6 | import java.util.ServiceLoader; 7 | 8 | /** 9 | * @author chenchuxin 10 | * @date 2021/8/10 11 | */ 12 | public class SPILoaderTest { 13 | public static void main(String[] args) { 14 | ServiceLoader serviceLoader = ServiceLoader.load(Serializer.class); 15 | Iterator iterator = serviceLoader.iterator(); 16 | while (iterator.hasNext()) { 17 | Serializer serializer= iterator.next(); 18 | System.out.println(serializer.getClass().getName()); 19 | } 20 | } 21 | 22 | public static void main2(String[] args) { 23 | ExtensionLoader loader = ExtensionLoader.getLoader(Serializer.class); 24 | Serializer serializer = loader.getExtension("protostuff"); 25 | System.out.println(serializer.getClass().getName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-client/src/main/java/com/ccx/rpc/demo/client/spi/Serializer.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.demo.client.spi; 2 | 3 | import com.ccx.rpc.common.extension.SPI; 4 | 5 | /** 6 | * @author chenchuxin 7 | * @date 2021/8/10 8 | */ 9 | @SPI 10 | public interface Serializer { 11 | byte[] serialize(Object object); 12 | } 13 | -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-client/src/main/resources/META-INF/ccx-rpc/com.ccx.rpc.demo.client.spi.Serializer: -------------------------------------------------------------------------------- 1 | json=com.ccx.rpc.demo.client.spi.JSONSerializer 2 | protostuff=com.ccx.rpc.demo.client.spi.ProtostuffSerializer -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-client/src/main/resources/META-INF/services/com.ccx.rpc.demo.client.spi.Serializer: -------------------------------------------------------------------------------- 1 | com.ccx.rpc.demo.client.spi.JSONSerializer 2 | com.ccx.rpc.demo.client.spi.ProtostuffSerializer -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-client/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | server.port=8864 -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-client/src/main/resources/ccx-rpc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenchuxin/ccx-rpc/35f1b448b12519885566e6915b2b753e03c80916/ccx-rpc-demo/ccx-rpc-demo-client/src/main/resources/ccx-rpc.properties -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | ccx-rpc-demo 7 | com.ccx 8 | 1.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | ccx-rpc-demo-service 13 | 14 | 15 | 16 | org.springframework.boot 17 | spring-boot-autoconfigure 18 | ${spring-boot.version} 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-service/src/main/java/com/ccx/rpc/demo/service/ServiceBootstrap.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.demo.service; 2 | 3 | import com.ccx.rpc.core.annotation.RpcScan; 4 | import com.ccx.rpc.core.remoting.server.netty.NettyServerBootstrap; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.context.annotation.AnnotationConfigApplicationContext; 7 | 8 | /** 9 | * 启动类 10 | * 11 | * @author chenchuxin 12 | * @date 2021/8/1 13 | */ 14 | @SpringBootApplication 15 | @RpcScan(basePackages = {"com.ccx.rpc.demo.service"}) 16 | public class ServiceBootstrap { 17 | 18 | public static void main(String[] args) { 19 | AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ServiceBootstrap.class); 20 | NettyServerBootstrap serverBootstrap = (NettyServerBootstrap) applicationContext.getBean("nettyServerBootstrap"); 21 | serverBootstrap.start(); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-service/src/main/java/com/ccx/rpc/demo/service/api/UserService.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.demo.service.api; 2 | 3 | import com.ccx.rpc.demo.service.bean.UserInfo; 4 | 5 | /** 6 | * 用户服务 7 | * 8 | * @author chenchuxin 9 | * @date 2021/8/2 10 | */ 11 | public interface UserService { 12 | 13 | /** 14 | * 根据用户 id 获取用户信息 15 | * 16 | * @param id 用户 id 17 | * @return 用户信息,如果获取不到,返回 null 18 | */ 19 | UserInfo getUser(Long id); 20 | } 21 | -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-service/src/main/java/com/ccx/rpc/demo/service/api/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.demo.service.api.impl; 2 | 3 | import com.ccx.rpc.core.annotation.RpcService; 4 | import com.ccx.rpc.demo.service.api.UserService; 5 | import com.ccx.rpc.demo.service.bean.UserInfo; 6 | 7 | /** 8 | * 用户服务 9 | * 10 | * @author chenchuxin 11 | * @date 2021/8/2 12 | */ 13 | @RpcService 14 | public class UserServiceImpl implements UserService { 15 | 16 | @Override 17 | public UserInfo getUser(Long id) { 18 | return UserInfo.builder().userId(id).userName("user" + id).build(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-service/src/main/java/com/ccx/rpc/demo/service/api/impl/UserServiceImplV2.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.demo.service.api.impl; 2 | 3 | import com.ccx.rpc.core.annotation.RpcService; 4 | import com.ccx.rpc.demo.service.api.UserService; 5 | import com.ccx.rpc.demo.service.bean.UserInfo; 6 | 7 | /** 8 | * 用户服务 9 | * 10 | * @author chenchuxin 11 | * @date 2021/8/2 12 | */ 13 | @RpcService(version = "v2") 14 | public class UserServiceImplV2 implements UserService { 15 | 16 | @Override 17 | public UserInfo getUser(Long id) { 18 | return UserInfo.builder().userId(id).userName("v2-user" + id).build(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-service/src/main/java/com/ccx/rpc/demo/service/bean/UserInfo.java: -------------------------------------------------------------------------------- 1 | package com.ccx.rpc.demo.service.bean; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | /** 9 | * 用户信息 10 | * 11 | * @author chenchuxin 12 | * @date 2021/8/2 13 | */ 14 | @Data 15 | @Builder 16 | @NoArgsConstructor 17 | @AllArgsConstructor 18 | public class UserInfo { 19 | private Long userId; 20 | private String userName; 21 | } 22 | -------------------------------------------------------------------------------- /ccx-rpc-demo/ccx-rpc-demo-service/src/main/resources/ccx-rpc.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chenchuxin/ccx-rpc/35f1b448b12519885566e6915b2b753e03c80916/ccx-rpc-demo/ccx-rpc-demo-service/src/main/resources/ccx-rpc.properties -------------------------------------------------------------------------------- /ccx-rpc-demo/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | ccx-rpc 7 | com.ccx 8 | 1.0-SNAPSHOT 9 | 10 | 11 | ccx-rpc-demo-service 12 | ccx-rpc-demo-client 13 | 14 | 4.0.0 15 | 16 | ccx-rpc-demo 17 | 18 | 19 | 20 | com.ccx 21 | ccx-rpc-core 22 | 23 | 24 | org.slf4j 25 | slf4j-simple 26 | ${slf4j.version} 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | com.ccx 8 | ccx-rpc 9 | ${project.version} 10 | 11 | ccx-rpc-core 12 | ccx-rpc-common 13 | ccx-rpc-demo 14 | 15 | pom 16 | 17 | ccx-rpc 18 | 19 | 20 | UTF-8 21 | 1.0-SNAPSHOT 22 | 4.13.2 23 | 5.1.0 24 | 1.18.12 25 | 5.7.4 26 | 1.7.4 27 | 4.1.45.Final 28 | 5.3.9 29 | 2.5.3 30 | 1.7.32 31 | 32 | 33 | 34 | 35 | 36 | com.ccx 37 | ccx-rpc-common 38 | ${project.version} 39 | 40 | 41 | com.ccx 42 | ccx-rpc-core 43 | ${project.version} 44 | 45 | 46 | junit 47 | junit 48 | ${junit.version} 49 | test 50 | 51 | 52 | org.projectlombok 53 | lombok 54 | ${lombok.version} 55 | 56 | 57 | 58 | org.apache.curator 59 | curator-recipes 60 | ${curator.version} 61 | 62 | 63 | org.apache.curator 64 | curator-test 65 | ${curator.version} 66 | test 67 | 68 | 69 | cn.hutool 70 | hutool-all 71 | ${hutool.version} 72 | 73 | 74 | io.protostuff 75 | protostuff-core 76 | ${protostuff.version} 77 | 78 | 79 | io.protostuff 80 | protostuff-runtime 81 | ${protostuff.version} 82 | 83 | 84 | io.netty 85 | netty-all 86 | ${netty.version} 87 | 88 | 89 | org.springframework 90 | spring-context 91 | ${spring.version} 92 | 93 | 94 | org.springframework.boot 95 | spring-boot 96 | ${spring-boot.version} 97 | 98 | 99 | org.slf4j 100 | slf4j-api 101 | ${slf4j.version} 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | maven-resources-plugin 110 | 3.0.2 111 | 112 | 113 | maven-compiler-plugin 114 | 3.8.0 115 | 116 | 8 117 | 8 118 | 119 | 120 | 121 | 122 | 123 | --------------------------------------------------------------------------------