支持将Java客户端与Java服务器集成的最简单方法

使用基于JavaFX的桌面应用程序界面解决日常任务时,无论如何,您都必须向Web服务器发出请求。 在J2EE和可怕的缩写RMI时代之后,发生了很多变化,服务器调用变得更加轻量级。 Web套接字标准及其交换任何内容的简单文本消息的标准都适用于此问题。 但是企业应用程序的问题在于,请求的多样性和数量使使用单独选择的业务服务创建和跟踪端点成为一个糟糕的例程,并增加了额外的代码行。


但是,如果我们采用以RMI为基础的严格类型化策略,在客户端和服务器之间有一个标准的Java 接口来描述方法,参数和返回类型,并添加了两个批注,而客户端甚至没有神奇地注意到该调用是通过网络进行的,该怎么办? 如果不仅仅是文本,而是序列化的Java对象通过网络传输怎么办? 如果我们在此策略中增加了Web套接字的便利性以及它们从服务器进行客户端推送调用的可能性的优势,那该怎么办? 如果客户端的Web套接字的异步响应被限制在通常的阻塞调用中,而对于延迟的调用,又增加了返回Future甚至CompletableFuture的可能性,该怎么办? 如果我们增加了向客户端订阅服务器中某些事件的功能,该怎么办? 如果服务器与每个客户端建立会话并连接怎么办? 它可以证明是任何Java程序员都熟悉的透明透明包,因为魔术将隐藏在接口后面,并且在测试时可以轻松替换接口。 但是,对于处理诸如股市行情之类的已加载应用程序,这还不是全部。


根据我的实践,在企业应用程序中,执行sql查询和从DBMS传输选定数据的速度与序列化和反射调用的开销是无法比拟的。 而且,对EJB调用的跟踪非常糟糕,即使对于最简单的请求,执行时间也增加到4-10毫秒,这不是问题,因为典型请求的持续时间从50毫秒到250毫秒。


让我们从最简单的开始-我们将使用Proxy对象模式来实现接口方法背后的魔力。 假设我有一种获取用户与其对手的对应关系的历史的方法:


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

我们将使用标准的Java工具创建代理对象,并在其上调用必要的方法:


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

如果同时建立工厂,并通过接口通过cdi-injection实现代理对象实例,那么您将获得最纯粹的魔力。 同时,根本不需要每次都打开/关闭插座。 相反,在我的应用程序中,套接字一直处于打开状态,可以接收和处理消息。 现在值得一看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; } } 

这是实际的客户端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()]; // 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; } } 

因此,要调用代理对象的任何方法,我们将进行一个开放的套接字会话,发送传递的参数和必须在服务器上调用的方法的详细信息,然后等待请求中指定的指南的响应被接收。 收到响应后,我们将检查异常,如果一切正常,则将响应结果放入Request中,并在WaitList中通知正在等待响应的流。 我不会给出此类WaitList的实现,因为它是微不足道的。 等待线程最多只能在WaitList.putRequest(请求,getRequestRunnable(请求))行之后继续工作 。 唤醒后,线程将检查throws部分中声明的异常 ,并将通过return返回结果。


上面的代码示例摘自该库的摘录,该库尚未准备好在github上发布。 有必要解决许可问题。 在发布之后,在源代码本身中查看服务器端的实现是有意义的。 但是那里没有什么特别的-搜索通过初始上下文在jndi中实现指定接口的ejb对象,并使用传输的详细信息进行反射调用。 当然,仍然有很多有趣的事情,但是如此大量的信息将不适用于任何文章。 在库本身中,首先执行了上述阻塞调用脚本,因为它是最简单的。 后来增加了对通过FutureCompletableFuture <>进行的非阻塞调用的支持。 该库已成功在带有桌面Java客户端的所有产品中使用。 如果有人分享与gnu gpl 2.0( tyrus-standalone-client )链接的开放源代码的经验,我将感到非常高兴。


因此,使用标准的IDE工具来建立方法调用的层次结构,直到UI表单本身,在该表单上按钮处理程序将拉动远程服务并不困难。 同时,我们得到了严格的类型输入以及客户端和服务器集成层的弱连接。 应用程序源代码的结构分为客户端,服务器和内核,它们通过上瘾连接到客户端和服务器。 所有远程接口和传输的对象都位于该文件中。 而且,与数据库中的查询相关联的开发人员例程需要在界面中采用一种新方法,并在服务器端实现该方法。 我认为,容易得多...

Source: https://habr.com/ru/post/zh-CN422073/


All Articles