前言
最近做项目,Java实际上一般情况也不用fatjar,毕竟CICD都是流水线构建,不过在预研的过程中,使用fatjar可以内置manifest的main类直接启动,就很方便,尤其是在服务器运行环境。实际上golang还是很方便的,可以交叉编译二进制可执行文件,不过在交叉编译跨语言的能力的时候经常很难弄环境。
fatjar
先构建一个java命令可执行的jar,可执行jar实际上就是fatjar,只不过没有内置依赖,内置依赖有2种主流方式:1、class文件内置;2、自定义内置class和jar(springboot)。
构建一个最简单的demo,那么怎么让这个main被java指令执行,就需要构建manifest文件,相当于jdk的元数据
java可以直接执行class文件,这里使用jar,一般而言项目不可能是是没有任何依赖的,而且依赖也不好管理,所以SpringBoot的单个jar都很大,因为包括依赖jar。
参考JDK官方文档:https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html
可执行jar
第1步加入
第2步加入main-class
通过官方文档知道原理后,一般都是通过Maven插件来打包,所以Maven加入如下插件
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<mainClass>
com.feng.fatjar.demo.FatJarMain
</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
打包后, 执行java -jar fat-jar-1.0-SNAPSHOT.jar
fatjar
刚刚把class打成jar,并且可执行,但是如果有其他依赖jar怎么办,那么经常是一个大的可执行jar包。比如
代码改为
打包就需要把依赖包打入fatjar,使用各种Maven插件:
assembly插件
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.1</version>
<configuration>
<archive>
<manifest>
<mainClass>com.feng.fatjar.demo.FatJarMain</mainClass>
</manifest>
</archive>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
打包后执行
shade插件
同理一般使用shade插件,实际上很多Javaagent就是用的这个插件来打fatjar的,通过自定义classloader载入。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.feng.fatjar.demo.FatJarMain</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
执行结果同理
查看fatjar
还可以对包名修改,这个一般在Javaagent中经常使用,比如日志jar的包名
修改包名
比如修改包名
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.feng.fatjar.demo.FatJarMain</mainClass>
</transformer>
</transformers>
<relocations>
<relocation>
<pattern>org.apache.commons.lang3</pattern>
<shadedPattern>shade.org.apache.commons.lang3</shadedPattern>
</relocation>
</relocations>
</configuration>
</execution>
</executions>
</plugin>
修改后会统一替换字节码
字节码修改技术
SF、DSA、RSA冲突
Exception in thread "main" java.lang.SecurityException: Invalid signature file digest for Manifest main attributes
如果jar包带签名,那么在fatjar执行时签名是不对的,毕竟都不是原来的jar了,计算的hash肯定不对,所谓验签就是拿证书(公钥)解密hash,然后自己计算hash,比对是否被改动。
fatjar必须排除这些文件,不能用原来的文件验签,也可以自己加签,这个是没问题的,见官方解释
经签名的Jar包内包含了以下内容:https://www.cnblogs.com/jackofhearts/p/jar_signing.html
- 原Jar包内的class文件和资源文件
- 签名文件 META-INF/*.SF:这是一个文本文件,包含原Jar包内的class文件和资源文件的Hash
- 签名block文件 META-INF/*.DSA:这是一个数据文件,包含签名者的 certificate 和数字签名。其中 certificate 包含了签名者的有关信息和 public key;数字签名是对 *.SF 文件内的 Hash 值使用 private key 加密得来
那么排除签名文件
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.feng.fatjar.demo.FatJarMain</mainClass>
</transformer>
</transformers>
<relocations>
<relocation>
<pattern>org.apache.commons.lang3</pattern>
<shadedPattern>shade.org.apache.commons.lang3</shadedPattern>
</relocation>
</relocations>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
springboot打包
这个经常用,毕竟现在的Java项目大部分都是springboot项目,参考springboot官网:Spring Boot Maven Plugin Documentation
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.5</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
这个就是springboot自定义加载的,还对要加载的类jar做了索引,方便读取
总结
实际上工作中大部分Java项目都是通过这种jar方式来来执行的,当然也可以封装java class -cp xxx的方式执行,不过文件太分散,不便管理。虽然很多情况,我们没细究这个执行逻辑,实际上大部分是各种基础中间件封装,原理还是JDK的执行指令和加载manifest逻辑。