Al resolver tareas cotidianas con la interfaz de una aplicación de escritorio basada en JavaFX, debe realizar una solicitud al servidor web de todos modos. Después de los tiempos de J2EE y la terrible abreviatura RMI, mucho ha cambiado y las llamadas al servidor se han vuelto más livianas. El estándar para los sockets web y su intercambio de mensajes de texto simples de cualquier contenido es adecuado para tal problema. Pero el problema con las aplicaciones empresariales es que la diversidad y la cantidad de solicitudes hace que la creación y el seguimiento de EndPoints con servicios comerciales seleccionados por separado sea una rutina terrible y agrega líneas de código adicionales.
Pero, ¿qué sucede si tomamos una estrategia estrictamente tipada con RMI como base, donde había una interfaz estándar de Java entre el cliente y el servidor que describe métodos, argumentos y tipos de retorno, donde se agregaron un par de anotaciones y el cliente ni siquiera notó mágicamente que la llamada se estaba haciendo a través de la red? ¿Qué sucede si no solo el texto, sino que los objetos serializados de Java se transmiten a través de la red? ¿Qué pasa si agregamos a esta estrategia la facilidad de los sockets web y sus ventajas de la posibilidad de llamadas push del cliente desde el servidor? ¿Qué sucede si la respuesta asíncrona del socket web para el cliente se limita a la llamada de bloqueo habitual y, para una llamada retrasada, agrega la posibilidad de devolver Future o incluso CompletableFuture ? ¿Qué sucede si agregamos la capacidad de suscribir al cliente a ciertos eventos del servidor? ¿Qué pasa si el servidor tiene una sesión y conexión con cada cliente? Puede resultar un buen paquete transparente familiar para cualquier programador de Java, ya que la magia estará oculta detrás de la interfaz, y al probar las interfaces se puede reemplazar fácilmente. Pero eso no es todo para las aplicaciones cargadas que procesan, por ejemplo, cotizaciones bursátiles.
En las aplicaciones corporativas de mi práctica, la velocidad de ejecución de una consulta SQL y la transferencia de datos seleccionados de un DBMS es inconmensurable con la sobrecarga de la serialización y las llamadas reflexivas. Además, un terrible rastro de llamadas EJB, que complementa el tiempo de ejecución a 4-10 ms incluso para la solicitud más simple, no es un problema, ya que la duración de las solicitudes típicas está en el corredor de 50 ms a 250 ms.
Comencemos con lo más simple: utilizaremos el patrón de objeto Proxy para implementar la magia detrás de los métodos de interfaz. Supongamos que tengo un método para obtener el historial de la correspondencia de un usuario con sus oponentes:
public interface ServerChat{ Map<String, <List<String>> getHistory(Date when, String login); }
Crearemos el objeto proxy usando herramientas estándar de Java y llamaremos al método necesario:
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; } }
Si al mismo tiempo configura fábricas e implementa la instancia del objeto proxy a través de la interfaz a través de inyección cdi, obtendrá la magia en su forma más pura. Al mismo tiempo, no es necesario abrir / cerrar un enchufe cada vez. Por el contrario, en mis aplicaciones, el socket está constantemente abierto y listo para recibir y procesar mensajes. Ahora vale la pena ver lo que sucede en RMIoverWebSocketProxyHandler :
public class RMIoverWebSocketProxyHandler implements InvocationHandler { public static final int OVERHEAD = 0x10000; public static final int CLIENT_INPUT_BUFFER_SIZE = 0x1000000;
Y aquí está el cliente real EndPoint en sí :
@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()];
Por lo tanto, para llamar a cualquier método del objeto proxy, tomamos una sesión de socket abierto, enviamos los argumentos pasados y los detalles del método que se debe invocar en el servidor y esperamos la respuesta con la guía especificada en la solicitud que se recibirá. Al recibir la respuesta, verificamos una excepción, y si todo está bien, colocamos el resultado de la respuesta en Solicitud y notificamos a la secuencia que está esperando una respuesta en la Lista de espera. No daré la implementación de dicha lista de espera, ya que es trivial. El hilo de espera, en el mejor de los casos, continuará funcionando después de la línea WaitList.putRequest (request, getRequestRunnable (request)); . Después de despertarse, el hilo verificará las excepciones declaradas en la sección de lanzamientos y devolverá el resultado a través de retorno .
Los ejemplos de código anteriores son extractos de la biblioteca, que aún no está lista para su publicación en github. Es necesario resolver los problemas de licencia. Tiene sentido mirar la implementación del lado del servidor que ya está en el código fuente en sí después de su publicación. Pero no hay nada especial allí: se realiza una búsqueda de un objeto ejb que implementa la interfaz especificada en jndi a través de InitialContext y se realiza una llamada reflexiva utilizando los detalles transmitidos. Por supuesto, todavía hay muchas cosas interesantes, pero ese volumen de información no cabe en ningún artículo. En la propia biblioteca, el script de llamada de bloqueo anterior se implementó en primer lugar, ya que es el más simple. Posteriormente se agregó soporte para llamadas sin bloqueo a través de Future y CompletableFuture <> . La biblioteca se utiliza con éxito en todos los productos con un cliente Java de escritorio. Me alegraría si alguien comparte su experiencia de abrir el código fuente que está vinculado con gnu gpl 2.0 ( tyrus-standalone-client ).
Como resultado, no es difícil construir una jerarquía de invocación de métodos utilizando herramientas IDE estándar hasta el formulario de interfaz de usuario, en el que el controlador de botones extrae servicios remotos. Al mismo tiempo, obtenemos una mecanografía estricta y una conectividad débil de la capa de integración de cliente y servidor. La estructura del código fuente de la aplicación se divide en un cliente, un servidor y un núcleo, que está conectado por adicción tanto al cliente como al servidor. Es allí donde se ubican todas las interfaces remotas y los objetos transferidos. Y la rutina del desarrollador asociada con la consulta en la base de datos requiere un nuevo método en la interfaz y su implementación en el lado del servidor. En mi opinión, mucho más fácil ...