Статьи

Загадка загрузки классов разгадана

Столкнувшись со старой доброй проблемой

Я боролся с проблемой загрузки классов на сервере приложений. Библиотеки были определены как зависимости maven и поэтому упакованы в файлы WAR и EAR. Некоторые из них также были установлены на сервер приложений, к сожалению, другой версии. Когда мы запустили приложение, мы столкнулись с различными исключениями, которые были связаны с этими типами проблем. Есть хорошая статья IBM об этих исключениях, если вы хотите копнуть глубже.

Несмотря на то, что мы знали, что ошибка была вызвана некоторыми дважды определенными библиотеками на пути к классам, потребовалось более двух часов, чтобы выяснить, какая версия нам действительно нужна, и какой JAR-файл удалить.

Та же тема случайно на кувшине на той же неделе

Через несколько дней мы приняли участие. Вы действительно получаете Classloaders? сессия Общества пользователей Java в Цюрихе. Саймон Мэйпл очень хорошо рассказал о загрузчиках классов и с самого начала углубился в детали. Это была сенсационная сессия для многих. Я также должен отметить, что Саймон работает без поворотов, и он благовествует JRebel. В такой ситуации учебная сессия обычно смещена в сторону реального продукта, который является хлебом для репетитора. В этом случае мое мнение таково, что Саймон был абсолютно джентльменом и придерживался этических норм.

Создание инструмента, чтобы разгадать тайну

просто чтобы создать еще один

Неделю спустя у меня было время заняться программированием, которого у меня не было на пару недель, и я решил создать небольшой инструмент, в котором перечислены все классы и файлы JAR, которые находятся на пути к классам, чтобы было легче найти исследование дубликаты. Я пытался опираться на тот факт, что загрузчики классов обычно являются экземплярами URLClassLoader и, таким образом, метод getURLs() может быть вызван для получения всех имен каталогов и файлов JAR.

Модульное тестирование в такой ситуации может быть очень сложным, поскольку функциональность тесно связана с поведением загрузчика классов. Чтобы быть прагматичным, я решил просто провести некоторое ручное тестирование, начинающееся с JUnit, при условии, что код является экспериментальным. Прежде всего я хотел посмотреть, стоит ли развивать эту концепцию дальше. Я планировал выполнить тест и посмотреть на операторы журнала, сообщающие об отсутствии дублирующихся классов, а затем выполнить тот же прогон, но во второй раз добавив некоторые избыточные зависимости в путь к классам. Я использовал JUnit 4.10. В этом случае важна версия.

Я выполнил модульный тест из командной строки и увидел, что повторяющихся классов не было, и я был счастлив. После этого я выполнял тот же тест из Eclipse и удивился: я получил 21 класс с избыточным определением!

1
2
3
4
5
12:41:51.670 DEBUG c.j.c.ClassCollector - There are 21 redundantly defined classes.
12:41:51.670 DEBUG c.j.c.ClassCollector - Class org/hamcrest/internal/SelfDescribingValue.class is defined 2 times:
12:41:51.671 DEBUG c.j.c.ClassCollector -   sun.misc.Launcher$AppClassLoader@7ea987ac:file:/Users/verhasp/.m2/repository/junit/junit/4.10/junit-4.10.jar
12:41:51.671 DEBUG c.j.c.ClassCollector -   sun.misc.Launcher$AppClassLoader@7ea987ac:file:/Users/verhasp/.m2/repository/org/hamcrest/hamcrest-core/1.1/hamcrest-core-1.1.jar
...

Погуглив немного, я легко обнаружил, что JUnit 4.10 имеет дополнительную зависимость, как показывает maven.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
$ mvn dependency:tree
[INFO] Scanning for projects...
[INFO]                                                                        
[INFO] ------------------------------------------------------------------------
[INFO] Building clalotils 1.0.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ clalotils ---
[INFO] com.verhas:clalotils:jar:1.0.0-SNAPSHOT
[INFO] +- junit:junit:jar:4.10:test
[INFO] |  \- org.hamcrest:hamcrest-core:jar:1.1:test
[INFO] +- org.slf4j:slf4j-api:jar:1.7.7:compile
[INFO] \- ch.qos.logback:logback-classic:jar:1.1.2:compile
[INFO]    \- ch.qos.logback:logback-core:jar:1.1.2:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2.642s
[INFO] Finished at: Wed Sep 03 12:44:18 CEST 2014
[INFO] Final Memory: 13M/220M
[INFO] ------------------------------------------------------------------------

На самом деле это исправлено в 4.11, поэтому, если я изменю зависимость на JUnit 4.11, я не столкнусь с проблемой. ОК. Половина загадки раскрыта. Но почему выполнение командной строки maven не сообщает дважды определенные классы?

Расширяя регистрацию, регистрируя все больше и больше, я мог заметить строку:

1
12:46:19.433 DEBUG c.j.c.ClassCollector - Loading from the jar file /Users/verhasp/github/clalotils/target/surefire/surefirebooter235846110768631567.jar

Что находится в этом файле? Давайте распакуем это:

1
2
$ ls -l /Users/verhasp/github/clalotils/target/surefire/surefirebooter235846110768631567.jar
ls: /Users/verhasp/github/clalotils/target/surefire/surefirebooter235846110768631567.jar: No such file or directory

Файл не выходит! Казалось бы, maven создает этот файл JAR, а затем удаляет его, когда выполнение теста заканчивается. Погуглив снова, я нашел решение.

Java загружает классы из пути к классам. Путь к классам может быть определен в командной строке, но есть и другие источники для загрузчиков классов приложений, из которых можно получить файлы. Одним из таких источников является файл манифеста JAR. Файл манифеста файла JAR может определять, какие другие файлы JAR необходимы для выполнения классов в файле JAR. Maven создает файл JAR, который не содержит ничего, кроме файла манифеста, определяющего JAR и каталоги для перечисления пути к классам. Эти JAR-файлы и каталоги НЕ возвращаются методом getURLs() , поэтому (первая версия) моего маленького инструмента не нашла дубликатов.

В демонстрационных целях я был достаточно быстр, чтобы сделать копию файла во время выполнения команды mvn test , и получил следующий вывод:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
$ unzip /Users/verhasp/github/clalotils/target/surefire/surefirebooter5550254534465369201\ copy.jar
Archive:  /Users/verhasp/github/clalotils/target/surefire/surefirebooter5550254534465369201 copy.jar
  inflating: META-INF/MANIFEST.MF   
$ cat META-INF/MANIFEST.MF
Manifest-Version: 1.0
Class-Path: file:/Users/verhasp/.m2/repository/org/apache/maven/surefi
 re/surefire-booter/2.8/surefire-booter-2.8.jar file:/Users/verhasp/.m
 2/repository/org/apache/maven/surefire/surefire-api/2.8/surefire-api-
 2.8.jar file:/Users/verhasp/github/clalotils/target/test-classes/ fil
 e:/Users/verhasp/github/clalotils/target/classes/ file:/Users/verhasp
 /.m2/repository/junit/junit/4.10/junit-4.10.jar file:/Users/verhasp/.
 m2/repository/org/hamcrest/hamcrest-core/1.1/hamcrest-core-1.1.jar fi
 le:/Users/verhasp/.m2/repository/org/slf4j/slf4j-api/1.7.7/slf4j-api-
 1.7.7.jar file:/Users/verhasp/.m2/repository/ch/qos/logback/logback-c
 lassic/1.1.2/logback-classic-1.1.2.jar file:/Users/verhasp/.m2/reposi
 tory/ch/qos/logback/logback-core/1.1.2/logback-core-1.1.2.jar
Main-Class: org.apache.maven.surefire.booter.ForkedBooter
 
$

Это действительно не что иное, как файл манифеста, определяющий путь к классам. Но почему Maven это делает? Люди типа Sonatype, некоторые из которых я лично знаю, умные люди. Они не делают это просто так. Причиной создания временного файла JAR для запуска тестов является то, что длина командной строки ограничена в некоторых операционных системах, которые могут превышать длину пути к классам. Несмотря на то, что сама Java (начиная с Java 6) разрешает символы подстановки в пути к классам, это не вариант для maven. Файлы JAR находятся в разных каталогах репозитория maven, каждый из которых имеет длинное имя. Разрешение с использованием подстановочных знаков не является рекурсивным, для этого есть веская причина, и даже если бы это было так, вы бы не хотели, чтобы все ваши локальные репо находились на пути к классам.

Вывод

  • Не используйте JUnit 4.10! Используйте что-то старое или новое или будьте готовы к неожиданностям.
  • Понять, что такое загрузчик классов и как он работает, что делает.
  • Используйте операционную систему с огромным ограничением максимального размера длины командной строки.
    Или просто жить с ограничением.

Что-то другое? Ваши идеи?

Ссылка: Загадка загрузки классов была решена нашим партнером по JCG Питером Верхасом в блоге Java Deep .