среда, 27 августа 2008 г.

Lightweight JEE

Расскажу о том, каким образом я разрабатываю на Java серверные приложения, у которых, как правило, несложный web-интерфейс и довольно замороченная внутренняя логика, в основном из-за необходимости общаться с внешними, иногда довольно "странными" системами (отрасль - телеком, если это кому-то что-то скажет ;) ). Число активных разработчиков от 1 до 2 :) - таким образом, с одной стороны, можно представить себе масштабы проектов, а с другой стороны - разрушить миф о том, что "для разработки на Java нужно много индусов".

Разумеется, я не использую тяжелые сервера приложений вроде JBoss и тем более WebSphere или WebLogic. Более того, я не использую Tomcat. В моем случае каждое приложение живет в отдельной JVM - примерно так же как OpenFire или XWiki.

Исходный код минимального приложения, которое ничего не делает, а просто пишет в лог сообщения о запуске и о завершении работы, можно взять здесь. Все необходимые для работы приложения библиотеки включены в проект и находятся в каталоге lib, рядом в каталоге lib-build также находятся библиотеки для сборки проекта - у вас должен быть установлен только JDK. В результате сборки получается архив, в который все включено - для запуска приложения вам потребуется только JRE.

Для тех, кому грустно тащить лишние мегабайты jar-ов, на http://enp.itx.ru/java/examples/ рядом с каждым *App.zip лежит *App.NoLib.zip.

IDE для работы в принципе не обязательна, однако без нее навигация по коду будет сильно затрудена. Можно использовать любую IDE с поддержкой Java, XML и JavaScript - я использую Eclipse IDE for Java EE Developers

Для сборки приложения используется Ant, который читает инструкции из файла build.xml. На UNIX сборка запускается с помощью build.sh, на Windows - с помощью build.bat - эти файлы можно переносить из одного проекта в другой без изменений. По умолчанию при сборке выполняется цель all, которая последовательно выполняет цели clean,prepare,compile,jar,dist. Цель можно указать явно как параметр в командной строке. Можно также увидеть список целей с помощью ключа -p.

При сборке генерируются стартовые скрипты statup.sh для запуска на UNIX и statup.bat для запуска на Windows, а также инит-скрипт в стиле SysV для работы приложения в качестве юниксового демона. Для генерации написан специальный ant task - его исходный код здесь. Исходные данные для генерации (краткое и полное имя приложения, имя главного класса) берутся из build.xml.

Главный и единственный класс приложения - startup.ServiceApp. Он имеет стандартную точку входа для запуска в качестве обычного java-приложения - метод main, а кроме этого реализует интерфейс org.apache.commons.daemon.Daemon из библиотеки Jakarta Commons Daemon - и таким образом может быть запущен в качестве сервиса UNIX System V с помощью jsvc и в качестве сервиса Windows c помощью procrun. Для реализации первой возможности и используется автоматически генерируемый при сборке инит-скрипт, который предполагает наличие установленного jsvc (в репозитариях ALT Linux эта утилита находится в одноменном пакете).

Для протоколирования используются библиотеки Commons Logging в качестве фасада и Log4j в качестве реализации.

Пока что все описанное сильно напоминает подъем солнца вручную. Т.е. можно, конечно, на этом остановиться и писать прикладную логику в виде java-классов без задействования всех преимуществ enterprise-технологий. Более того, новичкам я бы даже рекомендовал какое-то время воздержаться от их использования. Например, мы хотим, чтобы наше приложение научилось отвечать на http-запросы. Для этого мы просто задействуем встроенный в Java 1.6 http-сервер - исходный код нового приложения можно взять здесь.

А вот теперь, после некоторой передышки, давайте задействуем Spring Framework. Если честно, я затрудняюсь объяснить в двух словах, зачем он нужен. Он умеет слишком много, но все use cases можно условно свести к двум случаям:
  • упрощение взаимодействия модулей системы друг с другом с помощью таких техник, как IoC и AOP и перенос взаимодействия в рантайм для повышения гибкости
  • упрощение работы с JDBC, ORM, JMX, JMS, Web Services и прочими полезными технологиями
Исходный код простого приложения, использующего Spring, можно взять здесь. Главный класс startup.SpringApp инициализирует контекст на основании файла context.xml или других файлов, указанных в командной строке при запуске приложения, и закрывает его при завершении работы приложения. Контекст - это и есть описание модулей (бинов) в формате xml. В нашем случае будут инициализированы бины fakeBookLoader и xmlInputStreamBookLoader, реализующие интерфейс beans.BookLoader (т.е. умеющие каким-либо способом загружать экземпляры класса beans.Book), и главный бин bookManager. Последний при инициализации получит ссылки на экземпляры beans.BookLoader, а после инициализации Spring вызовет его метод displayBooks. Подробности в документации Spring.

Теперь усложним приложение по двум направлениям:
  • реализуем извлечение экземпляров класса beans.Book из реляционной БД - исходный код приложения берем здесь
  • реализуем web-интерфейс к фунциональности бина bookManager - а это берем здесь
Первый пример использует 3 способа работы работы с СУБД (и, соответственно, 3 реализации beans.BookLoader, используемые бином bookManager):
  • JDBC без управления транзакциями (каждая операция выполняется в отдельной транзакции) - для этого в контексте определены бины dataSource (параметры подключения вынесены в отдельный файл db.properties, т.к. они же используются еще и в build.xml для создания таблиц в БД) и jdbcBookLoader
  • JPA (стандарт ORM для Java) с ручным управлением транзакциями - для этого определены бины entityManagerFactory (фабрика реализаций интерфейса javax.persistence.EntityManager, управляющих сохранением/извлечением объектов в/из БД) и jpaBookLoader
  • JPA с автоматическим управлением транзакциями - для этого используются бин transactionManager, специальные тэги <tx:advice>, <aop:config> и <context:annotation-config>, а также бин jpaTransactionalBookLoader (для него при вызове каждого метода Spring автоматически создает экземпляр EntityManager, стартует транзакцию и завершает ее после выполненения метода или откатывает в том случае, если при выполнении метода возникло исключение).
Класс beans.Book аннотирован для того, чтобы EntityManager знал, каким образом его экземпляры нужно сохранять/извлекать в/из БД. Кроме того, в файле META-INF/persistence.xml описано, какой провайдер JPA и с какими параметрами будет использоваться. В данном случае используется Hibernate, который помимо стандартного JPA API имеет также собственный API, пример использования можно взять здесь, рядом лежит более простой пример использования Hibernate без Spring, а тут - пример совсем простого приложения для работы с БД без всяких фреймворков с использованием аналога спрингового уровня абстракции над JDBC - Jakarta Commons DbUtils.

Теперь о приложении с web-интерфейсом для бина bookManager. Как правило, такого рода приложения пакуются в WAR и размещаются в одном из сервлет-контейнеров. В случае использования Spring контекст инициализируется и закрывается сервлетом. Такая схема представляется вполне логичной для web-ориентированных приложений, особенно в том случае, когда в одном контейнере предполагается размещать множество web-приложений.

В моем случае это не так: web-часть приложения сравнительно небольшая по сравнению с основной логикой, приложение является единственным на хосте, и мне хочется конфигурировать http-сервер в том же контексте, что и все приложение. Чем, в конце концов, мои бины, бины Spring, бины дополнительных библиотек отличаются от бинов http-сервера?

Если бы мне требовалась загрузка и выгрузка модулей без остановки всего приложения, я, возможно, обратил бы внимание на совсем недавно появившуюся SpringSource Application Platform, которая использует стандарт OSGi, однако пока это не нужно, проще описать бины http-сервера прямо в контексте.

Я не пробовал встраивать Tomcat в контекст Spring, Jetty встраивается без проблем, т.к. его основные классы удовлетворяют спецификации JavaBeans (если бы это и было не так, можно было бы написать небольшую обертку), а для встроенного в Java 1.6 http-сервера такая обертка уже есть.

Приложение интенсивно использует технологию AJAX, которая позволяет отделить клиентскую логику от серверной - взаимодействуют они по протоколу JSON-RPC. Существует единственная полная реализация JSON-RPC для Java - jabsorb, но она использует в качестве транспорта сервлеты, не реализованные во встроенном в Java 1.6 http-сервере. Существует также JSON-lib для сериализации JavaBeans в JSON, транспорта там просто нет, но его можно реализовать по аналогии с описанным здесь способом.

Итак, в контексте приложения определен бин httpServer с двумя http-контекстами:
  • staticHttpHandler - выдает статические страницы по имени файла
  • jsonRpcHttpHandler - отвечает на запросы JSON-RPC к бинам, перечисленным в свойстве services
Клиентов целых два: один использует для отрисовки данных стандартные теги html, другой использует библиотеку виджетов ExtJS. Для формирования запросов JSON-RPC оба используют реализацию от jabsorb.

Ну и наконец итоговое приложение BookManager - в нем объединены возможности двух предыдущих: книги сохраняются в БД и извлекаются оттуда, класс tools.JsonRpcHttpHandler переписан и поддерживает базовую http-авторизацию, которая может быть использована двумя способами:
  • для ограничения доступа определенных групп пользователей к сервисам
  • для получения методом сервиса информации о пользователе, который его вызвал
На этом все. Некоторые релевантные обсуждения, долго сподвигавшие и таки сподвигнувшие меня написать этот текст, можно найти здесь (там же жалкие попытки оправдать включение библиотек в состав проекта, более настойчивые попытки объяснить, зачем нужен Spring, а также некоторые уже неактуальные вещи вроде Jetty, вместо которого я сейчас использую встроенный в Java 1.6 http-сервер):

Fax/SMTP gateway для CallWeaver

Задача: отправить факс путем отправки письма с вложенным ps/pdf/tif, принять факс в виде почтового сообщения с вложением. Инструменты - CallWeaver и какой-то враппер для ОGI и Manager API на Python, который я вовремя не опакетил, таская с собой, а теперь уж и забыл, откуда взял (да, я понимаю, что я неправ, но вот только сейчас руки дошли хотя бы до публикации решения). Архив со скриптами можно взять здесь.

Работает оно следующим образом. От локального отправителя письмо с вложением получает MTA и передает его на stdin скрипту fax-send.py. Скрипт делает из файла tiff и с помощью Originate коммутирует вызываемого внешнего абонента (контекст [call-fax-send], если не дозвонились, вызывается fail-fax.py) и факс(контекст [fax-send], о результате прохождения факса отправителю сообщает check-fax.py). Параметры передаются через Variable.

Упомянутые контексты описаны так:
[call-fax-send]
exten => 0,1,NoOp(Calling ${RECIPIENT} for sending fax)
exten => 0,2,Dial(SIP/${RECIPIENT}@${PEER},25)
exten => 0,3,NoOp(${DIALSTATUS})
exten => 0,4,DeadOGI(fail-fax.py)

[fax-send]
exten => 0,1,NoOp(Sending file ${FILE} as fax from ${SENDER} to ${RECIPIENT}@${PEER} with NOTIFY=${NOTIFY})
exten => 0,2,GotoIf($[${NOTIFY}=Yes]?3:6)
exten => 0,3,Playback(fax)
exten => 0,4,Playback(beep)
exten => 0,5,Wait(2)
exten => 0,6,Set(LOCALSTATIONID=CallWeaver)
exten => 0,7,Set(LOCALHEADERINFO=CallWeaver Fax)
exten => 0,8,TxFAX(${FILE})
exten => h,1,NoOp(RX: REMOTESTATIONID is ${REMOTESTATIONID})
exten => h,2,NoOp(RX: PHASEESTATUS is ${PHASEESTATUS})
exten => h,3,NoOp(RX: PHASEESTRING is ${PHASEESTRING})
exten => h,4,DeadOGI(check-fax.py)
Локальный же получатель факсов переадресует внешнего отправителя в контекст [fax-receive], там получившийся файл подбирает read-fax.py, он же сообщает о неудаче.

Контекст для приема факсов:
[fax-receive]
exten => _0.,1,Set(SENDER=${CALLERID(num)})
exten => _0.,2,Set(RECIPIENT=${EXTEN:1})
exten => _0.,3,Set(LOCALSTATIONID=Vertol EXPO)
exten => _0.,4,Set(LOCALHEADERINFO=Vertol EXPO Fax)
exten => _0.,5,Set(FILE=/data/callweaver/fax-receive/${UNIQUEID}.tif)
exten => _0.,6,RxFAX(${FILE})
exten => h,1,NoOp(RX: REMOTESTATIONID is ${REMOTESTATIONID})
exten => h,2,NoOp(RX: PHASEESTATUS is ${PHASEESTATUS})
exten => h,3,NoOp(RX: PHASEESTRING is ${PHASEESTRING})
exten => h,4,DeadOGI(read-fax.py)
Отлов исключений сделан только там, где без него совсем грустно (например, MTA лучше не знать, что внутри fax-send.py приключилось что-то нехорошее). В OGI невозможность удалить файл, например, ни к чему плохому не приводит.

Да, о поддержке факсов в CallWeaver читать здесь.

CallWeaver из коробки

В дополнение к предыдущему сообщению хочу добавить, что ALT Linux 4.0 Server Lite - один из немногих дистрибутивов, в которых CallWeaver работает практически из коробки и не слишком устарел (ну а если вдруг, то я его майнтейнер :) ). После установки достаточно, не вынимая установочного диска, сказать:
# apt-get install callweaver callweaver-sounds freemusic-signate
и прочесть файл /usr/share/doc/callweaver-1.2/QUICKSTART.ru_RU.UTF-8. Процитирую, пожалуй, его содержимое:

Введение
========

CallWeaver - это IP PBX, форк проекта Asterisk, причинами создания которого послужили 
организационные (зависимость от компании Digium, двойное лицензирование) и технические 
(зависимость от zaptel, отсутствие поддержки T.38 и т.д.) проблемы последнего. Подробнее - 
http://www.callweaver.org/wiki/CallWeaver


Описание конфигурации по умолчанию
==================================

CallWeaver реализован в виде загрузчика с минимальной функциональностью и набора модулей 
расположенных в каталоге /usr/lib/callweaver/modules, которые необходимо описать в файле 
modules.conf. В этом файле отключена автозагрузка модулей, а вместо этого явно указаны 
минимально необходимые модули.

Загрузка модуля chan_sip для поддержки протокола SIP по умолчанию закомментирована. 
При загрузке модуль chan_sip читает файл sip.conf, в котором описаны:

* общие настройки в секции [general]
* собственные абоненты в секциях [101] и [102]
* выход во внешний мир - параметры подключения к оператору sipnet.ru в секции [sipnet] 
и параметр register в секции [global] - в качестве {account} и {password} должны быть 
указаны реальные параметры, выданные оператором

Правила коммутации собственных абонентов друг с другом и с внешним миром описаны в файле 
extensions.conf. Правила оформляются в виде контестов, возможно использующих друг друга 
с помощью include. В контексте [local] описан вызов процедуры Hello с проигрыванием 
звукового файла (файлы находятся в пакете callweaver-sounds) при наборе 100 и вызов 
соответствующих внутренних абонентов при наборе 1ХХ (X - любая цифра от 0 до 9). 
Абоненты были предварительно описаны в файле sip.conf, и для них был указан контекст 
[office] - это значит, что им разрешено выполнять действия, описанные в этом контексте, 
т.е. во вложенном в него [local], а также набирать XXX. (. - любое количество любых 
цифр) - при этом вызов будет выполняться c использованием [sipnet] из sip.conf. 
В контекст [incoming] на номер 100 (как указано в параметре register в sip.conf) 
поступают входящие вызова с sipnet.ru

Более сложные примеры настройки доступны в пакете callweaver-docs в каталоге samples.


Как подключиться и начать использовать CallWeaver
=================================================

В ALT Linux есть несколько софтфонов, поддерживающих протокол SIP, с помощью которых 
можно подключиться к CallWeaver - ekiga, twinkle, sflphone. Для подключения необходимо 
сначала раскомментировать загрузку модуля chan_sip в modules.conf и запустить CallWeaver 
с помощью service callweaver start. В софтфонах необходимо создать учетную запись, указав 
в качестве SIP Proxy адрес сервера с запущенным CallWeaver, а в качестве имени и пароля - 
параметры из секций [101] и [102] файла sip.conf. После этого с каждого софтфона можно будет 
набрать 100 и услышать звуковой файл или набрать 101 или 102 и услышать друг друга. Если 
настроено подключение к sipnet.ru, можно позвонить во внешний мир или принять вызов снаружи 
и проиграть для него звуковой файл.

Для наблюдения за работой CallWeaver можно подключится к его консоли с помощью callweaver_cli. 
То, что будет видно на консоли, нельзя протоколировать стандартным образом, но, поскольку 
для подключения к серверу CallWeaver используется UNIX-сокет, можно использовать конструкцию, 
подобную socat -u UNIX-CONNECT:/var/run/callweaver/callweaver.ctl STDOUT

вторник, 26 августа 2008 г.

ALT Linux 4.0 Server Lite - неофициальная сборка

Долгое время, отвечая на вопрос "а что мне лучше поставить на сервер?", я пребывал в некотором замешательстве. С одной стороны, было бы странно мне, как члену ALT Linux Team, советовать что-либо отличное от дистрибутивов ALT Linux. С другой стороны, ни один из официальных и неофициальных дистрибутивов не отвечает (точнее, до сих пор не отвечал) моим собственным представлениям о том, как должен выглядеть серверный дистрибутив общего назначения.

А представления мои таковы: на сервере не должно быть ничего лишнего (т.е. сервисов, которые не будут использоваться). Я не использую Alterator, предпочитая делать все руками и скриптами, и не далеко не на всех серверах использую OpenVZ, поэтому ALT Linux 4.0 Server и тем более ALT Linux 4.0 Office Server практически для всех моих задач требуют основательной чистки после установки.

Проблема, конечно, решается изготовлением эталонного образа минимальной системы, однако для новичка, задающего вопрос "а что мне лучше поставить на сервер?", это решение несколько неудобно. Более того, мне оно тоже неудобно. Инсталлятор все же значительно лучше.

И тут можно вспомнить, что вообще-то ALT Linux - это не дистрибутив, а сундук с инструментами для построения дистрибутивов. Штатным инструментом для сборки специализированных дистрибутивов в настоящее время является mkimage. С его помощью, а также с помощью участников рассылки devel-distro (которые совсем недавно перебрались сюда из devel и devel-conf) и был изготовлен так называемый ALT Linux 4.0 Server Lite.

Собран он на пакетной базе branch/4.0. В свежеустановленной системе отсутствуют alterator и ovz, потушен portmap (т.к. nfs нужен не всем), поднят acpid и загружен модуль button для корректного выключения системы кнопкой power (в официальном ALT Linux 4.0 Server эта фича уже не помню почему отсутствовала).

Доступны iso для архитертур i586 и x86_64.

Шаг выбора дополнительного ПО из инсталлера исключен, дополнительное ПО предлагается устанавливать с помощью apt уже после установки, перечень дополнительного ПО здесь.

Замечания и пожелания по составу ПО принимаются (уже поступила просьба добавить все, что имеет отношение к pppoe).

В дальнейших планах - всерьез посмотреть на connexion/ncsh и, возможно, заменить им дефолтный etcnet.