среда, 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-сервер):
Отправить комментарий