2013-11-30

API over ActiveMQ

Открываю цикл постов про MQ и API. Прежде всего стоит определиться с терминологией: RMI или RPC. Method Invocation подразумевает за собой объекты и состояние, мы же будем рассматривать stateless вызовы, поэтому логичней это называть RPC. Лет пять назад Umputun рассказывал в одном из своих подкастов про то, как он спрашивал своих бойцов как бы они сделали RMI и для них это была вещь в себе, но ведь не "боги горшки обжигают".


Постановка задачи

Итак, для клиента нужен универсальный API. Мы почти сразу отказались от google-gson и взяли java реализацию JMS - ActiveMQ. Технология очень проста и позволяет очень быстро стартануть. Главный недостаток этого мы навелосипедили очень много стандартных вещей. Чего хочется:
1. клиент авторизуется при подключении к серверу, соответственно аккаунты должны лежать в базе и быть нашими существующими аккаунтами
2. клиент может вызывать методы серверной реализации и получать результаты в том числе исключения, если таковые возникают
3. клиенты уведомляются о событиях на сервере, при этом события могут быть как широковещательными, так и P2P. Впоследствии от P2P мы отказались, поскольку подавляющая часть событий были широковещательными.
4. на будущее возможность подключать не только java клиенты будет очень привлекательна

Низкий уровень

В JMQ передача сообщений строится на двух концепциях: топики, сообщения получают все подписанты, и очереди, сообщение получает первый подписавшийся. С топиками все просто -  они идеально подходят на роль широковещательных уведомлений. С очередями сложнее: у нас один слушатель - сервер и много писателей - клиенты, именно они инциируют посылку сообщений. Как же возвращать результат пославшему запрос? Есть официальный путь для этого: на каждое соединение клиент открывает временную очередь и сообщает серверу куда ему идти слать ответ через JMSReplyTo. Клиент у нас работает в синхронном режиме и ждет ответа.

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.

Итог

Если захочется поиграть, то запускаем сначала ServerApp, а потом ClientApp и наблюдаем за консолью. Продолжение полное танцев вприсядку про .Net и 1C с необычной развязкой следует...