29 Add Meal HTTP Protocol JSONRPCDubbo Cross-Language is So Simple #
In the previous lesson, when introducing the implementation of Protocol and Invoker, we focused on AbstractProtocol and DubboInvoker. In fact, there is another branch of implementation for Protocol called AbstractProxyProtocol, as shown in the following diagram:
Inheritance diagram of AbstractProxyProtocol
From the diagram, we can see that the Protocol implementations of gRPC, HTTP, WebService, Hessian, Thrift, and other protocols all inherit from the abstract class AbstractProxyProtocol.
In today’s diverse technology stack, many companies use languages such as Node.js, Python, Rails, Go, etc. to develop web applications, while many services are implemented using Java technology stack. This leads to a significant demand for cross-language invocation. As an RPC framework, Dubbo naturally hopes to achieve cross-language invocation. Currently, Dubbo uses the “HTTP Protocol + JSON-RPC” approach to achieve this, where both the HTTP protocol and JSON are naturally cross-language standards with mature libraries available in various languages.
Now let’s focus on analyzing Dubbo’s support for the HTTP protocol. First, I will introduce the basics of JSON-RPC and help you get started quickly through an example. Then I will explain the specific implementation of HttpProtocol in Dubbo, which combines the HTTP protocol with JSON-RPC to achieve cross-language invocation.
JSON-RPC #
The HTTP protocol supported by Dubbo is actually based on the JSON-RPC protocol.
JSON-RPC is a cross-language remote procedure call protocol based on JSON. The XML-RPC and WebService protocols supported by modules such as dubbo-rpc-xml and dubbo-rpc-webservice in Dubbo, like JSON-RPC, are all text-based protocols. However, JSON-RPC has a simpler and more compact format compared to XML, WebService, etc. Compared to binary protocols such as Dubbo and Hessian, JSON-RPC is easier to debug and implement. Therefore, JSON-RPC is a very excellent remote procedure call protocol.
In the Java ecosystem, there are many mature JSON-RPC frameworks, such as jsonrpc4j and jpoxy. Among them, jsonrpc4j is lightweight, easy to use, and can be used independently or seamlessly integrated with Spring, making it very suitable for Spring-based projects.
Now let’s take a look at the basic format of a JSON-RPC request:
{
"id": 1,
"method": "sayHello",
"params": [
"Dubbo json-rpc"
]
}
The fields in a JSON-RPC request have the following meanings:
- The
id
field is used to uniquely identify a remote procedure call. - The
method
field specifies the name of the method to be called. - The
params
array represents the parameters passed to the method. If the method doesn’t take any parameters, an empty array is passed.
After the JSON-RPC server receives a call request, it will look for the corresponding method and invoke it. Then it formats the return value of the method into the following format and returns it to the client:
{
"id": 1,
"result": "Hello Dubbo json-rpc",
"error": null
}
The fields in a JSON-RPC response have the following meanings:
- The
id
field is used to uniquely identify a remote procedure call, and its value is consistent with theid
field in the request. - The
result
field records the return value of the method. If there is no return value, it returns null. If an error occurs during the invocation, it returns null as well. - The
error
field represents the exception information when an error occurs during the invocation. If the method executes without exceptions, this field is null.
Basic Usage of jsonrpc4j #
Dubbo uses the jsonrpc4j library to implement the JSON-RPC protocol. Now let’s use jsonrpc4j to write a simple JSON-RPC server example and client example. Through these two examples, we will demonstrate the basic usage of jsonrpc4j.
First, we need to create the domain classes and service interface that both the server and client require. Let’s start by creating a User class as the most basic data object:
public class User implements Serializable {
private int userId;
private String name;
private int age;
// Getter/setter methods and toString() method for the above fields are omitted
}
Next, let’s create a UserService interface as the service interface. It defines 5 methods for creating User, querying User and related information, and deleting User:
public interface UserService {
User createUser(int userId, String name, int age);
User getUser(int userId);
String getUserName(int userId);
int getUserId(String name);
void deleteAll();
}
UserServiceImpl is the implementation class of the UserService interface. It manages User objects using an ArrayList collection. The specific implementation is as follows:
public class UserServiceImpl implements UserService {
// Manages all User objects
private List<User> users = new ArrayList<>();
@Override
public User createUser(int userId, String name, int age) {
System.out.println("createUser method");
User user = new User();
user.setUserId(userId);
user.setName(name);
user.setAge(age);
users.add(user); // Create a new User object and add it to the users collection
return user;
}
@Override
public User getUser(int userId) {
System.out.println("getUser method");
return users.stream().filter(u -> u.getUserId() == userId).findAny().get();
}
@Override
public String getUserName(int userId) {
System.out.println("getUserName method");
return getUser(userId).getName();
}
@Override
public int getUserId(String name) {
System.out.println("getUserId method");
return users.stream().filter(u -> u.getName().equals(name)).findAny().get().getUserId();
}
@Override
public void deleteAll() {
System.out.println("deleteAll");
users.clear(); // Clear the users collection
}
}
} }
The core of the entire user management business is roughly like this. Now let’s see how the server associates UserService with JSON-RPC.
First, we create the RpcServlet class, which is a subclass of HttpServlet and overrides the service() method of HttpServlet. As we know, when HttpServlet receives GET and POST requests, it will eventually call its service() method for processing. HttpServlet will also wrap the HTTP request and response into HttpServletRequest and HttpServletResponse and pass them into the service() method. In the implementation of RpcServlet, a JsonRpcServer is created, and the HTTP request is delegated to JsonRpcServer for processing in the service() method:
public class RpcServlet extends HttpServlet {
private JsonRpcServer rpcServer = null;
public RpcServlet() {
super();
// JsonRpcServer will invoke methods in UserServiceImpl according to json-rpc requests
rpcServer = new JsonRpcServer(new UserServiceImpl(), UserService.class);
}
@Override
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
rpcServer.handle(request, response);
}
}
Finally, we create a JsonRpcServer as the entry class for the server. In its main() method, Jetty is started as the web container. The implementation is as follows:
public class JsonRpcServer {
public static void main(String[] args) throws Throwable {
// Server listens on port 9999
Server server = new Server(9999);
// Associate with an existing context
WebAppContext context = new WebAppContext();
// Set the descriptor location
context.setDescriptor("/dubbo-demo/json-rpc-demo/src/main/webapp/WEB-INF/web.xml");
// Set the Web content context path
context.setResourceBase("/dubbo-demo/json-rpc-demo/src/main/webapp");
// Set the context path
context.setContextPath("/");
context.setParentLoaderPriority(true);
server.setHandler(context);
server.start();
server.join();
}
}
The web.xml configuration file used here is as follows:
<?xml version="1.0" encoding="UTF-8"?>
<web-app
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<servlet>
<servlet-name>RpcServlet</servlet-name>
<servlet-class>com.demo.RpcServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>RpcServlet</servlet-name>
<url-pattern>/rpc</url-pattern>
</servlet-mapping>
</web-app>
After completing the server-side code, now let’s continue to write the JSON-RPC client. In JsonRpcClient, a JsonRpcHttpClient is created, and the client sends requests to the server through the JsonRpcHttpClient:
public class JsonRpcClient {
private static JsonRpcHttpClient rpcHttpClient;
public static void main(String[] args) throws Throwable {
// Create JsonRpcHttpClient
rpcHttpClient = new JsonRpcHttpClient(new URL("http://127.0.0.1:9999/rpc"));
JsonRpcClient jsonRpcClient = new JsonRpcClient();
jsonRpcClient.deleteAll(); // Call the deleteAll() method to delete all Users
// Call the createUser() method to create a User
System.out.println(jsonRpcClient.createUser(1, "testName", 30));
// Call the getUser(), getUserName(), and getUserId() methods to query
System.out.println(jsonRpcClient.getUser(1));
System.out.println(jsonRpcClient.getUserName(1));
System.out.println(jsonRpcClient.getUserId("testName"));
}
public void deleteAll() throws Throwable {
// Call the deleteAll() method on the server
rpcHttpClient.invoke("deleteAll", null);
}
public User createUser(int userId, String name, int age) throws Throwable {
Object[] params = new Object[]{userId, name, age};
// Call the createUser() method on the server
return rpcHttpClient.invoke("createUser", params, User.class);
}
public User getUser(int userId) throws Throwable {
Integer[] params = new Integer[]{userId};
// Call the getUser() method on the server
return rpcHttpClient.invoke("getUser", params, User.class);
}
public String getUserName(int userId) throws Throwable {
Integer[] params = new Integer[]{userId};
// Call the getUserName() method on the server
return rpcHttpClient.invoke("getUserName", params, String.class);
}
public int getUserId(String name) throws Throwable {
String[] params = new String[]{name};
// Call the getUserId() method on the server
return rpcHttpClient.invoke("getUserId", params, Integer.class);
}
}
// Output:
// User{userId=1, name='testName', age=30}
// User{userId=1, name='testName', age=30}
// testName
// 1
AbstractProxyProtocol #
In the export()
method of AbstractProxyProtocol
, it first checks the exporterMap
cache based on the URL. If the query fails, it calls ProxyFactory.getProxy()
method to wrap the Invoker
into a proxy class of the business interface. Then, it starts the underlying ProxyProtocolServer
by calling the doExport()
method implemented by the subclass and initializes the serverMap
collection. The specific implementation is as follows:
public <T> Exporter<T> export(final Invoker<T> invoker) throws RpcException {
// First query the exporterMap collection
final String uri = serviceKey(invoker.getUrl());
Exporter<T> exporter = (Exporter<T>) exporterMap.get(uri);
if (exporter != null) {
if (Objects.equals(exporter.getInvoker().getUrl(), invoker.getUrl())) {
return exporter;
}
}
// Use ProxyFactory to create a proxy class and wrap the Invoker into a proxy class of the business interface
final Runnable runnable = doExport(proxyFactory.getProxy(invoker, true), invoker.getInterface(), invoker.getUrl());
// The Runnable returned by the doExport() method is a callback that will destroy the underlying Server and will be called in the unexport() method
exporter = new AbstractExporter<T>(invoker) {
public void unexport() {
super.unexport();
exporterMap.remove(uri);
if (runnable != null) {
runnable.run();
}
}
};
exporterMap.put(uri, exporter);
return exporter;
}
In the doExport()
method of the HttpProtocol
, similar to the implementation of the DubboProtocol
introduced earlier, it also starts a RemotingServer
. To adapt to various HTTP servers, such as Tomcat, Jetty, etc., Dubbo abstracts an HttpServer
interface at the Transporter layer.
Location of the dubbo-remoting-http module
The entry point of the dubbo-remoting-http module is the HttpBinder
interface, which is annotated with @SPI
and is an extension interface with three extension implementations. The default implementation is JettyHttpBinder
, as shown in the following diagram:
Inheritance diagram of JettyHttpBinder
The bind()
method in the HttpBinder
interface is annotated with @Adaptive
and selects the corresponding HttpBinder
extension implementation based on the server
parameter in the URL. Different HttpBinder
implementations return the corresponding HttpServer
implementation. The inheritance diagram of the HttpServer
is shown in the following diagram:
Inheritance diagram of HttpServer
Here, we take JettyHttpServer
as an example to briefly introduce the implementation of HttpServer
. In JettyHttpServer
, the Jetty Server is initialized, and the thread pool and request handler used by the Jetty Server are configured:
public JettyHttpServer(URL url, final HttpHandler handler) {
// Initialize the url and handler fields in AbstractHttpServer
super(url, handler);
this.url = url;
DispatcherServlet.addHttpHandler( // Add HttpHandler
url.getParameter(Constants.BIND_PORT_KEY, url.getPort()), handler);
// Create thread pool
int threads = url.getParameter(THREADS_KEY, DEFAULT_THREADS);
QueuedThreadPool threadPool = new QueuedThreadPool();
threadPool.setDaemon(true);
threadPool.setMaxThreads(threads);
threadPool.setMinThreads(threads);
// Create Jetty Server
server = new Server(threadPool);
// Create ServerConnector and specify the bound ip and port
ServerConnector connector = new ServerConnector(server);
String bindIp = url.getParameter(Constants.BIND_IP_KEY, url.getHost());
if (!url.isAnyHost() && NetUtils.isValidLocalHost(bindIp)) {
connector.setHost(bindIp);
}
connector.setPort(url.getParameter(Constants.BIND_PORT_KEY, url.getPort()));
server.addConnector(connector);
// Create ServletHandler and associate it with Jetty Server, DispatcherServlet handles all requests
ServletHandler servletHandler = new ServletHandler();
ServletHolder servletHolder = servletHandler.addServletWithMapping(DispatcherServlet.class, "/*");
servletHolder.setInitOrder(2);
// Create ServletContextHandler and associate it with Jetty Server
ServletContextHandler context = new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS);
context.setServletHandler(servletHandler);
ServletManager.getInstance().addServletContext(url.getParameter(Constants.BIND_PORT_KEY, url.getPort()), context.getServletContext());
server.start();
}
We can see that all requests received by JettyHttpServer
will be delegated to DispatcherServlet
, which is an HttpServlet
implementation. The service()
method of DispatcherServlet
delegates the request to the HttpHandler
corresponding to the port:
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// Query the HttpHandler object corresponding to the port from the HANDLERS collection
HttpHandler handler = HANDLERS.get(request.getLocalPort());
if (handler == null) { // No HttpHandler implementation for the port
response.sendError(HttpServletResponse.SC_NOT_FOUND, "Service not found.");
} else { // Delegate the request to the HttpHandler object
handler.handle(request, response);
}
}
handler.handle(request, response);
}
}
After understanding the abstraction of HttpServer in Dubbo and the core of JettyHttpServer, let's continue analyzing the doExport() method in HttpProtocol.
In the doExport() method of HttpProtocol, a HttpServer object is created using HttpBinder and put into the serverMap to handle HTTP requests. The initialization of HttpServer and the HttpHandler used to handle requests are implemented as inner classes in HttpProtocol. Similar HttpHandler implementation can be found in other RPC protocol implementations based on the HTTP protocol, as shown in the following diagram:
![Drawing 4.png](../images/CgqCHl-hFGCARUTkAABNZnY-dJg331.png)
In the handle() method of the HttpHandler in the InternalHandler class in HttpProtocol, the request is delegated to the JsonRpcServer object recorded in the skeletonMap collection for processing:
```java
public void handle(HttpServletRequest request, HttpServletResponse response) throws ServletException {
String uri = request.getRequestURI();
JsonRpcServer skeleton = skeletonMap.get(uri);
if (cors) { ... // handle cross-origin issue }
if (request.getMethod().equalsIgnoreCase("OPTIONS")) {
response.setStatus(200); // handle OPTIONS request
} else if (request.getMethod().equalsIgnoreCase("POST")) {
// only handle POST request
RpcContext.getContext().setRemoteAddress(request.getRemoteAddr(), request.getRemotePort());
skeleton.handle(request.getInputStream(), response.getOutputStream());
} else { // other types of method requests, such as GET request, return 500 directly
response.setStatus(500);
}
}
The JsonRpcServer object in the skeletonMap collection is initialized together with the HttpServer object in the doExport() method. Finally, let’s take a look at the implementation of the doExport() method in HttpProtocol:
protected <T> Runnable doExport(final T impl, Class<T> type, URL url) throws RpcException {
String addr = getAddr(url);
// check serverMap cache first
ProtocolServer protocolServer = serverMap.get(addr);
if (protocolServer == null) { // cache check failed
// create HttpServer, note that the InternalHandler implementation is passed in
RemotingServer remotingServer = httpBinder.bind(url, new InternalHandler(url.getParameter("cors", false)));
serverMap.put(addr, new ProxyProtocolServer(remotingServer));
}
// create JsonRpcServer object and record the mapping between URL and JsonRpcServer in the skeletonMap collection
final String path = url.getAbsolutePath();
final String genericPath = path + "/" + GENERIC_KEY;
JsonRpcServer skeleton = new JsonRpcServer(impl, type);
JsonRpcServer genericServer = new JsonRpcServer(impl, GenericService.class);
skeletonMap.put(path, skeleton);
skeletonMap.put(genericPath, genericServer);
return () -> { // return a Runnable callback executed in the unexport() method in Exporter
skeletonMap.remove(path);
skeletonMap.remove(genericPath);
};
}
After introducing the implementation of service export in HttpProtocol, let’s take a look at the implementation of service reference related methods in HttpProtocol, namely the protocolBindingRefer() method. This method first creates a proxy for the business interface using the doRefer() method. It integrates with the jsonrpc4j library and Spring, and creates a JsonRpcHttpClient object in its afterPropertiesSet() method:
public void afterPropertiesSet() {
... ... // omitting the ObjectMapper and other objects
try {
// create JsonRpcHttpClient for sending json-rpc requests
jsonRpcHttpClient = new JsonRpcHttpClient(objectMapper, new URL(getServiceUrl()), extraHttpHeaders);
jsonRpcHttpClient.setRequestListener(requestListener);
jsonRpcHttpClient.setSslContext(sslContext);
jsonRpcHttpClient.setHostNameVerifier(hostNameVerifier);
} catch (MalformedURLException mue) {
throw new RuntimeException(mue);
}
}
Now let’s look at the implementation of the doRefer() method:
protected <T> T doRefer(final Class<T> serviceType, URL url) throws RpcException {
final String generic = url.getParameter(GENERIC_KEY);
final boolean isGeneric = ProtocolUtils.isGeneric(generic) || serviceType.equals(GenericService.class);
JsonProxyFactoryBean jsonProxyFactoryBean = new JsonProxyFactoryBean();
... // omitting other initialization logic
jsonProxyFactoryBean.afterPropertiesSet();
// return a proxy object of type serviceType
return (T) jsonProxyFactoryBean.getObject();
}
In the protocolBindingRefer() method of AbstractProxyProtocol, the proxy object returned by the doRefer() method is converted to an Invoker object using the ProxyFactory.getInvoker() method, and it is recorded in the Invokers collection, as shown in the following code:
protected <T> Invoker<T> protocolBindingRefer(final Class<T> type, final URL url) throws RpcException {
final Invoker<T> target = proxyFactory.getInvoker(doRefer(type, url), type, url);
Invoker<T> invoker = new AbstractInvoker<T>(type, url) {
@Override
protected Result doInvoke(Invocation invocation) throws Throwable {
Result result = target.invoke(invocation);
// omit exception handling logic
return result;
}
};
invokers.add(invoker); // add Invoker to the invokers collection
return invoker;
}
Summary #
In this lesson, we have focused on how to achieve cross-language invocation in Dubbo using the “HTTP protocol + JSON-RPC” approach. First, we introduced the basic format of requests and responses in JSON-RPC, as well as the basic usage of the jsonrpc4j library. Then, we also detailed the core classes in Dubbo, such as AbstractProxyProtocol and HttpProtocol, and analyzed the implementation of the “HTTP protocol + JSON-RPC” approach in Dubbo.