Открываю цикл постов про MQ и API. Прежде всего стоит определиться с терминологией: RMI или RPC. Method Invocation подразумевает за собой объекты и состояние, мы же будем рассматривать stateless вызовы, поэтому логичней это называть RPC. Лет пять назад Umputun рассказывал в одном из своих подкастов про то, как он спрашивал своих бойцов как бы они сделали RMI и для них это была вещь в себе, но ведь не "боги горшки обжигают".
1. клиент авторизуется при подключении к серверу, соответственно аккаунты должны лежать в базе и быть нашими существующими аккаунтами
2. клиент может вызывать методы серверной реализации и получать результаты в том числе исключения, если таковые возникают
3. клиенты уведомляются о событиях на сервере, при этом события могут быть как широковещательными, так и P2P. Впоследствии от P2P мы отказались, поскольку подавляющая часть событий были широковещательными.
4. на будущее возможность подключать не только java клиенты будет очень привлекательна
идти слать ответ через JMSReplyTo. Клиент у нас работает в синхронном режиме и ждет ответа.
Вторая проблема - сериализуемость. Когда клиент был только на java, мы не парились и сериализовались стандартным механизмом. Когда появился .Net, пришлось быстро перейти на XML и сериализоваться через XStream. А для того, чтобы сохранить generic-чистоту кода, пришлось рожать такие конструкции:
Методы описаны в интерфейсе FooService, события, посылаемые клиентом, описаны в виде inner static классов в Events после получения их очень удобно засылать как есть через гуавовский EventBus (@Subscribe в ClientApp). Клиентское приложение стартует и вызывает методы с различным результатом (пустой, объект, исключение). Сервер поднимает ActiveMQ и периодически засылает уведомления, параллельно отвечая на запросы. Я там продолжаю экспериментировать с DI через Guice - пока не очень удобно. На клиенте самой интересной частью является runtime генерация клиентской реализации для интерфейсов через Proxy. На сервере самая интересная часть - поиск серверной реализации вызванного метода в invoke.
Постановка задачи
Итак, для клиента нужен универсальный API. Мы почти сразу отказались от google-gson и взяли java реализацию JMS - ActiveMQ. Технология очень проста и позволяет очень быстро стартануть. Главный недостаток этого мы навелосипедили очень много стандартных вещей. Чего хочется:1. клиент авторизуется при подключении к серверу, соответственно аккаунты должны лежать в базе и быть нашими существующими аккаунтами
2. клиент может вызывать методы серверной реализации и получать результаты в том числе исключения, если таковые возникают
3. клиенты уведомляются о событиях на сервере, при этом события могут быть как широковещательными, так и P2P. Впоследствии от P2P мы отказались, поскольку подавляющая часть событий были широковещательными.
4. на будущее возможность подключать не только java клиенты будет очень привлекательна
Низкий уровень
В JMQ передача сообщений строится на двух концепциях: топики, сообщения получают все подписанты, и очереди, сообщение получает первый подписавшийся. С топиками все просто - они идеально подходят на роль широковещательных уведомлений. С очередями сложнее: у нас один слушатель - сервер и много писателей - клиенты, именно они инциируют посылку сообщений. Как же возвращать результат пославшему запрос? Есть официальный путь для этого: на каждое соединение клиент открывает временную очередь и сообщает серверу куда емуJava flavour
Но тут есть проблема: таймаут на подключение 15 секунд, и две причины: очень долгий метод и очень большой ответ. В первом случае результат приходится посылать через уведомление (обычно он небольшого размера), а во втором бьется на небольшие чанки (обычно он достаточно быстрый). Натягивание совы многопоточности/асинхронности на однопоточный глобус JavaFX - тема для отдельного поста.Вторая проблема - сериализуемость. Когда клиент был только на java, мы не парились и сериализовались стандартным механизмом. Когда появился .Net, пришлось быстро перейти на XML и сериализоваться через XStream. А для того, чтобы сохранить generic-чистоту кода, пришлось рожать такие конструкции:
<T extends List<SomeArg> & Serializable> Collection<SomreResult>
getSmth(T tags, Foo f, Bar b) throws SomeException;
К сожалению java конпелятор недостаточно умен для вывода типов (надеюсь с <~> все станет лучше) и для перегруженных методов приходится писать странные конструкции вида
<T extends List<SomeArg> & Serializable> Collection<SomreResult>
getSmth(Foo f, Bar b) throws SomeException{
return getSmth((T) null, Foo f, Bar b);
}
Аннотировать @SuppressWarnings("RedundantCast"), @SuppressWarnings("UnusedDeclaration")чтобы IDE не умничала и обрамлять их жирными предупреждениями: НЕ ИСПРАВЛЯТЬ!!11ОДИНОДИН ВСЕ СЛОМАЕТСЯ КРОВЬ-КИШКИ. Диагностика таких ошибок очень длительна и нетривиальна.
В ActiveMQ очень легко добавить трансформер XStreamMessageTransformer, который будет сериализовать объекты в XML. Единственная проблема с ним была в том, что он терял UserID в процессе сериализации, но починить это оказалось очень легко.
Высокий уровень
Для развлечения я извлек модель очень похожую на то, что работает в нашем проекте. Главный бонус здесь в том, что в песочнице можно попробовать различные вещи, а затем перенести в проект. Собственно код состоит из трех частей: интерфейсы лежат в пакете api, серверная и клиентская реализации в server и client соответственно.Методы описаны в интерфейсе FooService, события, посылаемые клиентом, описаны в виде inner static классов в Events после получения их очень удобно засылать как есть через гуавовский EventBus (@Subscribe в ClientApp). Клиентское приложение стартует и вызывает методы с различным результатом (пустой, объект, исключение). Сервер поднимает ActiveMQ и периодически засылает уведомления, параллельно отвечая на запросы. Я там продолжаю экспериментировать с DI через Guice - пока не очень удобно. На клиенте самой интересной частью является runtime генерация клиентской реализации для интерфейсов через Proxy. На сервере самая интересная часть - поиск серверной реализации вызванного метода в invoke.