The Simplest Way To Support The Integration Of A Java Client With A Java Server

The Simplest Way To Support The Integration Main Logo

The Simplest Way To Support The Integration Of A Java Client With A Java Server

When solving everyday tasks with the interface of a desktop application implemented in JavaFX, you must, in any case, make a request to the web server.

After the J2EE times and the terrible abbreviation RMI, much has changed, and calls to the server have become more lightweight. One can not help the standard of web-sockets and its exchange of simple text messages of any content. But the problem with enterprise applications is that the diversity and number of queries turn the creation and tracking of EndPoints when there are separately allocated business services in an eerie routine and adds extra lines of code.

  • And what if we take as a basis a strongly 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 did not even notice that the call was on the network?
  • What if we transmit not simply text, but serialized java-objects on the network?
  • What if we add to this strategy the ease of Web Sockets and their advantages of pushing the client from the server side?
  • What if the asynchrony of the Web socket responses for the client is curbed in the usual blocking call, and for the delayed call, add the possibility of returning Future or even CompletableFuture?
  • What if we add the ability to subscribe a client to certain events from the server?
  • What if the server has a session and connection to each client? It can turn out a good transparent bunch that is familiar to any Java programmer, as the interface will hide magic, and in testing, interfaces can easily be changed. But that’s just all this is not for loaded applications that process, for example, quotes from a stock exchange.

Incorporate applications, from our experience, the speed of SQL-query execution and the transfer of selectable data from the DBMS are incommensurable with the overhead of serialization and reflexive calls. Moreover, the terrible EJB-call tracing, supplementing the execution time to 4-10 ms, even for the most simple query is not a problem, since the duration of typical requests is in the corridor from 50 ms to 250 ms.

Let’s start with the simplest – use the Proxy-object pattern to implement the magic behind the interface methods. Suppose that we have a method to get the history of the user’s correspondence with his opponents:


public interface ServerChat{

Map<String, <List<String>> getHistory(Date when, String login);
}

  • Proxy-object will be created using standard Java tools, and we will call the necessary 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;
}

}

// connect and open the socket
// ...

ServerChat chat = ClientProxyUtils.create (ServerChat.class, "java: global / test_app / ServerChat");
Map <String, <List <String >> history = chat.getHistory (new Date (), "tester");

// ...
// closing the socket and connection

If you configure the factories and configure the proxy object instance via the CDI-injection interface, you get magic in its pure form. It is not necessary to open/close the socket every time. On the contrary, in our applications, the socket is always open and ready to receive and process messages. Now you should see 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:


@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 ()]; // do not 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 the proxy object, we take an open session of the socket, the helmet passed arguments and the requisites of the method that must be called on the server, and wait until the response with the above specified in the request is given. When we receive a response, we check for an exception, and if everything is fine, we put the response result in Request and notify the thread waiting for the answer in the Waitlist.

  • The implementation of such a WaitList will not be given since it is trivial. The expecting thread will at best continue to work after the line WaitList.putRequest (request, getRequestRunnable (request)). After waking up, the thread will check for the exceptions declared in the throws section, and return the result via return.

The code examples given are an excerpt from the library, which is not yet ready for layout on GitHub. It is necessary to work out licensing issues. The server-side implementation makes sense to look already in the source code after its publication. But there is nothing special there – the search for an EJB-object that implements the specified interface, in jndi through InitialContext, is performed and a reflexive call is made on the transferred details.

  • There is certainly a lot more interesting, but in no article, such a volume of information will not fit. In the library itself, the above script of the blocking call was implemented in the first place, since it is the simplest. Later, support for nonblocking calls via Future and CompletableFuture <> was added. The library is successfully used in all products with a desktop java-client. We 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 not difficult to build a hierarchy of method invocation by standard IDE tools to the very UI-form, on which the button handler pulls remote services. In doing so, we obtain strict typing and weak connectivity of the client-server integration layer. The source code structure of the application is divided into a client, a server and a kernel, which is connected by a dependency both on the client and to the server. It is in it that all remote interfaces and transmitted objects are located. A routine task of the developer associated with the query in the database requires a new method in the interface and its implementation on the server side.