前言

最近做项目,Java实际上一般情况也不用fatjar,毕竟CICD都是流水线构建,不过在预研的过程中,使用fatjar可以内置manifest的main类直接启动,就很方便,尤其是在服务器运行环境。实际上golang还是很方便的,可以交叉编译二进制可执行文件,不过在交叉编译跨语言的能力的时候经常很难弄环境。

fatjar

先构建一个java命令可执行的jar,可执行jar实际上就是fatjar,只不过没有内置依赖,内置依赖有2种主流方式:1、class文件内置;2、自定义内置class和jar(springboot)。

maven 发布jar到私服 maven fat jar_java

构建一个最简单的demo,那么怎么让这个main被java指令执行,就需要构建manifest文件,相当于jdk的元数据

maven 发布jar到私服 maven fat jar_java_02

java可以直接执行class文件,这里使用jar,一般而言项目不可能是是没有任何依赖的,而且依赖也不好管理,所以SpringBoot的单个jar都很大,因为包括依赖jar。

maven 发布jar到私服 maven fat jar_java_03

参考JDK官方文档:https://docs.oracle.com/javase/8/docs/technotes/guides/jar/jar.html 

可执行jar

第1步加入

maven 发布jar到私服 maven fat jar_jar_04

第2步加入main-class

maven 发布jar到私服 maven fat jar_apache_05

 

 通过官方文档知道原理后,一般都是通过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

maven 发布jar到私服 maven fat jar_java_06

fatjar 

刚刚把class打成jar,并且可执行,但是如果有其他依赖jar怎么办,那么经常是一个大的可执行jar包。比如

maven 发布jar到私服 maven fat jar_jar_07

代码改为

maven 发布jar到私服 maven fat jar_jar_08

 

打包就需要把依赖包打入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>

打包后执行

maven 发布jar到私服 maven fat jar_jar_09

 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>

执行结果同理

maven 发布jar到私服 maven fat jar_maven 发布jar到私服_10

查看fatjar

maven 发布jar到私服 maven fat jar_maven_11

 

 还可以对包名修改,这个一般在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>

修改后会统一替换字节码 

maven 发布jar到私服 maven fat jar_apache_12

字节码修改技术

maven 发布jar到私服 maven fat jar_apache_13

 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必须排除这些文件,不能用原来的文件验签,也可以自己加签,这个是没问题的,见官方解释

maven 发布jar到私服 maven fat jar_apache_14

经签名的Jar包内包含了以下内容:

  • 原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做了索引,方便读取

maven 发布jar到私服 maven fat jar_java_15

总结

实际上工作中大部分Java项目都是通过这种jar方式来来执行的,当然也可以封装java class -cp xxx的方式执行,不过文件太分散,不便管理。虽然很多情况,我们没细究这个执行逻辑,实际上大部分是各种基础中间件封装,原理还是JDK的执行指令和加载manifest逻辑。