Backend Development 25 min read

Implementing a Simple Java RPC Framework: Architecture, Service Registration, Proxy Generation, and Network Transport

This article explains the principles and implementation of a lightweight Java RPC framework, covering service registration with Zookeeper, client-side dynamic proxies, serialization, compression, Netty-based network transport, and both reflection and Javassist proxy generation, with extensive code examples and performance comparison.

Top Architect
Top Architect
Top Architect
Implementing a Simple Java RPC Framework: Architecture, Service Registration, Proxy Generation, and Network Transport

RPC Definition

Remote Procedure Call (RPC) enables invoking methods on a remote server as if they were local calls. The concept originated in 1981 and has evolved into many frameworks such as Dubbo, Thrift, gRPC, and brpc, each emphasizing performance, simplicity, or completeness.

RPC Principles

To realize the original goal of RPC—transparent remote method invocation—four problems must be solved:

How to discover available remote servers (service registration & discovery).

How to represent data (serialization & deserialization).

How to transmit data (network communication).

How the server determines and invokes the target method (method mapping).

These map directly to common distributed‑system concepts. The article demonstrates a simple RPC project that addresses each of these points.

Implementation Details

Service Registration and Discovery

The project uses Zookeeper as the registry. Zookeeper stores data in memory, offering high read‑performance and suitable read‑many/write‑few scenarios. Services register a persistent node under /rpc/{serviceName}/service and create an ephemeral child node for each instance.

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);
}

Clients lazily fetch the node list when a remote method is invoked, cache it locally, and clear the cache when Zookeeper notifies of changes.

public List
getServices(String name) {
String servicePath = "rpc/" + name + "/service";
List
children = zkClient.getChildren(servicePath);
List
serviceList = Optional.ofNullable(children).orElse(new ArrayList<>())
.stream().map(str -> {
String decoded = URLDecoder.decode(str, StandardCharsets.UTF_8);
return gson.fromJson(decoded, Service.class);
}).collect(Collectors.toList());
SERVER_MAP.put(name, serviceList);
return serviceList;
}

Client Proxy

The client creates a dynamic proxy for each remote interface using Java dynamic proxies. The proxy gathers service metadata, selects a server, builds an RpcRequest , serializes, compresses, and sends it via Netty.

public class ClientProxyFactory {
public
T getProxy(Class
clazz, String group, String version, boolean async) {
if (async) {
return (T) asyncObjectCache.computeIfAbsent(...);
} else {
return (T) objectCache.computeIfAbsent(...);
}
}
private class ClientInvocationHandler implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String serviceName = clazz.getName();
List
serviceList = getServiceList(serviceName);
Service service = loadBalance.selectOne(serviceList);
RpcRequest rpcRequest = new RpcRequest();
// set fields …
RpcResponse response = netClient.sendRequest(rpcRequest, service, protocol, compress);
return response.getReturnValue();
}
}
}

Network Transport

After serialization and compression, the request is sent over TCP using Netty. Netty provides decoders for handling TCP packet fragmentation (sticky packets) such as FixedLengthFrameDecoder , LineBasedFrameDecoder , and LengthFieldBasedFrameDecoder .

Server Side

On the server, the request is decompressed, deserialized, and dispatched to the appropriate service implementation. Two handler implementations are provided:

Reflection‑based handler uses Java reflection to invoke the target method.

Javassist‑based handler generates a lightweight proxy class at runtime and delegates to an InvokeProxy interface.

public class RequestReflectHandler extends RequestBaseHandler {
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;
}
}
public class RequestJavassistHandler extends RequestBaseHandler {
public RpcResponse invoke(ServiceObject serviceObject, RpcRequest request) throws Exception {
InvokeProxy invokeProxy = (InvokeProxy) serviceObject.getObj();
return invokeProxy.invoke(request);
}
}

Proxy Generation with Javassist

Javassist creates a new class that implements InvokeProxy . The generated class holds a static reference to the Spring bean of the service implementation and forwards calls after parameter conversion.

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 the Javassist proxy is marginally faster than reflection, but the difference is small (e.g., ~5 % improvement for 1 000 000 calls). Therefore, reflection offers sufficient simplicity for most cases.

Conclusion

The article covered RPC fundamentals, service registration & discovery with Zookeeper, client‑side dynamic proxies, Netty transport, and two server‑side proxy strategies (reflection and Javassist). It also discussed serialization options, compression, sticky‑packet handling, and provided performance data, giving readers a solid foundation for building or extending their own RPC frameworks.

distributed systemsJavaRPCReflectionZookeeperNettyJavassist
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

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.