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:接口全限定名,作为服务查找的 keymethod:方法名,配合参数个数定位具体方法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=ProviderBootstrapTestMarpcInvocationHandlerTest — 测试消费端代理行为
通过反射将 OkHttpClient 替换为 mock,控制 HTTP 响应内容:
// 验证:String 返回值正确反序列化
// 验证:int 返回值正确反序列化
// 验证:error response 抛出 RuntimeException
// 验证:发出的 HTTP 请求体包含正确的 service 和 method 字段运行:
mvn test -pl . -Dtest=MarpcInvocationHandlerTestMarpcTransportTest — 测试 HTTP 端点
使用 @WebMvcTest + MockMvc,mock ProviderBootstrap:
// 验证:成功响应时 HTTP 200,body 包含 status=true 和 data
// 验证:失败响应时 HTTP 200,body 包含 status=false 和 errorMessage运行:
mvn test -pl . -Dtest=MarpcTransportTestmvn 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 是最小可用实现,以下能力留给后续版本:
| 问题 | 后续版本方向 |
|---|---|
| Provider 地址硬编码 | 服务注册与发现(Zookeeper / HTTP Registry) |
| 无负载均衡 | Random / RoundRobin 策略 |
| 方法重载无法区分 | 方法签名格式 method@argCount_type1_type2 |
| 无容错机制 | 重试、熔断器 |
| 参数类型依赖 JSON 推断 | 显式类型信息传递 |
| Provider/Consumer 同进程 | 拆分为独立应用 |
[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);
...
}
}
});