A maneira mais fácil de apoiar a integração de um cliente java com um servidor java

Ao resolver tarefas diárias com a interface de um aplicativo de desktop baseado em JavaFX, você deve fazer uma solicitação ao servidor da Web de qualquer maneira. Após os dias do J2EE e a terrível abreviatura RMI, muita coisa mudou e as chamadas do servidor ficaram mais leves. O padrão para soquetes da Web e sua troca de mensagens de texto simples de qualquer conteúdo é adequado para esse problema. Mas o problema com aplicativos corporativos é que a diversidade e o número de solicitações tornam a criação e o rastreamento de EndPoints com serviços de negócios selecionados separadamente uma péssima rotina e adicionam linhas de código extras.


Mas e se adotássemos uma estratégia estritamente digitada com o RMI como base, onde houvesse uma interface java padrão entre o cliente e o servidor que descrevesse métodos, argumentos e tipos de retorno, onde algumas anotações foram adicionadas e o cliente nem percebeu magicamente que a chamada estava sendo feita pela rede? E se não apenas texto, mas objetos java serializados são transmitidos pela rede? E se adicionarmos a essa estratégia a facilidade dos soquetes da Web e suas vantagens da possibilidade de chamadas do cliente pelo servidor? E se a resposta assíncrona do soquete da web para o cliente for restringida na chamada de bloqueio usual e, para uma chamada atrasada, adicionar a possibilidade de retornar Future ou mesmo CompletableFuture ? E se adicionarmos a capacidade de inscrever o cliente em certos eventos do servidor? E se o servidor tiver uma sessão e conexão com cada cliente? Pode ser um bom pacote transparente familiar a qualquer programador java, pois a mágica fica oculta por trás da interface e, no teste, as interfaces podem ser facilmente substituídas. Mas isso não é tudo para aplicativos carregados que processam, por exemplo, cotações do mercado de ações.


Em aplicativos corporativos da minha prática, a velocidade de executar uma consulta sql e transferir dados selecionados de um DBMS é incomensurável com a sobrecarga de serialização e chamadas reflexivas. Além disso, um rastreamento terrível de chamadas EJB, complementando o tempo de execução de 4 a 10 ms, mesmo para a solicitação mais simples, não é um problema, já que a duração das solicitações típicas ocorre no corredor de 50ms a 250ms.


Vamos começar com o mais simples - usaremos o padrão de objeto Proxy para implementar a mágica por trás dos métodos de interface. Suponha que eu tenha um método para obter o histórico da correspondência de um usuário com seus oponentes:


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

Criaremos o objeto proxy usando ferramentas java padrão e chamaremos o método necessário:


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

Se, ao mesmo tempo, você configurar fábricas e implementar a instância do objeto proxy através da interface por meio da injeção de cdi, você obterá a mágica em sua forma mais pura. Ao mesmo tempo, abrir / fechar um soquete toda vez não é necessário. Pelo contrário, em meus aplicativos, o soquete está constantemente aberto e pronto para receber e processar mensagens. Agora vale a pena ver o que acontece no 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; } } 

E aqui está o próprio cliente EndPoint em si :


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

Portanto, para chamar qualquer método do objeto proxy, fazemos uma sessão de soquete aberto, enviamos os argumentos e detalhes passados ​​do método que deve ser chamado no servidor e aguardamos a resposta com o guia especificado na solicitação. Após o recebimento da resposta, verificamos uma exceção e, se estiver tudo bem, colocamos o resultado da resposta em Request e notificamos o fluxo que está aguardando uma resposta em WaitList. Não darei a implementação de tal WaitList, pois é trivial. O encadeamento em espera, na melhor das hipóteses, continuará a funcionar após a linha WaitList.putRequest (request, getRequestRunnable (request)); . Depois de acordar, o encadeamento verificará as exceções declaradas na seção throws e retornará o resultado via return .


Os exemplos de código acima são trechos da biblioteca, que ainda não estão prontos para publicação no github. É necessário resolver problemas de licenciamento. Faz sentido olhar para a implementação do lado do servidor já no próprio código-fonte após sua publicação. Mas não há nada de especial lá - é feita uma pesquisa por um objeto ejb que implementa a interface especificada no jndi via InitialContext e uma chamada reflexiva é feita usando os detalhes transmitidos. É claro que ainda há muitas coisas interessantes, mas esse volume de informações não cabe em nenhum artigo. Na própria biblioteca, o script de chamada de bloqueio acima foi implementado antes de tudo, pois é o mais simples. Mais tarde, foi adicionado suporte para chamadas sem bloqueio via Future e CompletableFuture <> . A biblioteca é usada com sucesso em todos os produtos com um cliente java de desktop. Eu ficaria feliz se alguém compartilhar sua experiência de abertura de código-fonte que está vinculado ao gnu gpl 2.0 ( tyrus-standalone-client ).


Como resultado, não é difícil criar uma hierarquia de invocação de método usando as ferramentas padrão do IDE até o próprio formulário da interface do usuário, no qual o manipulador de botão extrai serviços remotos. Ao mesmo tempo, temos uma digitação estrita e fraca conectividade da camada de integração de cliente e servidor. A estrutura do código-fonte do aplicativo é dividida em um cliente, um servidor e um kernel, que é conectado por dependência ao cliente e ao servidor. É nele que todas as interfaces remotas e objetos transferidos estão localizados. E a rotina do desenvolvedor associada à consulta no banco de dados requer um novo método na interface e sua implementação no lado do servidor. Na minha opinião, muito mais fácil ...

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


All Articles