Skip to content

Latest commit

 

History

History
212 lines (160 loc) · 6.99 KB

v1.0.md

File metadata and controls

212 lines (160 loc) · 6.99 KB

marpc v1.0 — 最小可用 RPC 框架

一、代码结构

src/main/java/com/malinghan/marpc/
├── annotation/
│   ├── EnableMarpc.java          # 启用框架的入口注解
│   ├── MarpcProvider.java        # 标记服务提供方实现类
│   └── MarpcConsumer.java        # 标记需要注入 RPC 代理的字段
├── config/
│   └── MarpcConfig.java          # Spring 配置类,装配三个核心 Bean
├── core/
│   ├── RpcRequest.java           # 请求协议体(service / method / args)
│   └── RpcResponse.java          # 响应协议体(status / data / errorMessage)
├── provider/
│   └── ProviderBootstrap.java    # 服务注册 + 请求分发(skeleton map)
├── consumer/
│   ├── ConsumerBootstrap.java    # 扫描 @MarpcConsumer 字段并注入动态代理
│   └── MarpcInvocationHandler.java  # 代理拦截器,序列化请求并发起 HTTP 调用
├── transport/
│   └── MarpcTransport.java       # HTTP 端点 POST /marpc,接收并转发请求
└── demo/
    ├── HelloService.java         # 示例接口
    └── HelloServiceImpl.java     # 示例实现(标注 @MarpcProvider)

src/test/java/com/malinghan/marpc/
├── provider/ProviderBootstrapTest.java       # 单元测试:服务注册与调用分发
├── consumer/MarpcInvocationHandlerTest.java  # 单元测试:代理序列化与反序列化
└── transport/MarpcTransportTest.java         # Web 层测试:HTTP 端点行为

关键依赖:

  • spring-boot-starter-webmvc — 提供 HTTP 服务端
  • okhttp 4.12.0 — 消费端 HTTP 客户端
  • fastjson2 2.0.47 — JSON 序列化/反序列化

二、设计思路

整体调用链

Consumer 业务代码
  │  调用接口方法(本地)
  ▼
MarpcInvocationHandler(JDK 动态代理)
  │  封装 RpcRequest { service, method, args }
  │  JSON 序列化 → HTTP POST /marpc
  ▼
MarpcTransport(@RestController)
  │  反序列化 RpcRequest
  ▼
ProviderBootstrap.invoke()
  │  skeleton map 查找实现类
  │  反射调用目标方法
  ▼
RpcResponse { status, data }
  │  JSON 序列化 → HTTP 响应
  ▼
MarpcInvocationHandler
  │  JSON.to(returnType, data) 反序列化返回值
  ▼
Consumer 业务代码拿到结果

核心设计决策

1. 协议层(RpcRequest / RpcResponse)

用最简结构描述一次 RPC 调用:

  • service:接口全限定名,作为服务查找的 key
  • method:方法名,配合参数个数定位具体方法
  • args:参数数组,JSON 序列化传输

响应统一包装为 { status, data, errorMessage },消费端根据 status 决定是返回数据还是抛异常。

2. Provider 侧(skeleton map)

ProviderBootstrap.start() 在启动时扫描所有标注 @MarpcProvider 的 Bean,遍历其实现的接口,以接口全限定名为 key 存入 skeleton map。

调用时通过 service 字段 O(1) 查找实现,再用参数个数匹配方法(v1.0 不处理重载,仅按名称+参数数量匹配)。

3. Consumer 侧(JDK 动态代理)

ConsumerBootstrap.start() 遍历所有 Spring Bean 的字段,对标注 @MarpcConsumer 的字段通过 Proxy.newProxyInstance 注入代理对象。

MarpcInvocationHandler 实现 InvocationHandler,拦截所有方法调用:

  • Object 类的方法(equals/hashCode/toString)直接本地执行,不走网络
  • 其余方法封装为 RpcRequest,通过 OkHttp 发送到 Provider

4. 传输层(HTTP + JSON)

选用 HTTP 而非自定义 TCP 协议,降低实现复杂度。单一端点 POST /marpc 接收所有 RPC 请求,由 MarpcTransport 委托给 ProviderBootstrap 处理。

5. 启动装配(@EnableMarpc)

@EnableMarpc 通过 @Import(MarpcConfig.class) 将框架配置引入 Spring 容器,用户只需在启动类加一个注解即可启用整个框架,无需手动注册任何 Bean。


三、测试方法

单元测试

ProviderBootstrapTest — 测试服务注册与调用分发

// mock ApplicationContext,注入带 @MarpcProvider 的实现类
// 验证:正常调用返回正确结果
// 验证:service 不存在返回 error response
// 验证:method 不存在返回 error response

运行:

mvn test -pl . -Dtest=ProviderBootstrapTest

MarpcInvocationHandlerTest — 测试消费端代理行为

通过反射将 OkHttpClient 替换为 mock,控制 HTTP 响应内容:

// 验证:String 返回值正确反序列化
// 验证:int 返回值正确反序列化
// 验证:error response 抛出 RuntimeException
// 验证:发出的 HTTP 请求体包含正确的 service 和 method 字段

运行:

mvn test -pl . -Dtest=MarpcInvocationHandlerTest

MarpcTransportTest — 测试 HTTP 端点

使用 @WebMvcTest + MockMvc,mock ProviderBootstrap:

// 验证:成功响应时 HTTP 200,body 包含 status=true 和 data
// 验证:失败响应时 HTTP 200,body 包含 status=false 和 errorMessage

运行:

mvn test -pl . -Dtest=MarpcTransportTest

全量测试

mvn test

集成验证(手动)

启动应用后,直接用 curl 模拟消费端调用:

curl -X POST http://localhost:8080/marpc \
  -H "Content-Type: application/json" \
  -d '{"service":"com.malinghan.marpc.demo.HelloService","method":"hello","args":["world"]}'

预期响应:

{"status":true,"data":"hello, world","errorMessage":null}

四、v1.0 的局限性

v1.0 是最小可用实现,以下能力留给后续版本:

问题 后续版本方向
Provider 地址硬编码 服务注册与发现(Zookeeper / HTTP Registry)
无负载均衡 Random / RoundRobin 策略
方法重载无法区分 方法签名格式 method@argCount_type1_type2
无容错机制 重试、熔断器
参数类型依赖 JSON 推断 显式类型信息传递
Provider/Consumer 同进程 拆分为独立应用

debug

[ProviderBootstrap] 服务未找到: com.malinghan.marpc.demo.HelloService

skeleton map 里没有注册 HelloService。根因在 ProviderBootstrap.start() 的扫描逻辑:

// ProviderBootstrap.java:33 for (Class<?> iface : bean.getClass().getInterfaces()) {

HelloServiceImpl 被 Spring 用 CGLIB 代理包装后,bean.getClass() 返回的是 HelloServiceImpl$$SpringCGLIB$$0 这类代理类,而不是 HelloServiceImpl。代理类的 getInterfaces() 可能返回空,或者返回的是代理相关接口,导致 HelloService 没被注册进去。

修复方式:用 AopUtils.getTargetClass() 或者向上遍历父类来获取真实类型:

Bootstrap.java
import org.springframework.aop.support.AopUtils;

providers.values().forEach(bean -> {
Class<?> targetClass = AopUtils.getTargetClass(bean); // 获取真实实现类
      for (Class<?> iface : targetClass.getInterfaces()) {
if (isUserDefinedInterface(iface)) {
skeleton.put(iface.getCanonicalName(), bean);
...
}
}
});