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 CompletableFuture? 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:


    publicinterfaceServerChat{
        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:


    publicclassClientProxyUtils{
        publicstatic BiFunction<String, Class, RMIoverWebSocketProxyHandler> defaultFactory = RMIoverWebSocketProxyHandler::new;
        publicstatic <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 you set up the factories, and inject an instance of the proxy-object through the interface via cdi-injection, then you 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 :


    publicclassRMIoverWebSocketProxyHandlerimplementsInvocationHandler{
        publicstaticfinalint OVERHEAD = 0x10000;
        publicstaticfinalint CLIENT_INPUT_BUFFER_SIZE = 0x1000000;// 16mbpublicstaticfinalint SERVER_OUT_BUFFER_SIZE = CLIENT_INPUT_BUFFER_SIZE - OVERHEAD;
        String jndiName;
        Class interfaze;
        publicRMIoverWebSocketProxyHandler(String jndiName, Class interfaze){
            this.jndiName = jndiName;
            this.interfaze = interfaze;
        }
        @Overridepublic 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;
        }
        publicstatic Runnable getRequestRunnable(Request request)throws IOException {
            finalbyte[] requestBytes = write(request);
            return () -> {
                try {
                    sendByByteBuffer(requestBytes, ClientRMIHandler.clientSession);
                } catch (IOException ex) {                
                    WaitList.clean();
                    ClientRMIHandler.notifyErrorListeners(new RuntimeException(FATAL_ERROR_MESSAGE, ex));
                }
            };
        }
        publicstaticbyte[] write(Object object) throws IOException {
            try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    ObjectOutputStream ous = new ObjectOutputStream(baos)) {
                ous.writeObject(object);
                return baos.toByteArray();
            }
        }
        publicstaticvoidsendByByteBuffer(byte[] responseBytes, Session wsSession)throws IOException {
            ...        
        }
        publicstaticvoidcheckError(Request request, Method method)throws Throwable {
            ...
        }
        @FunctionalInterfacepublicinterfaceCallback<V>  {
            V call()throws Throwable;
        }
    }
    

    But actually the client EndPoint itself :


    @ClientEndpointpublicclassClientRMIHandler{
        publicstaticvolatile Session clientSession;
        @OnOpenpublicvoidonOpen(Session session){
            clientSession = session;
        }
        @OnMessagepublicvoidonMessage(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));
            }
        }
        privatevoidstandartResponse(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);
            }
        }
        @OnClosepublicvoidonClose(Session session, CloseReason closeReason){    
            WaitList.clean();  
        }
        @OnErrorpublicvoidonError(Session session, Throwable error){
            notifyErrorListeners(error);
        }
        privatestatic Object read(ByteBuffer message)throws ClassNotFoundException, IOException {
            Object readObject;
            byte[] b = newbyte[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 in the Request the result of the response 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 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 ...


    Also popular now: