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

  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 является создание одного артефакта на проект. Хотя бывают исключения, например, 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>
  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 выглядит следующим образом:

<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>
  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 проста:

<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"

Комментарии (0)