Статьи

Jar Hell made Easy — Демистификация пути к классу с помощью jHades

Некоторые из самых сложных проблем, с которыми когда-либо столкнется Java-разработчик, — это ошибки пути к классам: ClassNotFoundException , NoClassDefFoundError , Jar Hell, Xerces Hell и компания.

В этом посте мы рассмотрим основные причины этих проблем и посмотрим, как минимальный инструмент ( JHades ) может помочь в их быстром решении. Мы увидим, почему Maven не может (всегда) предотвращать дублирование пути к классам, а также:

  • Единственный способ справиться с Jar Hell
  • Классовые погрузчики
  • Цепочка погрузчиков класса
  • Приоритет загрузчика классов: родительский первый по сравнению с родительским последним
  • Устранение неполадок при запуске сервера
  • Осмысление Jar Hell с помощью JHades
  • Простая стратегия для избежания проблем с classpath
  • Путь к классам исправлен в Java 9?

Единственный способ справиться с Jar Hell

Проблемы с classpath могут занимать много времени для отладки и, как правило, возникают в наихудшие возможные времена и места: до выпусков и часто в средах, где команда разработчиков практически не имеет доступа.

Они также могут происходить на уровне IDE и стать источником снижения производительности. Мы, разработчики, склонны обнаруживать эти проблемы рано и часто, и это обычный ответ:

разочарование человек потянув-его-волосы-аут

Давайте попробуем сэкономить нам немного волос и докопаться до сути. К этим типам проблем трудно подойти методом проб и ошибок. Единственный реальный способ их решения — действительно понять, что происходит , но с чего начать?

Оказывается, проблемы с Jar Hell проще, чем они выглядят, и для их решения требуется всего несколько концепций. В конце концов, основными причинами проблем Jar Hell являются:

  • банка отсутствует
  • есть одна банка слишком много
  • класс не виден там, где он должен быть

Но если это так просто, то почему проблемы с classpath так трудно отлаживать?

Следы стека Jar Hell неполные

Одна из причин заключается в том, что в трассировке стека для проблем с classpath отсутствует много информации, необходимой для устранения проблемы. Возьмем для примера эту трассировку стека:

1
2
3
4
java.lang.IncompatibleClassChangeError: 
Class org.jhades.SomeServiceImpl does not implement 
the requested interface org.jhades.SomeService 
    org.jhades.TestServlet.doGet(TestServlet.java:19)

Это говорит о том, что класс не реализует определенный интерфейс. Но если мы посмотрим на источник класса:

1
2
3
4
5
6
public class SomeServiceImpl implements SomeService { 
    @Override
    public void doSomething() {
        System.out.println( "Call successful!" );
    }
}

Ну, класс явно реализует отсутствующий интерфейс! Так что же тогда происходит? Проблема в том, что в трассировке стека отсутствует много информации, которая имеет решающее значение для понимания проблемы.

Трассировка стека, вероятно, должна была содержать сообщение об ошибке, подобное этому (мы узнаем, что это значит):

Класс SomeServiceImpl загрузчика классов / path / to / tomcat / lib не реализует интерфейс SomeService загружаемый из загрузчика классов Tomcat — WebApp — / path / to / tomcat / webapps / test

Это будет по крайней мере указание с чего начать:

  • Кто-то новичок в изучении Java, по крайней мере, знает, что существует понятие загрузчика классов, которое необходимо для понимания происходящего.
  • Было бы ясно, что один участвующий класс загружался не из WAR, а каким-то образом из некоторого каталога на сервере ( SomeServiceImpl ).

Что такое загрузчик классов?

Для начала, Class Loader — это просто класс Java, точнее экземпляр класса во время выполнения. Это НЕ недоступный внутренний компонент JVM, как, например, сборщик мусора.

Возьмем, к примеру, WebAppClassLoader от Tomcat, здесь это javadoc . Как вы можете видеть, это просто простой класс Java, мы даже можем написать наш собственный загрузчик классов, если это необходимо.

Любой подкласс ClassLoader квалифицируется как загрузчик классов. Основные обязанности загрузчика классов — знать, где находятся файлы классов, а затем загружать классы по требованию JVM.

Все связано с загрузчиком классов

Каждый объект в JVM связан с его классом через getClass() , а каждый класс связан с загрузчиком классов через getClassLoader() . Это значит, что:

Каждый объект в JVM связан с загрузчиком классов!

Давайте посмотрим, как этот факт можно использовать для устранения неполадок в сценарии ошибки classpath.

Как найти файл класса на самом деле

Давайте возьмем объект и посмотрим, где находится его файл класса в файловой системе:

1
2
3
System.out.println(service.getClass() 
    .getClassLoader()
    .getResource("org/jhades/SomeServiceImpl.class"));

Это полный путь к файлу класса: jar:file:/Users/user1/.m2/repository/org/jhades/jar-2/1.0-SNAPSHOT/jar-2-1.0-SNAPSHOT.jar!/org/jhades/SomeServiceImpl.class
jar:file:/Users/user1/.m2/repository/org/jhades/jar-2/1.0-SNAPSHOT/jar-2-1.0-SNAPSHOT.jar!/org/jhades/SomeServiceImpl.class

Как мы видим, загрузчик классов — это просто компонент времени выполнения, который знает, где в файловой системе искать файлы классов и как их загружать.

Но что произойдет, если загрузчик классов не сможет найти данный класс?

Цепь погрузчика Класс

По умолчанию в JVM, если загрузчик классов не находит класс, он запрашивает загрузчик родительского класса для того же класса и так далее.

Это продолжается вплоть до загрузчика классов JVM (подробнее об этом позже). Эта цепочка загрузчиков классов является цепочкой делегирования загрузчиков классов .

Приоритет загрузчика классов: родительский первый по сравнению с родительским последним

Некоторые загрузчики классов сразу же делегируют запросы родительскому загрузчику классов, не выполняя поиск сначала в своем собственном известном наборе каталогов для файла класса. Считается, что загрузчик классов, работающий в этом режиме, находится в режиме « Родитель первый ».

Если загрузчик классов сначала ищет класс локально и только после запросов к родителю, если класс не найден, то считается, что этот загрузчик классов работает в режиме Parent Last .

Все ли приложения имеют цепочку загрузчиков классов?

Даже самый простой метод Hello World имеет 3 загрузчика классов:

  • Загрузчик классов приложений, отвечающий за загрузку классов приложений (родительский в первую очередь)
  • Загрузчик класса Extensions, который загружает $JAVA_HOME/jre/lib/ext из $JAVA_HOME/jre/lib/ext (родительский сначала)
  • Загрузчик класса Bootstrap, который загружает любой класс, поставляемый с JDK, такой как java.lang.String (без загрузчика родительского класса)

Как выглядит цепочка загрузчика классов приложения WAR?

В случае серверов приложений, таких как Tomcat или Websphere, цепочка загрузчиков классов настраивается иначе, чем простая программа основного метода Hello World. Возьмем, к примеру, цепочку загрузчиков классов Tomcat:

кот-класс-погрузчик-цепь

Здесь мы понимаем, что каждая WAR запускается в WebAppClassLoader , который работает в родительском последнем режиме (он также может быть установлен в родительский в первую очередь). Загрузчик классов Common загружает библиотеки, установленные на уровне сервера.

Что спецификация Servlet говорит о загрузке классов?

Только небольшая часть поведения цепочки загрузчика классов определяется спецификацией контейнера сервлета:

  • Приложение WAR запускается в своем собственном загрузчике классов приложений, который может использоваться другими приложениями или нет
  • Файлы в WEB-INF/classes имеют приоритет над остальными

После этого кто-нибудь угадает! Остальные полностью открыты для интерпретации поставщиками контейнеров.

Почему нет общего подхода к загрузке классов между поставщиками?

Обычно контейнеры с открытым исходным кодом, такие как Tomcat или Jetty, по умолчанию настроены на поиск классов в WAR, а затем только в загрузчиках классов серверов.

Это позволяет приложениям использовать свои собственные версии библиотек, которые переопределяют доступные на сервере.

А как насчет больших железных серверов?

Коммерческие продукты, такие как Websphere, попытаются «продать» вам свои библиотеки, предоставляемые сервером, которые по умолчанию имеют приоритет над библиотеками, установленными в WAR.

Это делается при условии, что, если вы купили сервер, вы также хотите использовать библиотеки и версии JEE, которые он предоставляет, что часто НЕ имеет место.

Это затрудняет развертывание определенных коммерческих продуктов, поскольку они ведут себя иначе, чем Tomcat или Jetty, которые разработчики используют для запуска приложений на своей рабочей станции. Мы еще увидим решение для этого.

Распространенная проблема: дублирующиеся версии классов

На данный момент у вас, наверное, огромный вопрос:

Что если в WAR есть два фляги, которые содержат один и тот же класс?

Ответ заключается в том, что поведение не определено, и только во время выполнения будет выбран один из двух классов . Какой из них будет выбран, зависит от внутренней реализации загрузчика классов, и нет способа узнать заранее.

Но, к счастью, в настоящее время большинство проектов используют Maven, и Maven решает эту проблему, обеспечивая добавление в WAR только одной версии данного jar-файла.

Так что проект Maven не застрахован от этого типа Jar Hell, верно?

Почему Maven не предотвращает дублирование пути к классам

К сожалению, Maven не может помочь во всех ситуациях Jar Hell. Фактически, многие проекты Maven, которые не используют определенные плагины контроля качества, могут иметь сотни дубликатов файлов классов на пути к классам (я видел транки с более чем 500 дубликатами). Для этого есть несколько причин:

  • Издатели библиотеки иногда меняют название артефакта банки: это происходит из-за ребрендинга или по другим причинам. Взять, к примеру, кувшин JAXB . Мэйвен никак не может идентифицировать эти артефакты как одну и ту же банку!
  • Некоторые jar публикуются с зависимостями и без них: Некоторые поставщики библиотек предоставляют версию jar «с зависимостями», которая включает в себя другие jar-файлы. Если у нас есть переходные зависимости с двумя версиями, мы получим дубликаты.
  • Некоторые классы копируются между jar-файлами. Некоторые создатели библиотек, столкнувшись с необходимостью определенного класса, просто извлекают его из другого проекта и копируют в новый jar-файл без изменения имени пакета.

Все ли дубликаты файлов классов опасны?

Если дубликаты файлов классов существуют в одном и том же загрузчике классов, а два дубликата файлов классов абсолютно идентичны, то не имеет значения, какой из них будет выбран первым — эта ситуация не опасна.

Если два файла классов находятся внутри одного и того же загрузчика классов и они не идентичны, то нет способа выбрать один из них во время выполнения — это проблематично и может проявиться при развертывании в разных средах.

Если файлы классов находятся в двух разных загрузчиках классов, то они никогда не считаются идентичными (см. Раздел «Кризис идентичности классов» далее).

Как избежать дубликатов WAR в classpath?

Этой проблемы можно избежать, например, с помощью плагина Maven Enforcer с включенным дополнительным правилом запрета дублирующих классов .

Вы также можете быстро проверить, чиста ли ваша WAR, с помощью отчета о двойных классах WAR JHades . У этого инструмента есть возможность отфильтровывать «безвредные» дубликаты (с одинаковым размером файла).

Но даже чистая WAR может иметь проблемы с развертыванием: отсутствуют классы, классы, взятые с сервера вместо WAR и, следовательно, с неправильной версией, исключения приведения классов и т. Д.

Отладка пути к классам с помощью JHades

Проблемы с classpath часто обнаруживаются при запуске сервера приложений, что является особенно плохим моментом, особенно при развертывании в среде с ограниченным доступом.

JHades — это инструмент, помогающий справиться с Jar Hell (заявление об отказе: я написал). Это один Jar без каких-либо зависимостей, кроме самого JDK7. Это пример того, как его использовать:

1
2
3
4
5
6
new JHades()
   .printClassLoaders()
   .printClasspath()
   .overlappingJarsReport()
   .multipleClassVersionsReport()
   .findClassByName("org.jhades.SomeServiceImpl")

Это выводит на экран цепочку загрузчиков классов, банки, дубликаты классов и т. Д.

Устранение неполадок при запуске сервера

JHades работает хорошо в сценариях, где сервер не запускается должным образом. Предусмотрен приемник сервлета, который позволяет печатать отладочную информацию о пути к классам даже до запуска любого другого компонента приложения.

ClassCastException и кризис идентичности классов

При устранении неполадок Jar Hell остерегайтесь ClassCastExceptions . Класс идентифицируется в JVM не только по его полному имени, но и по загрузчику классов.

Это нелогично, но в ретроспективе имеет смысл: мы можем создать два разных класса с одним и тем же пакетом и именем, отправить их в две банки и поместить в два разных загрузчика классов. Один, скажем, расширяет ArrayList а другой — Map .

Поэтому классы совершенно разные (несмотря на одно и то же имя) и не могут быть приведены друг к другу! Среда выполнения сгенерирует CCE для предотвращения этой потенциальной ошибки, потому что нет никакой гарантии, что классы могут быть преобразованы.

Добавление загрузчика классов к идентификатору класса стало результатом кризиса идентификации классов, который произошел в более ранние дни Java.

Стратегия предотвращения проблем с классами

Это легче сказать, чем сделать, но лучший способ избежать проблем развертывания, связанных с classpath, — запустить производственный сервер в режиме P arent Last .

Таким образом, версии классов WAR имеют приоритет над версиями на сервере, и те же классы используются в рабочей среде и на рабочей станции разработчика, где вероятно использование Tomcat, Jetty или другого сервера с открытым исходным кодом Parent Last .

На некоторых серверах, таких как Websphere, этого недостаточно, и вам также необходимо предоставить специальные свойства в файле манифеста для явного отключения определенных библиотек, таких как, например, JAX-WS.

Исправление пути к классам в Java 9

В Java 9 classpath полностью обновлен с новой системой модульности Jigsaw . В Java 9 jar может быть объявлен как модуль, и он будет работать в своем собственном изолированном загрузчике классов, который считывает файлы классов из других подобных загрузчиков классов модулей способом OSGI.

Это позволит нескольким версиям одного Jar сосуществовать в одном приложении, если это необходимо.

Выводы

В конце концов, проблемы с Jar Hell не так низки или недоступны, как могут показаться на первый взгляд. Все дело в том, что zip-файлы (файлы jar) присутствуют / не присутствуют в определенных каталогах, как найти эти каталоги и как отлаживать путь к классам в средах с ограниченным доступом.

Зная ограниченный набор понятий, таких как загрузчики классов, режимы загрузчика классов и режимы «Родитель первый / Родитель последний», эти проблемы могут быть эффективно решены.

Внешние ссылки

Эта презентация Действительно ли вы получаете загрузчики классов от Евгения Кабанова из ZeroTurnaround (компания JRebel ) — это отличный ресурс о Джар Хелл и различных типах исключений, связанных с classpath.