Избранное сообщение

Фетісов В. С. Комп’ютерні технології в тестуванні. Навчально-методичний посібник. 2-ге видання, перероблене та доповнене / Мои публикации

В 10-х годах я принимал участие в программе Европейского Союза Tempus "Освітні вимірювання, адаптовані до стандартів ЄС". В рамк...

Благодаря Интернету количество писателей и поэтов увеличивается в геометрической прогрессии. Поголовье читателей начинает заметно отставать.

вторник, 4 сентября 2018 г.

Максимально простой в поддержке способ интеграции java-клиента с java-сервером / Программирование на Java

При решении повседневных задач с интерфейсом настольного приложения, реализованного на JavaFX, приходится в любом случае делать запрос на веб-сервер. После времен J2EE и страшной аббревиатуры RMI многое изменилось, а вызовы на сервер стали более легковесными. Как нельзя кстати для подобной проблемы подходит стандарт веб-сокетов и его обмен простыми текстовыми сообщениями любого содержания. Но проблема корпоративных приложений в том, что разнообразность и количество запросов превращает создание и отслеживание EndPoint-ов при наличии отдельно выделенных бизнес-сервисов в жуткую рутину и добавляет лишних строк кода.
А что если взять за основу строго типизированную стратегию с RMI, где между клиентом и сервером существовал стандартный java interface, описывающий методы, аргументы и возвращаемые типы, где добавлялось пару аннотаций, и волшебным образом клиент даже не замечал, что идет вызов по сети? Что если по сети передавать не просто текст, а сериализованные java-объекты? Что если добавить к этой стратегии легкость веб-сокетов и их преимущества возможности push-вызовов клиента со стороны сервера? Что если асинхронность ответов веб-сокета для клиента обуздать в привычный блокирующий вызов, а для отложенного вызова добавить возможность возвращения Future или даже CompletableFuture? Что если добавить возможность подписки клиента на определенные события от сервера? Что если на сервере иметь сессию и подключение к каждому клиенту? Может получиться неплохая прозрачная связка привычная любому java-программисту, так как за интерфейсом будет скрыта магия, а в тестировании интерфейсы легко подменить. Но вот только это все не для нагруженных приложений, обрабатывающих, например, котировки с фондовой биржи.

В корпоративных приложениях из моей практики скорость выполнения sql-запроса и передачи выбираемых данных из СУБД несоизмеримы с накладными расходами на сериализацию и рефлексивные вызовы. Более того страшная трассировка EJB-вызовов, дополняющая длительность выполнения до 4 — 10 мс даже на самый простенький запрос не является проблемой, так как длительность типичных запросов находится в коридоре от 50мс до 250мс.
Начнем с самого простого — воспользуемся паттерном Proxy-объект для реализации магии за методами интерфейса. Предположим, что у меня есть метод получения истории переписки пользователя с его оппонентами:
public interface ServerChat{
Map<String, <List<String>> getHistory(Date when, String login);
}

Proxy-объект создадим стандартными средствами 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");
//...
//закрытие сокета и соединения

Если при этом настроить фабрики, а экземпляр proxy-объекта внедрять по интерфейсу через cdi-инъекцию, то получится магия в чистом виде. При этом открывать/закрывать сокет каждый раз совсем не обязательно. Напротив в моих приложениях сокет постоянно открыт и готов к приему и обработке сообщений. Теперь стоит посмотреть, что такого происходит в 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.args = args;
request.methodName = method.getName();
request.interfaze = interfaze;
request.argsType = method.getParameterTypes();
checkError(request, method);
WaitList.putRequest(request, getRequestRunnable(request));
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;
}
}

Таким образом, на вызов любого метода proxy-объекта берем открытую сессию сокета, шлем переданные аргументы и реквизиты метода, который необходимо вызвать на сервере, и wait-имся до получения ответа с указанными ранее в запросе гуидом. При получении ответа проверяем на наличие исключения, и, если все хорошо, то кладем в Request результат ответа и нотифицируем поток, ожидающий ответа в WaitList-е. Реализацию такого WaitList-а приводить не буду, так как она тривиальна. Ожидающий поток в лучшем случае продолжит работать после строки WaitList.putRequest(request, getRequestRunnable(request));. После пробуждения поток проверит наличие задекларированных в секции throws исключений, и выполнит возврат результата через return.
Приведенные примеры кода являются выдержкой из библиотеки, которая пока не готова для выкладки на github. Необходимо проработать вопросы лицензирования. Реализацию серверной стороны имеет смысл смотреть уже в самом исходном коде после его опубликования. Но ничего особенного там нет — выполняется поиск ejb-объекта, который реализует указанный интерфейс, в jndi через InitialContext и делается рефлексивный вызов по переданным реквизитам. Там конечно еще много чего интересного, но ни в одну статью такой объем информации не влезет. В самой библиотеке приведенный сценарий блокирующего вызова был реализован в первую очередь, так как является самым простым. Позже была добавлена поддержка неблокирующих вызовов посредством Future и CompletableFuture<>. Библиотека успешно используется во всех продуктах с настольным java-клиентом. Буду рад, если кто-то поделится опытом открытия исходного кода, который линкуется с gnu gpl 2.0 (tyrus-standalone-client).
В итоге построить иерархию вызова метода стандартными средствами IDE до самой UI-формы, на которой обработчик кнопки дергает удаленные сервисы, не составляет труда. При этом получаем строгую типизацию и слабую связанность слоя интеграции клиента и сервера. Структура исходного кода приложения делится на клиент, сервер и ядро, которое подключается зависимостью и в клиент, и в сервер. Именно в нем и находятся все удаленные интерфейсы и передаваемые объекты. А рутинная задача разработчика, связанная с запросом в БД, требует нового метода в интерфейсе и его реализации на стороне сервера. На мой взгляд, куда уж проще...


Смотри также:

Зачем нужна Java. http://fetisovvs.blogspot.com/2014/07/java.html
Разбор основных концепций параллелизма. http://fetisovvs.blogspot.com/2018/04/java.html
Первый контакт с «var» в Java 10. http://fetisovvs.blogspot.com/2018/01/var-java-10-java.html
JAVA 9. Что нового? http://fetisovvs.blogspot.com/2017/10/java-9-java.html
Руководство по Java 9 для тех, кому приходится работать с legacy-кодом. http://fetisovvs.blogspot.com/2018/08/java-9-legacy-java.html
Концепции объектно-ориентированного программирования — ООП в Java. http://fetisovvs.blogspot.com/2017/01/java-java.html
Анимации в Android по полочкам (Часть 1. Базовые анимации). http://fetisovvs.blogspot.com/2018/02/android-1-java.html
Двести пятьдесят русскоязычных обучающих видео докладов и лекций о Java. http://fetisovvs.blogspot.com/2015/12/java-5-java-java.html
Абстрактные классы и методы. http://fetisovvs.blogspot.com/2017/02/java.html
Полное руководство по Java Reflection API. Рефлексия на примерах. http://fetisovvs.blogspot.com/2017/02/java-reflection-api-java.html
Микросервисы для Java программистов. Практическое введение во фреймворки и контейнеры. http://fetisovvs.blogspot.com/2017/10/java-java.html
Микросервисы для Java программистов. Практическое введение во фреймворки и контейнеры. (Часть 3). http://fetisovvs.blogspot.com/2017/10/java-3-java.html
ТОП-3 способа конвертировать массив в ArrayList. Пример на Java. http://fetisovvs.blogspot.com/2016/09/3-arraylist-java-java.html
Ввод–вывод в Java. http://fetisovvs.blogspot.com/2016/05/java-java_28.html
Enum-Всемогущий. http://fetisovvs.blogspot.com/2017/02/enum-java.html
Массивы в Java. Создание и обработка. http://fetisovvs.blogspot.com/2017/10/java-java_18.html
Arrays, Collections: Алгоритмический минимум. http://fetisovvs.blogspot.com/2017/12/arrays-collections.html
Популярные методы для работы с Java массивами. http://fetisovvs.blogspot.com/2016/09/java-java_29.html
Пример использования метода replace в Java. Как заменить символ в строке? http://fetisovvs.blogspot.com/2017/01/replace-java-java.html
Класс Scanner в Java — описание и пример использования. http://fetisovvs.blogspot.com/2017/01/scanner-java-java.html
Пример использования метода trim в Java: как удалить пробелы в начале и конце строки? http://fetisovvs.blogspot.com/2017/01/trim-java-java.html
Spark — Потрясающий веб-микрофреймворк для Java. http://fetisovvs.blogspot.com/2017/10/spark-java-java.html
Чтение и запись CSV файла с помощью SuperCSV. http://fetisovvs.blogspot.com/2017/01/csv-supercsv-java-java.html
Конструкция try/catch/finally (исключения). http://fetisovvs.blogspot.com/2017/01/trycatchfinally-java.html
1000+ часов видео по Java на русском. http://fetisovvs.blogspot.nl/2017/06/1000-java-java.html
Шпаргалка Java программиста 7.1 Типовые задачи: Оптимальный путь преобразования InputStream в строку. http://fetisovvs.blogspot.com/2016/04/java-71-inputstream-java.html
Шпаргалки Java программиста 10: Lombok. http://fetisovvs.blogspot.nl/2017/12/java-10-lombok-java.html
Шпаргалки Java программиста 9: Java SE — Шпаргалка для собеседований и повторений. http://fetisovvs.blogspot.com/2017/12/java-9-java-se-java.html
Шпаргалка Java программиста 8. Библиотеки для работы с Json (Gson, Fastjson,
LoganSquare, Jackson, JsonPath и другие). http://fetisovvs.blogspot.com/2016/04/java-8-json-gson-fastjson-logansquare.html
Java 8 и паттерн Стратегия. http://fetisovvs.blogspot.com/2018/03/java-8-java.html
Java EE Concurency API. http://fetisovvs.blogspot.com/2018/08/java-ee-concurency-api-java.html
Реализация ООП-наследования в классах, работающих с SQL и MS Entity Framework. http://fetisovvs.blogspot.com/2017/02/sql-ms-entity-framework.html
Как установить соединение с СУБД MySQL в IntelliJ IDEA в редакции Community. http://fetisovvs.blogspot.com/2016/04/mysql-intellij-idea-community-java.html
Как с помощью maven работать с библиотеками, которых в maven нет. http://fetisovvs.blogspot.com/2017/03/maven-maven-java.html
Проекты по созданию компиляторов из Java в JavaScript и исполняемые файлы. http://fetisovvs.blogspot.com/2018/01/java-javascript-java.html
Диагностика утечек памяти в Java. http://fetisovvs.blogspot.com/2017/03/java-java_18.html
Spring AOP и JavaConfig в плагинах для Atlassian Jira. http://fetisovvs.blogspot.com/2018/04/spring-aop-javaconfig-atlassian-jira.html
Блеск и нищета Java для настольных систем. http://fetisovvs.blogspot.com/2018/04/java-haulmont-java.html
Разбор задачек от Одноклассников на JPoint 2018. http://fetisovvs.blogspot.com/2018/04/jpoint-2018-java.html
Программируем… выход из лабиринта. http://fetisovvs.blogspot.com/2015/10/java.html
Основы работы с IntelliJ IDEA. Интерфейс программы. http://fetisovvs.blogspot.com/2016/09/intellij-idea-java.html
Ускоряем время сборки и доставки java web приложения. http://fetisovvs.blogspot.com/2018/03/java-web-java.html

Комментариев нет:

Отправить комментарий