
Когда программное приложение выходит за пределы десятка строк кода, вам, вероятно, следует разделить код на несколько классов. На этом этапе встает вопрос о том, как их распределить. В Java классическим форматом является Java-архив, более известный как JAR. Но реальные программы, вероятно, зависят от других JAR.
Цель этой статьи — описать способы создания самодостаточных исполняемых (self-contained executable) JAR, также известных как uber-JAR или fat JAR.
Что такое самодостаточный JAR?
JAR — это просто набор файлов классов. Чтобы быть исполняемым, его файл META-INF/MANIFEST.MF должен указывать на класс, реализующий метод main(). Это делается с помощью атрибута Main-Class. Вот пример:
Main-Class: path.to.MainClass
У MainClass метод static main(String… args)
Работа с classpath
Большинство программ зависит от существующего кода. Java предоставляет концепцию classpath. Путь класса — это список элементов пути, который будет просматриваться во время выполнения программы, что поможет найти зависимый код. При запуске классов Java вы определяете classpath с помощью параметра командной строки -cp:
java -cp lib/one.jar;lib/two.jar;/var/lib/three.jar path.to.MainClass
Время выполнения Java создает classpath, объединяя все классы из всех связанных JAR и добавляя при этом главный класс.
Новые проблемы возникают при дистрибуции JAR, которые зависят от других JAR:
1.Вам необходимо определить те же библиотеки в той же версии.
2. Что еще более важно, аргумент -cp не работает с JAR. Чтобы ссылаться на другие JAR, classpath должен быть задан в манифесте JAR через атрибут Class-Path:
Class-Path: lib/one.jar;lib/two.jar;/var/lib/three.jar
3. По этой причине вам необходимо поместить JAR в то же место, относительное или абсолютное, в целевую файловую систему в соответствии с манифестом. Это означает, что сначала нужно открыть JAR и прочитать манифест.
Одним из способов решения этих проблем является создание уникальной единицы развертывания, которая содержит классы из всех JAR и может быть распространена как один артефакт. Существует несколько вариантов создания таких JAR:
-
Плагин Assembly
-
Плагин Shade
-
Плагин Spring Boot (Для проектов Spring Boot)
Плагин Apache Assembly
Assembly Plugin для Apache Maven позволяет разработчикам объединять результаты проекта в единый распространяемый архив, который также содержит зависимости, модули, документацию сайта и другие файлы.
— Плагин Apache Maven Assembly
Одним из правил проектирования Maven является создание одного артефакта на проект. Существуют исключения, например, артефакты Javadocs и артефакты исходного кода, но в целом, если вам нужно несколько артефактов, вам нужно создать один проект для каждого артефакта. Идея плагина Assembly заключается в том, чтобы обойти это правило.
Плагин Assembly полагается на специальный конфигурационный файл assembly.xml. Он позволяет вам выбирать, какие файлы будут включены в артефакт. Обратите внимание, что конечный артефакт не обязательно должен быть JAR: конфигурационный файл позволяет вам выбирать между доступными форматами, например, zip, war и т.д.
Плагин регулирует общие случаи использования, предоставляя предварительно определенные сборки (assemblies). Среди них — распространение самодостаточных JAR. Конфигурация выглядит следующим образом:
pom.xml
<plugin> <artifactId>maven-assembly-plugin</artifactId> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <mainClass>ch.frankel.blog.executablejar.ExecutableJarApplication</mainClass> </manifest> </archive> </configuration> <executions> <execution> <goals> <goal>single</goal> </goals> <phase>package</phase> </execution> </executions> </plugin>
-
Ссылайтесь на предварительно определенную самодостаточную конфигурацию JAR
-
Установите главный класс для исполнения
-
Выполните
single<goal> -
Привяжите <goal> к
packageпосле формирование исходного JAR
Запуск mvn package дает два артефакта:
-
<name>-<version>.jar -
<name>-<version>-with-dependencies.jar
Первый JAR имеет то же содержимое, что и тот, который был бы создан без плагина. Второй — это самодостаточный JAR. Вы можете выполнить его следующим образом:
java -jar target/executable-jar-0.0.1-SNAPSHOT.jar
В зависимости от проекта он может выполняться успешно… или нет. Например, в примере проекта Spring Boot он не работает со следующим сообщением:
%d [%thread] %-5level %logger - %msg%n java.lang.IllegalArgumentException: No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.
Причина в том, что разные JAR предоставляют разные ресурсы по одному и тому же пути, как например с META-INF/spring.factories.
Зачастую плагин следует стратегии «побеждает последний записавший». Порядок основывается на имени JAR.
С помощью Assembly вы можете исключить ресурсы, но не объединять их. Если вам нужно объединить ресурсы, вы, вероятно, захотите использовать плагин Apache Shade.
Плагин Apache Shade
Плагин Assembly является общим; плагин Shade ориентирован исключительно на задачу создания самодостаточных JAR.
Этот плагин предоставляет возможность упаковать артефакт в uber-jar, включая его зависимости, и оттенить — т.е. переименовать — пакеты некоторых зависимостей.
— Плагин Apache Maven Shade
Плагин основан на концепции преобразователей: каждый преобразователь отвечает за работу с одним типом ресурсов. Преобразователь может копировать ресурс как есть, добавлять статическое содержимое, объединять его с другими и т.д.
Хотя вы можете разработать свой преобразователь, плагин предоставляет набор готовых преобразователей:

Конфигурация плагина Shade к приведенному выше Assembly выглядит следующим образом:
pom.xml
<plugin> <artifactId>maven-shade-plugin</artifactId> <executions> <execution> <id>shade</id> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>ch.frankel.blog.executablejar.ExecutableJarApplication</mainClass> <manifestEntries> <Multi-Release>true</Multi-Release> </manifestEntries> </transformer> </transformers> </configuration> </execution> </executions> </plugin>
-
shadeпривязан к фазеpackageпо умолчанию -
Этот преобразователь предназначен для генерации файлов манифеста
-
Выполните ввод
Main-Class -
Настройте финальный JAR так, чтобы он был многорелизным JAR. Это необходимо в случае, когда любой из исходных JAR является многорелизным JAR
Запуск mvn package дает два артефакта:
-
<name>-<version>.jar: самодостаточный исполняемый JAR -
original-<name>-<version>.jar: «обычный» JAR без встроенных зависимостей
При работе с проектом, взятым за образец, финальный исполняемый файл все еще не работает так, как ожидалось. Действительно, во время сборки появляется множество предупреждений о дублировании ресурсов. Два из них мешают корректной работе проекта. Чтобы правильно их объединить, нам нужно посмотреть на их формат:
-
META-INF/org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat:этот Log4J2 файл содержит предварительно скомпилированные данные плагина Log4J2. Он закодирован в двоичном формате, и ни один из готовых преобразователей не может объединить такие файлы. Тем не менее, случайный поиск показывает, что кто-то уже занимался этой проблемой и выпустил преобразователь для работы с объединением.
-
META-INF/spring.factories: эти файлы, специфичные для Spring, они имеют формат «один ключ/много значений». Поскольку они текстовые, ни один готовый преобразователь не может корректно объединить их. Однако разработчики Spring предоставляют такую возможность (и многое другое) в своем плагине.
Чтобы настроить эти преобразователи, нам нужно добавить вышеуказанные библиотеки в качестве зависимостей к плагину Shade:
<plugin> <artifactId>maven-shade-plugin</artifactId> <version>3.2.4</version> <executions> <execution> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>ch.frankel.blog.executablejar.ExecutableJarApplication</mainClass> <manifestEntries> <Multi-Release>true</Multi-Release> </manifestEntries> </transformer> <transformer implementation="com.github.edwgiz.maven_shade_plugin.log4j2_cache_transformer.PluginsCacheFileTransformer" /> <transformer implementation="org.springframework.boot.maven.PropertiesMergingResourceTransformer"> <resource>META-INF/spring.factories</resource> </transformer> </transformers> </configuration> </execution> </executions> <dependencies> <dependency> <groupId>com.github.edwgiz</groupId> <artifactId>maven-shade-plugin.log4j2-cachefile-transformer</artifactId> <version>2.14.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.4.1</version> </dependency> </dependencies> </plugin>
pom.xml
-
Объедините Log4J2
.datфайлы -
Объедините файлы
/META-INF/spring.factories -
Добавьте необходимый код для преобразователей
Эта конфигурация работает! Тем не менее, есть оставшиеся предупреждения:
-
Манифесты
-
Лицензии, предупреждения и схожие файлы
-
Spring Boot файлы, например,
spring.handlers,spring.schemasиspring.tooling -
Файлы Spring Boot-Kotlin, например,
spring-boot.kotlin_module,spring-context.kotlin_module, и так далее. -
Файлы конфигурации Service Loader
-
Файлы JSON
Вы можете добавить и настроить дополнительные преобразователи для устранения оставшихся предупреждений. В целом, весь процесс требует глубокого понимания каждого вида ресурсов и процесса работы с ними.
Плагин Spring Boot
Плагин Spring Boot использует совершенно другой подход. Он не объединяет ресурсы из JAR по отдельности; он добавляет зависимые JAR по мере их нахождения внутри uber JAR. Для загрузки классов и ресурсов он предоставляет специальный механизм загрузки классов. Очевидно, что он предназначен для проектов Spring Boot.
Настройка плагина Spring Boot проста:
pom.xml
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.4.1</version> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin>
Давайте проверим структуру финального JAR:
/ |__ BOOT-INF | |__ classes | |__ lib |__ META-INF | |__ MANIFEST.MF |__ org |__ springframework |__ loader
-
Скомпилированные классы проекта
-
JAR зависимости
-
Загрузка классов в Spring Boot
Вот выдержка из манифеста по образцу проекта:
MANIFEST.MF
Main-Class: org.springframework.boot.loader.JarLauncher Start-Class: ch.frankel.blog.executablejar.ExecutableJarApplication
Как вы можете видеть, главный класс является специфичным классом Spring Boot, в то время как «настоящий» главный класс упоминается в другой записи.
Для получения дополнительной информации о структуре JAR, пожалуйста, ознакомьтесь со справочной документацией.
Заключение
В этой статье мы описали 3 различных способа создания самодостаточных исполняемых JAR:
-
Assembly хорошо подходит для простых проектов
-
Когда проект становится более сложным и вам нужно работать с дублирующимися файлами, используйте Shade
-
Наконец, для проектов Spring Boot лучше всего использовать специальный плагин.
Полный исходный код этой статьи можно найти на Github в формате Maven.
Материалы для дополнительного изучения:
Что такое «хороший код» — это во многом спорная тема. Кто-то скажет, что если код работает, значит он достаточно хорош. Кто-то обязательно добавит, что код должен быть легок в понимании и сопровождении. А кто-то добавит, что код еще обязательно должен быть быстрым. Об этом уже много написано и сказано. Что же, давайте еще раз поговорим на эту интересную и холиварную тему. Регистрируйтесь на онлайн-интенсив
Перевод подготовлен в рамках курса «Java Developer. Basic»
ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/563860/
Добавить комментарий