Implementing a Simple Java RPC Framework with Zookeeper, Netty, and Javassist
This article walks through the design and implementation of a lightweight Java RPC framework, covering core concepts such as service registration and discovery with Zookeeper, network communication via Netty, serialization, compression, dynamic proxy generation using Javassist, and performance comparisons between reflection and bytecode‑generated proxies.
In the era of distributed systems, Remote Procedure Call (RPC) plays a crucial role, with popular frameworks like Dubbo, Thrift, and gRPC. This article demonstrates how to build a simple RPC framework in Java, exposing the underlying principles and practical code examples that cover service registration and discovery, client proxies, network transmission, serialization, compression, and server-side method invocation.
RPC Definition
Remote Procedure Call (RPC) aims to make remote method invocation as simple as local calls. Over four decades, various frameworks have evolved to address this goal with different trade‑offs in simplicity, performance, and feature set.
RPC Principles
To achieve transparent remote calls, an RPC system must solve four problems: locating available servers, representing data, transmitting data, and invoking the target method on the server.
Overall Architecture
The server registers its service nodes in a registry (Zookeeper). Clients subscribe to the registry to obtain available nodes, cache them locally, and invoke remote methods. When the registry updates, clients are notified to avoid stale nodes.
Service Registration and Discovery
Zookeeper is used as the registry because it excels in read‑heavy scenarios. Persistent nodes store service metadata, while temporary nodes represent live service instances. The following code shows how a service is exported to Zookeeper:
public void exportService(Service serviceResource) {
String name = serviceResource.getName();
String uri = GSON.toJson(serviceResource);
String servicePath = "rpc/" + name + "/service";
if (!zkClient.exists(servicePath)) {
zkClient.createPersistent(servicePath, true);
}
String uriPath = servicePath + "/" + uri;
if (zkClient.exists(uriPath)) {
zkClient.delete(uriPath);
}
zkClient.createEphemeral(uriPath);
}When two services start locally, Zookeeper shows two service nodes under the /rpc/{serviceName}/service path.
Client Proxy Generation
The client uses Java dynamic proxies to intercept method calls. ClientProxyFactory.getProxy creates a proxy that builds an RpcRequest , selects a service instance, serializes and compresses the request, sends it via Netty, and processes the RpcResponse :
public class ClientProxyFactory {
public
T getProxy(Class
clazz, String group, String version, boolean async) {
if (async) {
return (T) asyncObjectCache.computeIfAbsent(clazz.getName() + group + version,
clz -> Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz},
new ClientInvocationHandler(clazz, group, version, async)));
} else {
return (T) objectCache.computeIfAbsent(clazz.getName() + group + version,
clz -> Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz},
new ClientInvocationHandler(clazz, group, version, async)));
}
}
private class ClientInvocationHandler implements InvocationHandler {
// ... build request, select service, send via Netty, return response.getReturnValue()
}
}Network Transmission
Requests are serialized (e.g., using Kryo, Protobuf, or JSON) and then compressed (e.g., Gzip) before being sent over TCP via Netty. Netty provides decoders for handling TCP packet fragmentation such as FixedLengthFrameDecoder , LineBasedFrameDecoder , and LengthFieldBasedFrameDecoder .
Server Side Handling
On the server, the received bytes are decompressed and deserialized into an RpcRequest . The RequestBaseHandler locates the appropriate service object and invokes the method either via reflection or via a Javassist‑generated proxy:
public class RequestReflectHandler extends RequestBaseHandler {
@Override
public RpcResponse invoke(ServiceObject serviceObject, RpcRequest request) throws Exception {
Method method = serviceObject.getClazz().getMethod(request.getMethod(), request.getParametersTypes());
Object value = method.invoke(serviceObject.getObj(), request.getParameters());
RpcResponse response = new RpcResponse(RpcStatusEnum.SUCCESS);
response.setReturnValue(value);
return response;
}
}When using Javassist, a proxy class implements InvokeProxy and forwards the request to the actual service implementation:
public class RequestJavassistHandler extends RequestBaseHandler {
@Override
public RpcResponse invoke(ServiceObject serviceObject, RpcRequest request) throws Exception {
InvokeProxy invokeProxy = (InvokeProxy) serviceObject.getObj();
return invokeProxy.invoke(request);
}
}Generating Proxies with Javassist
Javassist creates bytecode at runtime to produce a class like HelloService$proxy1649315143476 , which holds a static reference to the Spring bean and implements an invoke method that delegates to the real service method.
public class HelloService$proxy1649315143476 {
private static HelloService serviceProxy = ((ApplicationContext)Container.getSpringContext()).getBean("helloServiceImpl");
public RpcResponse hello(RpcRequest request) throws Exception {
Object[] params = request.getParameters();
String arg0 = ConvertUtil.convertToString(params[0]);
String returnValue = serviceProxy.hello(arg0);
return new RpcResponse(returnValue);
}
public RpcResponse invoke(RpcRequest request) throws Exception {
if (request.getMethod().equalsIgnoreCase("hello")) {
return hello(request);
}
return null;
}
}Performance Comparison
Benchmarks on a MacBook Pro M1 show that reflection and Javassist proxies have comparable latency, with Javassist being marginally faster for high request volumes.
Conclusion
The article covered RPC fundamentals, service registration/discovery with Zookeeper, client proxy creation, network transmission, serialization/compression, and two server‑side proxy strategies (reflection vs. Javassist). Readers are encouraged to explore the full source code, experiment with extensions such as custom protocols, asynchronous calls, and advanced load‑balancing.
Project repository: https://github.com/ppphuang/rpc-spring-starter
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.