创建独立的Java可执行JAR的三种方法 - frankel


在这篇文章中,我们描述了三种创建独立的可执行JAR的方法。
当您的应用程序超出了十几行代码时,您可能应该将代码分成多个类。在Java中,经典打包格式是Java ARchive,也称为JAR。但是实际应用程序可能依赖于其他JAR包。
这篇文章旨在描述创建独立的可执行JAR(也称为uber-JAR或胖JAR)的方法。
 
什么是可执行JAR?
JAR只是类文件的集合。为了可执行,其META-INF/MANIFEST.MF文件应指向实现该main()方法的类。您可以使用Main-Class属性来执行此操作。这是一个例子:
Main-Class: path.to.MainClass

MainClass有一个static main(String…​ args)方法

 
处理类路径
大多数应用程序依赖现有代码。Java提供了类路径classpath的概念。类路径是运行时将查找以查找依赖代码的路径元素的列表。当运行Java类,定义通过类路径中-cp的命令行选项:
java -cp lib/one.jar;lib/two.jar;/var/lib/three.jar path.to.MainClass

Java运行时通过聚合来自所有引用的JAR的所有类并添加主类来创建类路径。
分发依赖于其他JAR的JAR时会出现新的问题:

  1. 您需要在相同版本中定义相同的库
  2. 更重要的是,该-cp参数不适用于JARs。要引用其他JAR,需要在JAR清单中通过Class-Path属性设置类路径:Class-Path: lib/one.jar;lib/two.jar;/var/lib/three.jar
  3. 因此,您需要根据清单将JAR放在目标文件系统上的相对或绝对*相同的位置。这意味着要打开JAR并先阅读清单。

 
Apache Assembly插件
Maven的Assembly Plugin使开发人员能够将项目输出组合到一个可分发的存档中,该存档还包含依赖项,模块,站点文档和其他文件。
Assembly插件依赖于特定的assembly.xml配置文件。它允许您选择要包含在工件中的文件。请注意,最终的工件不必是JAR:配置文件可让您在可用格式(例如zip,war等)之间进行选择。
该插件通过提供预定义的程序集来管理常见的用例。自包含的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目标
  • 将目标绑定到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.
%d [%thread]%-5level%logger-%msg%n java.lang.IllegalArgumentException:
  在META-INF/spring.factories中找不到自动配置类。
  如果使用的是自定义包装,请确保该文件正确无误。

原因是在同一路径下不同的JAR提供不同的资源文件,例如 META-INF/spring.factories。该插件遵循最后写入胜出策略覆盖相同资源文件。顺序是基于JAR的名称。
使用Assembly,您可以排除资源,但不能合并它们。当您需要合并资源时,您可能需要使用Apache Shade插件。
 
Apache Shade插件
Assembly插件是通用的;Shade插件仅专注于创建独立的JAR的任务。
该插件提供了将工件打包(包括其依赖项)并遮蔽(即重命名)某些依赖项的包的功能。
该插件基于转换器的概念:每个转换器负责处理一种类型的资源。转换器可以按原样复制资源,添加静态内容,将其与其他资源合并等。
虽然您可以开发一个转换器,但是该插件提供了一组现成的转换器:

ApacheLicenseResourceTransformer:防止许可证重复
ApacheNoticeResourceTransformer:准备合并 NOTICE
AppendingTransformer:向资源添加内容
ComponentsXmlResourceTransformer:聚合多个 components.xml
DontIncludeResourceTransformer:防止包含匹配资源
GroovyResourceTransformer:合并Apache Groovy扩展模块
IncludeResourceTransformer:从项目添加文件
ManifestResourceTransformer:在 MANIFEST设置输入项
PluginXmlResourceTransformer:聚合多个Maven配置 plugin.xml
ResourceBundleAppendingTransformer:合并ResourceBundle
ServicesResourceTransformer:META-INF/services资源中已重定位的类名并将其合并
XmlAppendingTransformer:将XML内容添加到XML资源
PropertiesTransformer:合并拥有序数的属性文件以解决冲突
OpenWebBeansPropertiesTransformer:合并Apache OpenWebBeans配置文件
MicroprofileConfigTransformer:根据序号合并冲突的Microprofile配置属性

 
上面程序集的Shade插件配置如下:
<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产生两个工件:
  1. <name>-<version>.jar:自包含的可执行文件JAR
  2. original-<name>-<version>.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档案
  • 添加所需的变压器代码

此配置有效!仍然有剩余警告:
  • Manifests
  • Licenses, notices and similar files
  • Spring Boot specific files i.e. spring.handlers, spring.schemas and spring.tooling
  • Spring Boot-Kotlin specific files e.g. spring-boot.kotlin_module, spring-context.kotlin_module, etc.
  • Service loader configuration files
  • JSON files

您可以添加和配置其他变压器来修复其余的警告。总而言之,整个过程需要对每种资源以及如何使用它们有深刻的理解。
 
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       


目录说明:

  • classes:项目编译类
  • lib:JAR依赖
  • loader:Spring Boot类加载类

这是我们的示例项目清单文件配置:
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: ch.frankel.blog.executablejar.ExecutableJarApplication

如您所见,主类是特定于Spring Boot的类,而“真实”主类在另一个条目下被引用。
有关JAR结构的更多信息,请参阅参考文档
 
结论
在这篇文章中,我们描述了三种创建独立的可执行JAR的方法:
  1. 组装非常适合简单的项目
  2. 当项目开始变得更加复杂并且您需要处理重复的文件时,请使用Shade
  3. 最后,对于Spring Boot项目,最好的选择是专用插件

可以在Github上以Maven格式找到此帖子的完整源代码。