Backend Development 25 min read

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

This article explains the principles and implementation of a Java RPC framework, covering service registration with Zookeeper, serialization, compression, network communication via Netty, dynamic proxy generation using reflection and Javassist, and performance considerations, providing extensive code examples for each component.

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

Remote Procedure Call (RPC) enables calling remote methods as if they were local. The article introduces the concept, its history, and the major frameworks such as Dubbo, Thrift, gRPC, and brpc.

RPC Principles

To achieve transparent remote invocation, four problems must be solved: service discovery, data representation (serialization), data transmission, and server-side method mapping.

System Architecture

The overall architecture consists of a registration center (Zookeeper), client proxies, network transport (Netty), and server handlers. Service nodes register themselves in Zookeeper; clients subscribe to updates and obtain available nodes.

Service Registration & Discovery (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);
}

Clients retrieve service lists lazily and cache them locally, clearing the cache when Zookeeper notifies of changes.

Client Proxy Generation

Clients use ClientProxyFactory.getProxy to create dynamic proxies. The proxy serializes the request, selects a service node, builds a RpcRequest , and sends it via Netty.

public
T getProxy(Class
clazz, String group, String version, boolean async) {
  if (async) {
    return (T) asyncObjectCache.computeIfAbsent(...);
  } else {
    return (T) objectCache.computeIfAbsent(...);
  }
}

The ClientInvocationHandler.invoke method assembles the request, chooses a protocol, compresses the payload, and receives a RpcResponse :

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  String serviceName = clazz.getName();
  Service service = loadBalance.selectOne(getServiceList(serviceName));
  RpcRequest rpcRequest = new RpcRequest();
  // set fields ...
  RpcResponse response = netClient.sendRequest(rpcRequest, service, protocol, compresser);
  return response.getReturnValue();
}

Serialization & Compression

Supported serialization includes Java Serializable , Protobuf, Kryo, and JSON. Compression uses Gzip to reduce payload size.

public interface Compresser {
  byte[] compress(byte[] bytes);
  byte[] decompress(byte[] bytes);
}

Network Transport (Netty)

Netty handles TCP communication, dealing with issues such as packet framing, sticky packets, and packet splitting. Decoders like FixedLengthFrameDecoder , LineBasedFrameDecoder , and LengthFieldBasedFrameDecoder are discussed.

Server Request Handling

The abstract RequestBaseHandler defines handleRequest , which looks up the service object and invokes the target method. Two concrete implementations exist:

RequestReflectHandler uses Java reflection.

RequestJavassistHandler invokes a generated InvokeProxy created by Javassist.

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

Javassist generates proxy classes at runtime, allowing method calls without reflection overhead. Example proxy class snippet:

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

Performance Test

A benchmark on a MacBook Pro M1 compares reflection and Javassist proxies over 1 000 000 calls. Results show only marginal differences, indicating either approach is acceptable.

Conclusion

The article provides a comprehensive walkthrough of building an RPC framework in Java, covering service registration, client proxies, serialization, compression, network transport, server-side handling, and two proxy generation strategies, with ample code examples for readers to study and extend.

distributed systemsJavaproxyRPCSerializationZookeeperNettyJavassist
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.