Загрузка классов в серверах приложений, особенности JBoss AS 7

в 14:39, , рубрики: classloader, intellij idea, java, jboss, jboss as 7, jvm, maven, метки: , , , , , ,

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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js