Создание самодостаточных исполняемых 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:

  • Плагин 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>
  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.

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

  • Maven Assembly plugin

  • Maven Shade plugin

  • maven-shaded-log4j-transformer

  • Maven Spring Boot plugin

  • The Executable Jar Format


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

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

Источник: https://habr.com/ru/company/otus/blog/563860/


Интересные статьи

Интересные статьи

Это моя вторая статья на тему создания адаптивной галереи проектов на JS. На основе замечаний и комментариев к первой я постарался исправить некоторые ошибки и поработал ...
Большинство тех, кто начинает изучать программирование, начали это из-за желания сделать свою игру. Нууу… я не исключение, но судьба меня занесла в веб разработку. Ч...
В первой части статьи мы рассмотрели основы работы с утилитой SIP, предназначенной для создания Python-обвязок (Python bindings) для библиотек, написанных на языках C и C++. Мы рассмотрели основн...
Существует традиция, долго и дорого разрабатывать интернет-магазин. :-) Лакировать все детали, придумывать, внедрять и полировать «фишечки» и делать это все до открытия магазина.
Всем доброго времени суток, меня зовут Сергей Носков. Сегодня я бы хотел рассказать о создании моего первого полноценного инди-проекта под названием 35ММ, вышедшего в Steam в 2016 году. Истор...