Backend Development 23 min read

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.

Architect's Guide
Architect's Guide
Architect's Guide
Implementing a Simple Java RPC Framework with Zookeeper, Netty, and Javassist

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

distributed systemsJavaRPCZookeeperNettyJavassist
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.