Создание самодостаточных исполняемых JAR

от автора

Когда программное приложение выходит за пределы десятка строк кода, вам, вероятно, следует разделить код на несколько классов. На этом этапе встает вопрос о том, как их распределить. В 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:

Плагин 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> 
  1. Ссылайтесь на предварительно определенную самодостаточную конфигурацию JAR

  2. Установите главный класс для исполнения

  3. Выполните single <goal>

  4. Привяжите <goal> к package после формирование исходного JAR 

Запуск mvn package дает два артефакта:

  1. <name>-<version>.jar

  2. <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>
  1. shade привязан к фазе package по умолчанию

  2. Этот преобразователь предназначен для генерации файлов манифеста

  3. Выполните ввод Main-Class

  4. Настройте финальный JAR так, чтобы он был многорелизным JAR. Это необходимо в случае, когда любой из исходных JAR является многорелизным JAR

Запуск mvn package дает два артефакта:

  1. <name>-<version>.jar: самодостаточный исполняемый JAR

  2. 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

  1. Объедините Log4J2 .dat файлы 

  2. Объедините файлы /META-INF/spring.factories

  3. Добавьте необходимый код для преобразователей

Эта конфигурация работает! Тем не менее, есть оставшиеся предупреждения:

  • Манифесты

  • Лицензии, предупреждения и схожие файлы

  • 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   
  1. Скомпилированные классы проекта

  2. JAR зависимости

  3. Загрузка классов в Spring Boot

Вот выдержка из манифеста по образцу проекта:

MANIFEST.MF

Main-Class: org.springframework.boot.loader.JarLauncher Start-Class: ch.frankel.blog.executablejar.ExecutableJarApplication

Как вы можете видеть, главный класс является специфичным классом Spring Boot, в то время как «настоящий» главный класс упоминается в другой записи.

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

Заключение 

В этой статье мы описали 3 различных способа создания самодостаточных исполняемых JAR:

  1. Assembly хорошо подходит для простых проектов

  2. Когда проект становится более сложным и вам нужно работать с дублирующимися файлами, используйте Shade

  3. Наконец, для проектов Spring Boot лучше всего использовать специальный плагин.

Полный исходный код этой статьи можно найти на Github в формате Maven.

Материалы для дополнительного изучения:


Что такое «хороший код» — это во многом спорная тема. Кто-то скажет, что если код работает, значит он достаточно хорош. Кто-то обязательно добавит, что код должен быть легок в понимании и сопровождении. А кто-то добавит, что код еще обязательно должен быть быстрым. Об этом уже много написано и сказано. Что же, давайте еще раз поговорим на эту интересную и холиварную тему. Регистрируйтесь на онлайн-интенсив

Перевод подготовлен в рамках курса «Java Developer. Basic»

ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/563860/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *