Der einfachste Weg, die Integration eines Java-Clients in einen Java-Server zu unterstützen

Wenn Sie alltägliche Aufgaben mit der Oberfläche einer JavaFX-basierten Desktop-Anwendung lösen, müssen Sie trotzdem eine Anfrage an den Webserver stellen. Nach den Tagen von J2EE und der schrecklichen Abkürzung RMI hat sich viel geändert, und Serveraufrufe sind leichter geworden. Der Standard für Web-Sockets und den Austausch einfacher Textnachrichten beliebiger Inhalte ist für ein solches Problem geeignet. Das Problem bei Unternehmensanwendungen besteht jedoch darin, dass das Erstellen und Verfolgen von EndPoints mit separat ausgewählten Geschäftsdiensten aufgrund der Vielfalt und Anzahl der Anforderungen eine schreckliche Routine darstellt und zusätzliche Codezeilen hinzufügt.


Was aber, wenn wir eine streng typisierte Strategie mit RMI als Basis verwenden, bei der es eine Standard-Java- Schnittstelle zwischen Client und Server gab, die Methoden, Argumente und Rückgabetypen beschreibt, bei der einige Anmerkungen hinzugefügt wurden und der Client nicht einmal auf magische Weise bemerkte, dass der Anruf über das Netzwerk getätigt wurde? Was ist, wenn nicht nur Text, sondern auch serialisierte Java-Objekte über das Netzwerk übertragen werden? Was ist, wenn wir dieser Strategie die Leichtigkeit von Web-Sockets und ihre Vorteile der Möglichkeit von Client-Push-Anrufen vom Server hinzufügen? Was ist, wenn die asynchrone Antwort des Web-Sockets für den Client auf den üblichen blockierenden Anruf beschränkt ist und bei einem verzögerten Anruf die Möglichkeit hinzugefügt wird, Future oder sogar CompletableFuture zurückzugeben ? Was ist, wenn wir die Möglichkeit hinzufügen, den Client für bestimmte Ereignisse vom Server zu abonnieren? Was ist, wenn der Server eine Sitzung und eine Verbindung zu jedem Client hat? Es kann sich als ein gutes transparentes Paket herausstellen, das jedem Java-Programmierer vertraut ist, da Magie hinter der Benutzeroberfläche verborgen ist und beim Testen die Schnittstellen leicht ersetzt werden können. Dies ist jedoch nicht alles für geladene Anwendungen, die beispielsweise Börsenkurse verarbeiten.


In Unternehmensanwendungen aus meiner Praxis ist die Geschwindigkeit der Ausführung einer SQL-Abfrage und der Übertragung ausgewählter Daten aus einem DBMS nicht mit dem Aufwand für Serialisierung und reflektierende Aufrufe vereinbar. Darüber hinaus ist eine schreckliche Spur von EJB-Aufrufen, die die Ausführungszeit selbst für die einfachste Anforderung auf 4 bis 10 ms ergänzt, kein Problem, da die Dauer typischer Anforderungen im Korridor zwischen 50 ms und 250 ms liegt.


Beginnen wir mit dem einfachsten - wir werden das Proxy-Objektmuster verwenden, um die Magie hinter den Schnittstellenmethoden zu implementieren. Angenommen, ich habe eine Methode, um den Verlauf der Korrespondenz eines Benutzers mit seinen Gegnern zu ermitteln:


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

Wir werden das Proxy-Objekt mit Standard-Java-Tools erstellen und die erforderliche Methode dafür aufrufen:


 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"); //... //    

Wenn Sie gleichzeitig Fabriken einrichten und die Proxy-Objektinstanz über die Schnittstelle durch CDI-Injection implementieren, erhalten Sie die Magie in ihrer reinsten Form. Gleichzeitig ist es nicht erforderlich, jedes Mal eine Steckdose zu öffnen / zu schließen. Im Gegenteil, in meinen Anwendungen ist der Socket ständig geöffnet und bereit, Nachrichten zu empfangen und zu verarbeiten. Jetzt lohnt es sich zu sehen, was in RMIoverWebSocketProxyHandler passiert:


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

Und hier ist der eigentliche Client EndPoint selbst :


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

Um eine beliebige Methode des Proxy-Objekts aufzurufen, nehmen wir eine Open-Socket-Sitzung, senden die übergebenen Argumente und Details der Methode, die auf dem Server aufgerufen werden muss, und warten auf die Antwort mit dem in der Anforderung angegebenen Leitfaden. Nach Erhalt der Antwort suchen wir nach einer Ausnahme. Wenn alles in Ordnung ist, setzen wir das Ergebnis der Antwort in Request und benachrichtigen den Stream, der auf eine Antwort wartet, in WaitList. Ich werde die Implementierung einer solchen Warteliste nicht geben, da sie trivial ist. Der wartende Thread funktioniert bestenfalls nach der Zeile WaitList.putRequest (request, getRequestRunnable (request)) weiter. . Nach dem Aufwachen sucht der Thread nach den im Abschnitt " Throws" deklarierten Ausnahmen und gibt das Ergebnis über " return" zurück .


Die obigen Codebeispiele sind Auszüge aus der Bibliothek, die noch nicht für die Veröffentlichung auf Github bereit ist. Es ist notwendig, Lizenzprobleme zu lösen. Es ist sinnvoll, die Implementierung der Serverseite bereits nach ihrer Veröffentlichung im Quellcode selbst zu betrachten. Dort gibt es jedoch nichts Besonderes - es wird nach einem ejb-Objekt gesucht, das die angegebene Schnittstelle in jndi über InitialContext implementiert, und es wird ein reflektierender Aufruf unter Verwendung der übertragenen Details durchgeführt. Natürlich gibt es noch viele interessante Dinge, aber ein solches Informationsvolumen passt in keinen Artikel. In der Bibliothek selbst wurde zunächst das oben genannte Blockierungsaufrufskript implementiert, da es das einfachste ist. Später wurde die Unterstützung für nicht blockierende Anrufe über Future und CompletableFuture <> hinzugefügt. Die Bibliothek wird in allen Produkten mit einem Desktop-Java-Client erfolgreich verwendet. Ich würde mich freuen, wenn jemand seine Erfahrungen mit dem Öffnen von Quellcode teilt, der mit gnu gpl 2.0 ( Tyrus-Standalone-Client ) verknüpft ist.


Daher ist es nicht schwierig, eine Hierarchie des Methodenaufrufs mit Standard-IDE-Tools bis zum UI-Formular selbst zu erstellen, auf dem der Button-Handler Remotedienste abruft. Gleichzeitig erhalten wir eine strikte Typisierung und schwache Konnektivität der Client- und Server-Integrationsschicht. Die Struktur des Anwendungsquellcodes ist in einen Client, einen Server und einen Kernel unterteilt, die durch Abhängigkeit sowohl vom Client als auch vom Server verbunden sind. Darin befinden sich alle Remote- Schnittstellen und übertragenen Objekte. Die mit der Abfrage in der Datenbank verknüpfte Entwicklerroutine erfordert eine neue Methode in der Schnittstelle und deren Implementierung auf der Serverseite. Meiner Meinung nach viel einfacher ...

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


All Articles