Java характеризуется динамической загрузкой классов. Для поиска и загрузки используется механизм делегирующих classloader'ов. В Java SE окружении их количество ограничивается 2-3, но в серверах приложений оно приближается к 10 иерархическим classloader'ам. Этим объясняется большое время запуска (обычно от минуты) и деплоя приложений.
В JBoss AS 7 разработчикам удалось сильно сократить время запуска (пустой appserver запускается за 3-4 секунды на рабочей станции). Этому способствовала новая система загрузки классов в данном сервере приложений. Такой подход всё же обладает своими недостатками.
Данная статья касается механизма загрузки классов в различных окружениях, особенностях работы с JBoss AS 7, сопряжения с системой сборки Apache Maven и IDE IntelliJ IDEA.
Загрузка классов в Java
Согласно Java Language Specifiaction JVM динамически загружает, связвает и инициализирует классы. Поиск и загрузка класса осуществляется иерархией класслоадеров.
При старте ява-машины используется bootstrap classloader (который отвечает за загрузку базовых классов из rt.jar и прочих частей реализации JVM). В дальнейшем обычно происходит делегация system classloader'у, который осуществляет поиск в пределах указанного программе CLASSPATH. И в простейшем случае этим ограничивается.
В дальнейшем происходит загрузка и разбор class-файла, его связывание (верификация, подготовка и разрешение зависимостей класса). После чего происходит инициализация. То есть, загрузка описания класса, инициализация статических полей и выполнение статических инициализаторов происходит
«лениво», по необходимости. На этом основан паттерн для lazy-инициализации со static-holder'ом, описанный в Effective Java.
В окружениях типа servlet-контейнеров, серверов приложений и OSGi всё существенно усложняется. В них существует сложная иерархия класслоадеров, призванная дать доступ к разрешенному (например, библиотекам, предоставляемым окружением) и, при этом, изолировать разные «модули» друг от друга.
То есть в двух загруженных веб-приложениях могут быть разные версии одного и того же класса, которые будут видны только соответствующим приложениям.
Естественно, что класс может и не обнаружится — тогда происходит ошибка загрузки оного, как правило, приводящая к падению JVM, если её не перехватывать (как делают сервера приложений).
Обычная структура загрузки в servlet-контейнере (tomcat, jetty)
В окружениях такого типа иерархия classloader'ов, конечно, усложняется. После system classloader'а, который предназначен для запуска самого servlet-контейнера, поиск делегируется далее по цепочке.
Для каждого приложения, задеплоенного в контейнер, создается свой класслоадер, который ищёт по классам в WEB-INF/classes и jar-файлам в WEB-INF/lib (всё в пределах war этого приложения).
Далее происходит делегация common classloader'у, который ищёт по общим для всех клиентов библиотекам (в CATALINA_HOME/lib в случае tomcat'а).
Сервера приложений
Дальнейший рассказ коснётся загрузки классов app-сервером glassfish. Как полагается, первый в длинной цепочке загрузки bootstrap classloader. После него управление передается public-api classloader'у, который отвечает за классы, интерфейсы и аннотации стека Java EE и другие API, экспортированные сервером приложений. Следующим выступает знакомый нам по tomcat'у common classloader, отвечающий за общие для всего app-server'a библиотеки. Далее в цепочке оказывается connector classloader, который позволяет получить всем приложениям доступ к JCA. После чего управление передается lifecyclemodule classloader'у. Он предназначен для загрузки классов, управляющих жизненным циклом бинов в приложениях. Следующий герой — applib classloader, сильно напоминающий common, но работающий с некоторыми группами приложений. И в последнюю очередь срабатывает archive classloader, работающий с классами из WAR/EAR/RAR/EJB-JAR и др.
Такая сложная система поиска и загрузки классов позволяет упростить сами класслоадеры, более чётко разделить на уровни абстрации, избежать дублирования библиотек. Основным же недостатком является огромная неповоротливость этой системы и, как следствие, большое время загрузки app-server'а и деплоя приложений.
Особенности героя повествования JBoss AS 7
JBoss AS 6 по структуре класслоадеров очень напоминает glassfish. По времени старта — аналогично. Чем же принципиально отлечается 7 версия? Почему она привлекла внимание разработчиков?
Ответ прост: очень быстрый старт. Раньше не было сервера приложений, стартующего за секунды. AS 6 и GlassFish стартую минуту в лучшем случае. Для разработчика, у которого несчатсный glassfish умирает по PermGen при постоянных редеплоях это просто спасение.
Итак, перейдём к технической стороне вопроса. Как удалось достигнуть таких скоростей? Основой, позволившей решить проблему медленной работы иерархических класслоадеров, является JBoss Modules. Этот проект направлен на модульный (а не иерархический) подход к загрузке классов. Поиск осуществляется в пределах небольшого набора модулей. Возникает вопрос, как окружению узнать в каких модулях необходимо искать?
Решается он, к сожалению, только явным перечислением модулей-зависимостей. Для этого в развертываемый war в его манифест дописывается поле Dependencies в котором перечисляются зависимости.
По её имени сервер находит xml, описывающий данный модуль: набор jar-файлов, входящих в него, и модули, от которых зависит данный.
Использование maven'а
При использовании maven для сборки такой подход к предоставлению пакетов приводит к тому, что часть зависимостей в pom.xml должны быть объявлены provided. Например, при использовании slf4j это будет выглядеть следуюзим образом:
<dependencides>
<dependecy>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.6.1</version>
<scope>provided</scope>
</dependecy>
<!-- ... -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Dependencies>org.slf4j</Dependencies>
</manifestEntries>
</archive>
</configuration>
</plugin>
<!-- ... -->
</plugins>
</build>
Отладка в IntelliJ IDEA
Несмотря на всю прелесть данной IDE в ней часто встречаются неприятные недоработки. Например, при взаимодействии с maven'ом. При попытке отлаживать проект, использующий конфиг из предыдущего пункта, выясняется, что артифакт собранный maven'ом корректно работает,
а при деплое из Idea вылетает NoClassDefFoundError. Это связано с тем, что IDEA не используется данные из pom.xml, а генерирует свой MANIFEST.MF, отлучающийся отсутствием пункта Dependencies.
Обойти такое поведение можно создав свой манифест такого вида:
Manifest-Version: 1.0
Dependencies: org.slf4j
Очевидный недостаток такого workaround'а — дублирование информации о зависимостях в двух местах.
Автор: grossws