📜 ⬆️ ⬇️

The easiest way to support the integration of java-client with java-server

When solving everyday tasks with a JavaFX desktop application interface, you have to make a request to a web server anyway. After the times of J2EE and the terrible abbreviation RMI, much has changed, and the calls to the server have become more lightweight. The standard for web sockets and its exchange with simple text messages of any content is suitable for such a problem. But the problem of corporate applications is that the diversity and number of requests makes the creation and tracking of EndPoint- s in the presence of separate business services in a terrible routine and adds extra lines of code.


But what if we take a strictly typed strategy with RMI, where there was a standard java interface between the client and the server, describing methods, arguments and return types, where a couple of annotations were added, and the client didn’t even notice that a call was coming over the network? What if the network does not transmit just text, but java-serialized objects? What if we add to this strategy the ease of web sockets and their advantages of being able to push client calls from the server? What if the asynchronous response of the web socket for the client is curbed into the usual blocking call, and for the deferred call add the ability to return Future or even Completable Future ? What if you add the ability to subscribe a client to certain events from the server? What if on the server to have a session and connect to each client? It can turn out to be a nice transparent bundle familiar to any java programmer, since magic will be hidden behind the interface, and in testing interfaces it is easy to substitute. But this is all not for loaded applications that process, for example, quotes from the stock exchange.


In corporate applications from my practice, the speed of the execution of the sql query and the transfer of selectable data from the DBMS is incommensurable with the overhead of serialization and reflexive calls. Moreover, the terrible tracing of EJB-calls, which complements the execution time up to 4–10 ms, even for the simplest request is not a problem, since the duration of typical requests lies in the corridor from 50ms to 250ms.


Let's start with the simplest - let's use the Proxy-object pattern to implement the magic behind the interface methods. Suppose that I have a method for retrieving the user's correspondence history with his opponents:


public interface ServerChat{ Map<String, <List<String>> getHistory(Date when, String login); } 

We will create a proxy object using standard java tools and call the required method on it:


 public class ClientProxyUtils { public static BiFunction<String, Class, RMIoverWebSocketProxyHandler> defaultFactory = RMIoverWebSocketProxyHandler::new; public static <T> T create(Class<T> clazz, String jndiName) { T f = (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, defaultFactory.apply(jndiName, clazz)); return f; } } //    //... ServerChat chat = ClientProxyUtils.create(ServerChat.class, "java:global/test_app/ServerChat"); Map<String, <List<String>> history = chat.getHistory(new Date(), "tester"); //... //    

If, at the same time, we set up the factories, and inject an instance of the proxy object through the interface via cdi-injection, then we get magic in its pure form. In this case, open / close a socket each time is not necessary. On the contrary, in my applications, the socket is constantly open and ready to receive and process messages. Now it’s worth seeing what happens in RMIoverWebSocketProxyHandler :


 public class RMIoverWebSocketProxyHandler implements InvocationHandler { public static final int OVERHEAD = 0x10000; public static final int CLIENT_INPUT_BUFFER_SIZE = 0x1000000;// 16mb public static final int SERVER_OUT_BUFFER_SIZE = CLIENT_INPUT_BUFFER_SIZE - OVERHEAD; String jndiName; Class interfaze; public RMIoverWebSocketProxyHandler(String jndiName, Class interfaze) { this.jndiName = jndiName; this.interfaze = interfaze; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Request request = new Request(); request.guid = UUID.randomUUID().toString(); request.jndiName = jndiName; request.methodName = method.getName(); request.args = args; request.argsType = method.getParameterTypes(); request.interfaze = interfaze; WaitList.putRequest(request, getRequestRunnable(request)); checkError(request, method); return request.result; } public static Runnable getRequestRunnable(Request request) throws IOException { final byte[] requestBytes = write(request); return () -> { try { sendByByteBuffer(requestBytes, ClientRMIHandler.clientSession); } catch (IOException ex) { WaitList.clean(); ClientRMIHandler.notifyErrorListeners(new RuntimeException(FATAL_ERROR_MESSAGE, ex)); } }; } public static byte[] write(Object object) throws IOException { try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream ous = new ObjectOutputStream(baos)) { ous.writeObject(object); return baos.toByteArray(); } } public static void sendByByteBuffer(byte[] responseBytes, Session wsSession) throws IOException { ... } public static void checkError(Request request, Method method) throws Throwable { ... } @FunctionalInterface public interface Callback<V> { V call() throws Throwable; } } 

But actually the client EndPoint itself :


 @ClientEndpoint public class ClientRMIHandler { public static volatile Session clientSession; @OnOpen public void onOpen(Session session) { clientSession = session; } @OnMessage public void onMessage(ByteBuffer message, Session session) { try { final Object readInput = read(message); if (readInput instanceof Response) { standartResponse((Response) readInput); } } catch (IOException ex) { WaitList.clean(); notifyErrorListeners(new RuntimeException(FATAL_ERROR_MESSAGE, ex)); } } private void standartResponse(final Response response) throws RuntimeException { if (response.guid == null) { if (response.error != null) { notifyErrorListeners(response.error); return; } WaitList.clean(); final RuntimeException runtimeException = new RuntimeException(FATAL_ERROR_MESSAGE); notifyErrorListeners(runtimeException); throw runtimeException; } else { WaitList.processResponse(response); } } @OnClose public void onClose(Session session, CloseReason closeReason) { WaitList.clean(); } @OnError public void onError(Session session, Throwable error) { notifyErrorListeners(error); } private static Object read(ByteBuffer message) throws ClassNotFoundException, IOException { Object readObject; byte[] b = new byte[message.remaining()]; // don't use message.array() becouse it is optional message.get(b); try (ByteArrayInputStream bais = new ByteArrayInputStream(b); ObjectInputStream ois = new ObjectInputStream(bais)) { readObject = ois.readObject(); } return readObject; } } 

Thus, to call any method of a proxy object, we take an open socket session, we send the arguments and the details of the method that we need to call on the server, and wait until we receive a response with the previously specified guid. When we receive a response, we check for an exception, and, if everything is fine, then we put the result of the response in Request and notify the thread waiting for a response in the WaitList. Realization of such WaitList I will not bring, as it is trivial. At best , the pending thread will continue to work after the WaitList.putRequest line (request, getRequestRunnable (request)); . After waking up, the thread will check for exceptions, declared in the throws section, and return the result via return .


The above code examples are excerpts from the library, which is not yet ready for display on github. Need to work on licensing issues. It makes sense to look at the implementation of the server side in the source code itself after its publication. But there is nothing special there - a search for an ejb object that implements the specified interface is performed, in jndi through InitialContext and a reflexive call is made on the passed details. There is certainly a lot of interesting things there, but such a volume of information will not fit into any article. In the library itself, the above blocking call script was implemented primarily because it is the simplest. Support for non-blocking calls via Future and CompletableFuture <> was added later. The library is successfully used in all products with a desktop java-client. I would be glad if someone shares the experience of opening the source code, which is linked with gnu gpl 2.0 ( tyrus-standalone-client ).


As a result, it is easy to build a hierarchy of the method call using standard IDE tools up to the UI form itself, on which the button handler pulls remote services. At the same time, we obtain a strict typification and a weak connection between the client and server integration layers. The structure of the application's source code is divided into a client, a server, and a kernel, which is connected by a dependency to both the client and the server. It is there that all remote interfaces and transmitted objects are located. And the routine task of the developer associated with a query in the database requires a new method in the interface and its implementation on the server side. In my opinion, it's much easier ...


')

Source: https://habr.com/ru/post/422073/


All Articles