Когда ваше программное приложение выходит за пределы десятка строк кода, вам, вероятно, следует разделить код на несколько классов. На этом этапе встает вопрос о том, как их распределить. В 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. Classpath - это список элементов пути, который будет просматриваться в runtime, что поможет найти зависимый код. При запуске классов Java вы определяете classpath с помощью параметра командной строки -cp
:
java -cp lib/one.jar;lib/two.jar;/var/lib/three.jar path.to.MainClass
Java runtime создает classpath
, объединяя все классы из всех связанных JAR и добавляя при этом главный класс.
Новые проблемы возникают при дистрибуции JAR, которые зависят от других JAR:
Вам необходимо синхронизировать версии библиотек.
Что еще более важно, аргумент
-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 является создание одного артефакта на проект. Хотя бывают исключения, например, Javadoc и исходный код, но в целом, если вам нужно несколько артефактов, нужно создавать один проект на каждый артефакт. Идея плагина Assembly заключается в том, чтобы обойти это правило.
Плагин Assembly полагается на специальный конфигурационный файл assembly.xml
. Он позволяет вам выбирать, какие файлы будут включены в артефакт. Обратите внимание, что конечный артефакт не обязательно должен быть JAR: конфигурационный файл позволяет вам выбирать между доступными форматами, например, zip, war и т.д.
Плагин регулирует общие случаи использования, предоставляя предварительно определенные сборки (assemblies). Среди них - распространение самодостаточных JAR. Конфигурация выглядит следующим образом:
<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 выглядит следующим образом:
<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
: самодостаточный исполняемый JARoriginal-<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>
Объедините 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 проста:
<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"