为了彻底理解 RPC(远程过程调用)的底层原理,我们这里不使用任何第三方 RPC 框架(如 Dubbo 或 Spring Cloud),而是使用 SPI 机制加载我们自己写的 HTTP 和 netty 实现。HTTP 服务使用内置的 Tomcat,通过最原始的 HTTP 请求 + Java 动态代理 + 反射,纯手工打造一个迷你版 RPC 框架。整个案例分成三个模块,公共模块 demo-common,服务提供者 demo-provider,以及服务的调用者 demo-consumer。下面是这三个模块的完整实现代码。
demo-common

依赖包:
1 2 3 4 5 6 7 8 9 10 11 12
| <dependencies> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.18.0</version> </dependency>
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies>
|
接口定义:
1 2 3
| public interface UserService { String sayHello(String username); }
|
公共类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
@Data @AllArgsConstructor public class Invocation implements Serializable { private String interfaceName; private String version; private String methodName; private Class[] paramType; private Object[] params; }
@Data @AllArgsConstructor @NoArgsConstructor public class TargetServiceInfo implements Serializable { private String hostname; private Integer port; }
|
公共组件 - 远程注册中心
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| public class RemoteMapRegister { private static final Map<String, List<TargetServiceInfo>> REGISTER = new HashMap<>();
public static void register(String interfaceName, String version, TargetServiceInfo info) { List<TargetServiceInfo> list = REGISTER.get(interfaceName + version); if (list == null) { list = new ArrayList<>(); } list.add(info); REGISTER.put(interfaceName + version, list); flushToFile(); }
public static List<TargetServiceInfo> get(String interfaceName, String version) { if (REGISTER.isEmpty()) { getFromFile(); } return REGISTER.get(interfaceName + version); }
private static void flushToFile() { try (FileOutputStream fos = new FileOutputStream("/tmp/targetServiceInfos.txt"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fos)) { objectOutputStream.writeObject(REGISTER); objectOutputStream.flush(); } catch (Exception e) { e.printStackTrace(); } }
private static void getFromFile() { try (FileInputStream fin = new FileInputStream("/tmp/targetServiceInfos.txt"); ObjectInputStream objectInputStream = new ObjectInputStream(fin)) { Map<String, List<TargetServiceInfo>> mm = (Map) objectInputStream.readObject(); REGISTER.putAll(mm); } catch (Exception e) { e.printStackTrace(); } } }
|
demo-provider

依赖包:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <dependencies> <dependency> <groupId>com.owlias.janus</groupId> <artifactId>demo-common</artifactId> <version>${project.version}</version> </dependency>
<dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-core</artifactId> <version>10.1.24</version> </dependency> </dependencies>
|
服务提供者启动类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| import com.owlias.janus.framework.Transporter; import com.owlias.janus.framework.register.LocalRegister; import com.owlias.janus.register.RemoteMapRegister; import com.owlias.janus.register.TargetServiceInfo; import com.owlias.janus.service.UserService; import com.owlias.janus.service.UserServiceImpl01; import com.owlias.janus.service.UserServiceImpl02; import java.util.ServiceLoader;
public class ProviderApp { public static void main(String[] args) { LocalRegister.register(UserService.class.getName(), "1.0", UserServiceImpl01.class); LocalRegister.register(UserService.class.getName(), "2.0", UserServiceImpl02.class);
TargetServiceInfo targetServiceInfo = new TargetServiceInfo("localhost", 8080); RemoteMapRegister.register(UserService.class.getName(),"1.0", targetServiceInfo); RemoteMapRegister.register(UserService.class.getName(),"2.0", targetServiceInfo);
String protocolName = "http"; Transporter targetServer = null; ServiceLoader<Transporter> serviceLoader = ServiceLoader.load(Transporter.class); for (Transporter server : serviceLoader) { if (server.support(protocolName)) { targetServer = server; break; } } if (targetServer != null) { targetServer.start("localhost", 8080); } else { throw new IllegalArgumentException("[SPI 错误] 未找到任何支持协议扩展名为 [" + protocolName + "] 的通信插件,请检查 META-INF/services 档案配置!"); } } }
|
服务的接口实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class UserServiceImpl01 implements UserService { @Override public String sayHello(String username) { return "hello version01: " + username; } }
public class UserServiceImpl02 implements UserService { @Override public String sayHello(String username) { return "hello version02: " + username; } }
|
本地服务注册封装类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
public class LocalRegister { private static final Map<String, Class<?>> map = new HashMap<>();
public static void register(String interfaceName, String version, Class<?> implClass) { map.put(interfaceName + version, implClass); }
public static Class<?> get(String interfaceName, String version) { return map.get(interfaceName + version); } }
|
定义统一的容器抽象接口,用于SPI的加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
public interface Transporter {
boolean support(String protocolName);
void start(String hostname, Integer port); }
|
在 demo-provider(或者专门的插件 Jar 包)的 resources 资源目录下,严格按照以下路径创建文件夹和文件:
- 创建目录:src/main/resources/META-INF/services/
- 在该目录下创建一个文本文件,文件名必须是接口的全限定名:com.owlias.janus.framework.Transporter
- 在文件内容里,写上你想要激活的具体实现类的全限定名(一行一个)
1 2
| com.owlias.janus.framework.http.HttpServer com.owlias.janus.framework.netty.NettyServer
|
完成了 SPI 改造后,我们的主应用启动类将升华到真正的微内核、框架级高度。它不再和 Tomcat 或 Netty 发生任何强耦合!下面是两个 Transporter server 的实现:
HttpServer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70
| import com.owlias.janus.framework.Transporter; import org.apache.catalina.LifecycleException; import org.apache.catalina.Server; import org.apache.catalina.Service; import org.apache.catalina.connector.Connector; import org.apache.catalina.core.StandardContext; import org.apache.catalina.core.StandardEngine; import org.apache.catalina.core.StandardHost; import org.apache.catalina.startup.Tomcat;
public class HttpServer implements Transporter {
@Override public boolean support(String protocolName) { return "http".equalsIgnoreCase(protocolName); }
@Override public void start(String hostname, Integer port) { Tomcat tomcat = new Tomcat();
Server server = tomcat.getServer();
Service service = server.findService("Tomcat");
StandardEngine engine = new StandardEngine(); engine.setDefaultHost(hostname);
StandardHost host = new StandardHost(); host.setName(hostname); engine.addChild(host);
String contextPath = ""; StandardContext context = new StandardContext(); context.setPath(contextPath); context.addLifecycleListener(new Tomcat.FixContextListener()); host.addChild(context);
Connector connector = new Connector(); connector.setPort(port); service.setContainer(engine); service.addConnector(connector);
tomcat.addServlet(contextPath, "dispatcher", new DispatcherServlet());
context.addServletMappingDecoded("/*", "dispatcher");
try { tomcat.start(); tomcat.getServer().start(); } catch (LifecycleException e) { e.printStackTrace(); } } }
|
DispatcherServlet:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException;
public class DispatcherServlet extends HttpServlet {
@Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { new HttpServletHandler().handler(req, resp); } }
|
HttpServletHandler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import com.owlias.janus.model.Invocation; import com.owlias.janus.framework.register.LocalRegister; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.io.IOUtils; import java.io.ObjectInputStream; import java.lang.reflect.Method;
public class HttpServletHandler { public void handler(HttpServletRequest req, HttpServletResponse resp) { try { Invocation invocation = (Invocation) new ObjectInputStream(req.getInputStream()).readObject(); String interfaceName = invocation.getInterfaceName(); String version = invocation.getVersion();
Class<?> classImpl = LocalRegister.get(interfaceName, version); Method method = classImpl.getMethod(invocation.getMethodName(), invocation.getParamType());
String result = (String) method.invoke(classImpl.getConstructor().newInstance(), invocation.getParams()); IOUtils.write(result, resp.getOutputStream()); } catch (Exception e) { throw new RuntimeException(e); } } }
|
NettyServer:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class NettyServer implements Transporter {
@Override public boolean support(String protocolName) { return "netty".equalsIgnoreCase(protocolName); }
@Override public void start(String hostname, Integer port) { System.out.println("[SPI 插件] 正在通过高性能 Netty (Epoll) 异步双工通道启动服务..."); } }
|
demo-comsumer

调用者服务启动测试类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| import com.owlias.janus.framework.HttpClient; import com.owlias.janus.model.Invocation; import com.owlias.janus.framework.LoadBalance; import com.owlias.janus.register.RemoteMapRegister; import com.owlias.janus.register.TargetServiceInfo; import com.owlias.janus.service.UserService; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.List;
public class ConsumerApp { public static void main(String[] args) {
UserService userService = (UserService) Proxy.newProxyInstance(UserService.class.getClassLoader(), new Class[]{UserService.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { List<TargetServiceInfo> serviceList = RemoteMapRegister.get(UserService.class.getName(), "2.0"); TargetServiceInfo targetService = LoadBalance.random(serviceList);
HttpClient httpClient = new HttpClient(); Invocation invocation = new Invocation(UserService.class.getName(), "2.0", "sayHello", new Class[]{String.class}, new Object[]{"zhangsan"}); return httpClient.send(targetService.getHostname(), targetService.getPort(), invocation); } });
String result = userService.sayHello("zhangsan"); System.out.println(result); } }
|
调用端负载均衡器:
一般的 RPC 服务,负载均衡器通常放在调用端,如果放在服务提供端的话,还需要 nginx 或其他前置组件。
1 2 3 4 5 6 7 8 9 10
| import com.owlias.janus.register.TargetServiceInfo; import java.util.List; import java.util.Random;
public class LoadBalance { public static TargetServiceInfo random(List<TargetServiceInfo>list) { int index = new Random().nextInt(list.size()); return list.get(index); } }
|
调用工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| import com.owlias.janus.model.Invocation; import org.apache.commons.io.IOUtils; import java.io.InputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.net.HttpURLConnection; import java.net.URL;
public class HttpClient { public String send(String hostname, Integer port, Invocation invocation) { try { URL url = new URL("http", hostname, port, "/"); HttpURLConnection httpUrlConnection = (HttpURLConnection) url.openConnection();
httpUrlConnection.setRequestMethod("POST"); httpUrlConnection.setDoOutput(true);
OutputStream outputStream = httpUrlConnection.getOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(outputStream); oos.writeObject(invocation); oos.flush(); oos.close();
InputStream inputStream = httpUrlConnection.getInputStream(); String result = IOUtils.toString(inputStream); return result; } catch (Exception e) { throw new RuntimeException(e); } } }
|
标题:
RPC - 基于 SPI 机制实现一个简单的 RPC 案例