本文抄自:jar包的一些事儿

前言:

作为java程序员,日常工作就是打jar包,可是对jar包的了解有多少呢?

一、什么是jar包

jar包就是 Java Archive File,顾名思义,它的应用是与 Java 息息相关的,是 Java 的一种文档格式,是一种与平台无关的文件格式,可将多个文件合成一个文件。jar 包与 zip 包非常相似——准确地说,它就是 zip 包,所以叫它文件包。jar 与 zip 唯一的区别就是在 jar 文件的内容中,包含了一个 META-INF/MANIFEST.MF 文件,该文件是在生成 jar 文件的时候自动创建的,作为jar里面的"详情单",包含了该Jar包的版本、创建人和类搜索路径Class-Path等信息,当然如果是可执行Jar包,会包含Main-Class属性,表明Main方法入口,尤其是较为重要的Class-Path和Main-Class。

此外,值得注意的是,因为jar包主要是对class文件进行打包,而java编译生成的class文件是平台无关的,这就意味着jar包是跨平台的,所以不必关心涉及具体平台的问题。说到jar里面的文件,咱们来看看最普通的一个带有静态页面的springboot项目jar里面的内容,就会发现解压出来的jar并不简单,为了贴近实际咱们未做任何删减,可以看到有很多东西

只需要运行如下指令,就能看到jar里面的内容(调用jar指令的前提是已经配置了jdk的环境变量)

jar -tf springbootdemo-0.0.1-SNAPSHOT.jar

其中-tf 后接的jar就是我们要查看的jar。

大致看看里面的东西我们可以发现,除了.MF以及.class文件之外,jar还能打包静态资源文件如.html、.css以及.js等项目所需的一切,这也就意味着咱们能将自己的项目打成jar,即不管是web应用还是底层框架,都能打成jar包。

有的jar包是可以直接通过 java -jar 指令来执行的。我们都知道,有的类之所以能够执行,是因为它用你有main函数,该函数是程序的入口,同理,可执行的jar包中肯定是有某个.class文件提供了main函数才使得其可执行。那么问题来了,一个jar里面可能存在多个.class文件都有main函数的情况,我怎么知道该执行哪个?其实答案非常简单,就是看前面说的MANIFEST.MF里面的Main-Class属性,它会指定函数入口。

二、为什么要打jar包

当我们开发了一个程序以后,程序中有很多的类,如果需要提供给别人使用,发给对方一大堆源文件是非常不好的,因此通常需要把这些类以及相关的资源文件打包成一个 jar 包,把这个 jar 包提供给别人使用,同时提供给使用者清晰的文档。这样他人在拿到我们提供的jar之后,就能方便地进行调用。而且安全。能够对JAR文件进行数字签名,只让能够识别数字签名的用户使用里面的东西。

因此,建议大家在平时写代码搬砖的时候,注意把自己代码的通用部分抽离出来,主键积累一些通用的util类,将其逐渐模块化,最后打成jar包供自己在别的项目或者模块中使用,同时不断打磨jar里面的内容,将其做得越来越容易理解和通用,这样的好处是除了会对你的代码重构能力以及模块抽象能力有很好的帮助之外,更是一种从长期解放你的重复工作量,让你有更多的精力去做其他事情的方式,甚至当你抽象出业内足够通用的jar之后,jar包还能为你带来意想不到的利润(当然公司里该保密的东西还是得保密的)。这也是java发展得如此之好的原因,无论出于盈利或者非盈利的目的,将自己的通用工具或者框架抽取出来,打成jar包供他人调用,使得整个java生态圈变得越来越强大–几乎很多业务场景都能找到对应的jar包。

三、如何打jar包

1、通过jdk命令、

A:含有多个类的jar,类之间存在调用关系

先创建一个java项目,编写两个非常简单的类,Welcome.java和Teacher.jar,其中Welcome类在main函数里调用了Teacher类的静态方法greeting

Welcome.java 
package com.imooc.jardemo1; 
import com.imooc.jardemo1.impl.Teacher; 
public class Welcome { 
    public static void main(String[] args) {
        Teacher.greeting(); 
    } 
}

Teacher.java 
package com.imooc.jardemo1.impl; 
public class Teacher { 
    public static void greeting(){ 
        System.out.printf("Welcome!"); 
    } 
}

在命令行里,去到项目的src路径下,执行javac指令

javac com/imooc/jardemo1/Welcome.java

此时就会生成与这两个类相对应的.class字节码文件

由于jvm实际解析的是.class字节码文件而非.java文件,且jar中最好不要包含代码源文件,我们来将.class文件打个jar包,在src根目录下执行如下指令

jar -cvf welcome.jar com/imooc/jardemo1/Welcome.class com/imooc/jardemo1/impl/Teacher.class

c表示要创建一个新的jar包,v表示创建的过程中在控制台输出创建过程的一些信息,f表示给生成的jar包命名

打jar的时候,会生成一个META-INF的目录,里面有MANIFEST.MF这个清单列表。内容为:

Manifest-Version: 1.0 Created-By: 11 (Oracle Corporation)

此时生成的jar包还不能执行,因为缺少咱们先前说的Main-Class属性,导致jar被执行的时候,不知道执行哪个main函数。因此我们需要加上Main-Class,后接main函数所在类的全路径名(注意冒号之后一定要跟英文的空格,整个文件最后有一行空行)。

Manifest-Version: 1.0 Created-By: 11 (Oracle Corporation) Main-Class:
com.imooc.jardemo1.Welcome

添加完成后,重新执行指令打包,这次咱们在打包指令里多加一个参数,即多传入修改完成后的MANIFEST.MF文件

jar -cvfm welcome.jar META-INF/MANIFEST.MF com/imooc/jardemo1/Welcome.class com/imooc/jardemo1/impl/Teacher.class

其中多了一个参数m,表示要定义MANIFEST文件。之后再重新执行

java -jar welcome.jar

就会发现jar已成功执行

为了更方便,编译的时候使用:

javac com/imooc/jardemo1/Welcome.java -d target

该命令表示,将所有编译后的.class文件,都放到src/target文件夹下

再将先前修改好的META-INF文件夹整体复制或者移动到target/下,去到target目录,直接执行

jar -cvfm welcome.jar META-INF/MANIFEST.MF *

即可完成打包,注意最后一个位置变成了*,表示把当前目录下所有文件都打在jar包里。
此外,还有一种更简单的也更灵活的方式,不需要修改META-INF/MANIFEST.MF,即不需要指定main函数,而通过如下指令来动态指定

java -cp welcome.jar com.imooc.jardemo1.Welcome

其中cp表示classpath,后面接上全限的main函数所在的类即可

此种方式虽然灵活,但是由于不需要在MANIFEST.MF里面标注执行函数以及后面要将的Class-Path,需要调用方充分熟悉jar及其内部构造,否则需要在MANIFEST.MF以及相关的使用说明文档里描述清楚。

 

B、含有多个jar,jar之间存在调用关系

在原先的jardemo1项目里给Teacher.class打个jar,即在jardemo1/src目录下执行

javac com/imooc/jardemo1/impl/Teacher.java -d target2/

随后去到target2文件夹里将里面的信息打个jar包

jar -cvf teacher.jar *


将生成好的jar复制粘贴到jardemo2项目的lib目录底下(需要先创建好lib目录,其位于jardemo2根目录下,与src同级)

并新建一个项目jardemo2。

package com.imooc.jardemo2; 
import com.imooc.jardemo1.impl.Teacher; 

public class Welcome { 
    public static void main(String[] args) { 
        Teacher.greeting(); 
    } 
}

此时直接打jar包仍不可以运行,会报找不到Teacher这个类的错误,我们需要需要跟javac -cp一样,将teacher.jar加入到classpath里即可,具体做法是在MANIFEST.MF中配置Class-Path(如果是多个jar,则用英文空格隔开)。CLASSPATH是指定程序中所使用的类文件所在的位置。

Manifest-Version: 1.0 Created-By: 11 (Oracle Corporation) Main-Class:
com.imooc.jardemo2.Welcome Class-Path: ../../lib/teacher.jar

打jar包:

jar -cvfm welcome.jar META-INF/MANIFEST.MF *

之后再执行jar包,就会发现执行成功了

当然,更快捷的方式还是通过现在的java -cp指令来执行

2、springboot通过maven打jar包。

在这里插入图片描述

1、修改项目发布形式

<packaging>jar</packaging>

2、配置加载第三方jar包的目录

其中如果要制作jar,需要在< plugins >中添加maven插件maven-compiler-plugin否则在执行maven package时会提示编译时找不到导入的第三方包中相关类的,具体的代码如下:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>${maven-compiler-plugin.version}</version>
    <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <skip>true</skip>
        <encoding>UTF-8</encoding>
        <compilerArguments>
            <extdirs>${project.basedir}/src/main/resources/lib</extdirs>
        </compilerArguments>
    </configuration>
</plugin>

3、指定第三方jar包的打包路径

<!-- 主要配置:将引用的第三方 jar 包打进生成的 jar 文件的 BOOT-INF/lib 目录中 -->
<resources>
    <resource>
        <directory>src\main\resources\lib</directory>
        <targetPath>BOOT-INF\lib</targetPath>
      <!--  <includes>
            <include>**/*.jar</include>
        </includes>-->
    </resource>
    <resource>
        <directory>src/main/resources</directory>
    </resource>
</resources>


4、执行mvn clean package命令即可生成相应jar包。或者直接:

这样在打包时首先执行clean,然后执行package即可完成jar包制作。
在这里插入图片描述

 

四、jar包资源访问路径问题

java web开发中,我们有时会在程序中读取一些资源文件的内容。当我们在本地运行调试的时候是没有任何问题的,但是当我们将程序打成jar包上传到集群中后运行时,发现报错:“找不到配置文件的路径”。虽然jar中确实存在配置文件,但是还是读取不到。

 java中jar包内的类访问jar包内部的资源文件的路径问题:

在本地项目中,若我们要访问项目中的资源文件,则一般使用相对路径或者用System.getProperities("user.dir")得到项目根目录,然后再访问资源文件,但是在将该工程和资源文件打包为jar包,运行该jar文件时,会显示找不到资源文件的错误。

在如下项目结构树中,项目根目录为nlpir,如果我们要在src下的某个package的某个java文件中访问blackWhite文件夹中的文件,则相对路径为"blackWhite/....."即可。但是在打包为jar包时,即使我们把blackWhite文件夹同样加入到打包的文件行列,在运行该jar包时,会出错:找不到blackWhite中某文件的路径。

解决方法:使用Class.getResource或者是ClassLoader.getResourceAsStream()将文件内容放到InputStream中,具体使用如下:

String s1 = this.getClass().getResource("/library.properties").getPath();
或者为:
String s1 = CodeTest.class.getResource("/library.properties").getPath();
注意,使用class的getRescource时,要注意路径前要加"/",即根目录,此处的根目录是src

注意,使用class的getRescource时,要注意路径前要加"/",即根目录,此处的根目录是src

若像如下使用:

String class_str = this.getClass().getResource("logback.xml").getPath();
则会出错如

使用ClassLoader时,如下:
this.getClass().getClassLoader().getResource()

在使用ClassLoader时,路径前面不能加"/",使用相对路径。

@Test
    public void test4(){
        String class_str = this.getClass().getResource("/logback.xml").getPath();
        String class_str2 = TempTest.class.getResource("/logback.xml").getPath();
        String classLoader_str = this.getClass().getClassLoader().getResource("logback.xml").getPath();
        InputStream is = this.getClass().getClassLoader().getResourceAsStream("logback.xml");
        System.out.println(class_str);
        System.out.println(class_str2);
        System.out.println(classLoader_str);
        System.out.println(is == null );
    }
 
结果如下:

String ss = TempTest.class.getResource("/").getPath();
上述该代码得到的是项目的根目录,即nlpir的根目录,结果如下:


/C:/eclipse/eclipse/workspace/nlpir/out/production/nlpir/


如下代码:
@Test
    public void readProperties(){
        String ss = TempTest.class.getResource("/").getPath();
        System.out.println(ss);
        String s = new File(ss).getParentFile().getPath();
        System.out.println(s);
        String system_str = System.getProperty("user.dir");
        System.out.println(system_str);
    }
运行结果如下:

由此可见,打包成jar包时和在ide中直接运行的结果并不一样,所以在jar包中的class类要访问自己jar包中的资源文件时,应该使用Class.getResource或者是getResourceAsStream放在InputStream中,再进行访问。但是该方法只能访问到src下的资源文件,因为其根目录对应的就是src,无法访问到项目根目录下src外的文件,如上述项目结构图中的blackWhite中的文件无法访问到,解决方法还木有找到。。。。。。

当jar包外部的类需要访问某个jar包的资源文件时,使用JarFile类。

获取jar包内部的资源文件:

http://blog.csdn.net/luo_jia_wen/article/details/50057191

【解惑】深入jar包:从jar包中读取资源文件:

http://www.iteye.com/topic/483115

 jar读取资源配置文件,jar包内包外,以及包内读取目录的方法:

http://blog.csdn.net/T1DMzks/article/details/75099029

 java加载jar包下的资源文件过程及原理分析:

http://blog.csdn.net/puhaiyang/article/details/77409203

五、如何读取jar包中的资源

1、读取jar内的资源文件

这种情况就是在普通的java项目内部创建一个资源文件并读取,由于实际和资源文件都打包在了一块,可以直接调用。像这里,如果在根目录下执行jar包的main函数时,main函数有如下指令

InputStream is = Welcome.getClass().getResourceAsStream("static/text.txt");

则便能获取到项目根目录static/下面的text.txt的信息。

2、读取jar外的资源文件

这种情况更简单,指明需要去读取的路径即可。像这里,如果在根目录下执行jar包的main函数时,main函数有如下指令

InputStream is = new FileInputStream("/home/work/outside/text.txt");

则便能获取到/home/work/outside/text.txt绝对路径下的text.txt内容。

3、读取外部jar包里的资源文件

结合上文三.1.b,咱们可以先指定MANIFEST.MF里的Class-Path为所要读取的jar包所在的路径,之后和第3种情况一样访问目标jar中的资源文件即可。

4、访问Jar包内部的Jar包资源

接着jardemo2,把teacher.jar从src/lib复制一份,粘贴到target里,同时将target里的welcome.jar删除,如图

这时我们想直接打包成一个jar包welcome.jar,此jar包中包含了teacher.jar。welcome.class代码中直接调用teacher.jar中的teacher.class。

需要怎么做呢?首先要明白java类的加载机制:

执行jar其实也就是执行里面的class,而class之所以能够被执行,前提提交是被classloader加载到内存当中,而目前如何加载内部jar的问题也就简化到了如何让classloader加载这些存在于内部jar里的class。

classloader的加载机制主要是研究其双亲委派机制,大致讲解一下jar的运行过程。jar 运行过程和类加载机制有关,而类加载机制又和我们自定义的类加载器有关,现在我们先来了解一下双亲委派模式。

java 中类加载器分为三个:

  1. BootstrapClassLoader 负责加载 ${JAVA_HOME}/jre/lib 部分 jar 包
  2. ExtClassLoader 加载 ${JAVA_HOME}/jre/lib/ext 下面的 jar 包
  3. AppClassLoader 加载用户自定义 -classpath 或者 Jar 包的 Class-Path 定义的第三方包

类的生命周期为:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸载(Unloading)七个阶段。

当我们执行 java -jar 的时候 jar 文件以二进制流的形式被读取到内存,但不会加载到 jvm 中,类会在一个合适的时机加载到虚拟机中。类加载的时机:

  1. 遇到 new、getstatic、putstatic 或 invokestatic 这四条字节码指令时,如果类没有进行过初始化,则需要先对其进行初始化。这四条指令的最常见的 Java 代码场景是使用 new 关键字实例化对象的时候,读取或设置一个类的静态字段调用一个类的静态方法的时候。
  2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类。

当触发类加载的时候,类加载器也不是直接加载这个类。首先交给 AppClassLoader ,它会查看自己有没有加载过这个类,如果有直接拿出来,无须再次加载,如果没有就将加载任务传递给 ExtClassLoader ,而 ExtClassLoader 也会先检查自己有没有加载过,没有又会将任务传递给 BootstrapClassLoader ,最后 BootstrapClassLoader 会检查自己有没有加载过这个类,如果没有就会去自己要寻找的区域去寻找这个类,如果找不到又将任务传递给 ExtClassLoader ,以此类推最后才是 AppClassLoader 加载我们的类。这样做是确保类只会被加载一次。通常我们的类加载器只识别 classpath (这里的 classpath 指项目根路径,也就是 jar 包内的位置)下 .class 文件。jar 中其他的文件包括 jar 包被当做了资源文件,而不会去读取里面的 .class 文件。但实际上我们可以通过自定义类加载器来实现一些特别的操作。

学到这里,我们便大致明白,之前咱们这样的做法是使用AppClassloader来加载相关jar里面的class的,而在加了-jar参数之后,AppClassloader就只关注welcome.jar范围内的class了,注意这里说的是class,并不包含内部的jar,其内部的jar此时相当于是前面说的内部资源文件,是以二进制流的形式存在的,因此,此时是访问不到内部jar文件的。那该如何是好?

其实,这里的线索已经很充足了,我们其实就是用自定义的classloader来发现并获取其内部jar里的class即可。自定义ClassLoader需要继承ClassLoader抽象类,重写findClass方法,这个方法定义了ClassLoader查找class的方式。前面提到,Java 本身支持访问Jar包里面的资源, 他们以 Stream 的形式存在(他们本就处于Jar包之中),而Jar文件被描述为JarFile, 里面的资源文件被描述为JarEntry,可以通过判断JarEntry的Jar属性使得直接访问Jar包内部的Jar包,这里给出一些关键的程序语句以及思路。
首先我们可以以前面静态访问jar内部资源文件的方式访问jar

InputStream stream =      ClassLoader.getSystemResourceAsStream(name);

其中name可以通过遍历jar里面的内容获取到,并且能够过滤出以.jar结尾的文件名并读取
获取到InputStream之后,可以将其转换为File(网上很多教程),而后转换成JarFile

JarFile jarFile = new JarFile(file);

获取到了jarFile之后,便能获取到jar里面的信息并进行后续的操作了,

Enumeration enum = jarFile.entries(); 
while (enum.hasMoreElements()) { 
    process(enum.nextElement()); 
}

后续的操作无非就是获取到class二进制流并传递给classloader defineClass去定义并做后续加载(能实现的前提是你了解自定义类加载器的工作原理)
上述过程比较复杂,如果希望直接在自己的类里面访问引用在 Jar包中的Jar包, 可以使用Spring Boot打包插件。见下文7。

六、jar包和war包的区别

war包想必大家也都接触过,war是一个可以直接运行的web模块,通常应用于web项目中,将其打成war包部署到Tomcat等容器中。以大家熟悉的Tomcat举例,将war包放置在tomcat根目录的webapps目录下,如果Tomcat成功启动,这个包就会自动解压,就相当于发布了。

发现除了目录结构外,jar里有的war里也都有。war包是Sun提出的一种web应用程序格式,与jar类似,是很多文件的压缩包。war包中的文件按照一定目录结构来组织。根据其根目录下包含有html和jsp文件,或者包含有这两种文件的目录,另外还有WEB-INF目录。通常在WEB-INF目录下含有一个web.xml文件和一个classes目录,web.xml是这个应用的配置文件,而classes目录下则包含编译好的servlet类和jsp,或者servlet所依赖的其他类(如JavaBean)。通常这些所依赖的类也可以打包成jar包放在WEB-INF下的lib目录下。这也就意味着,war能打包的内容,jar也都可以。有的同学会问了,那既然是这样,直接用jar来替代war不就可以了?诚然,对于现今的应用来讲,主流都是用jar来替代war了。因为war仅服务于Web应用,而jar的涵盖范围更广。目前,war相较于jar的唯一优势在于,就拿tomcat来讲,当tomcat的进程启动之后,将符合规范的war包放在tomcat的webapps目录下的时候,tomcat会自动将war包解压并对外提供web服务,而jar包则不行。

过去由于并未通过微服务将机器资源进行隔离,因此提倡的是一个tomcat实例管理多个java web项目,因此对于java web项目,都提倡将其打成war包然后放置于同一个tomcat的webapps下进行管理,便于资源的统一利用。而随着微服务成为主流,同一台机器上的多个web服务可以通过docker等容器进行隔离,因此我们可以让每个容器都单独运行一个tomcat实例,每个tomcat实例独立运行一个web服务,换句话说,我们可以像springboot一样,将tomcat和web项目打成jar放在一起,以内嵌的方式来启动web服务,使得所有服务的启动方式更优雅和统一,不管是Web服务还是后台服务,均使用java -jar指令来启动。

七、springboot打的jar包和其他有什么区别

在spring boot里,很吸引人的一个特性是可以直接把应用打包成为一个jar/war,然后这个jar/war是可以通过命令(java -jar xxx.jar)直接启动的,不需要另外配置一个Web Server。这种jar包不能被其他项目所依赖,即使被依赖了也不能直接使用其中的类。

SpringBoot 的可执行jar包又称fat jar ,fat jar中主要增加了两部分,第一部分是lib目录,存放的是Maven依赖的jar包文件,jar 包中嵌入了除 java 虚拟机以外的所有依赖,第二部分是spring boot loader相关的类。在 \BOOT-INF\classes 目录下才是我们的代码,因此无法被直接引用。如果非要引用,可以在 pom.xml 文件中增加配置,将 Spring Boot 项目打包成两个 jar ,一个可执行,一个可引用。

普通的jar包,解压后直接就是包名,包里就是我们的代码。

3.2 META-INF

├─META-INF
│ ├─maven
│ ├─MANIFEST.MF 
在META-INF目录中有三个较为重要的文件,pom.xml和pom.properties和与maven同级的MANIFEST.MF,其中pom.xml即项目对应的pom.xml,

pom.properties,对应了项目的三个坐标。

#Created by Apache Maven 3.3.9
version=0.0.1-SNAPSHOT
groupId=com.cetc52
artifactId=platform

3.2.2 MANIFEST.MF

Manifest-Version: 1.0
Implementation-Title: platform
Implementation-Version: 0.0.1-SNAPSHOT
Built-By: lenovo
Implementation-Vendor-Id: com.cetc52
Spring-Boot-Version: 2.0.3.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.cetc52.platform.PlatformApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_131
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo ot-starter-parent/platform

3.3 BOOT-INF
在BOOT-INF中lib目录包含了Maven的jar依赖和第三方导入的jar依赖,这样才能顺利的运行该jar。classes目录则包含了全量的程序员编写的类对应的class文件和资源如js、html文件。其中在BOOT-INF/classes目录中包含配置文件application.properties,该文件记录了项目的配置信息,如果是打成了war包,则可以直接修改该配置文件,重启tomcat即可修改相关的配置。

八、springboot打出此jar包的原理

在5.3中已经提到过了,springboot打出来的jar包中包含了其他jar包,Spring boot能做到以一个fat jar来启动,最重要的一点是它实现了jar in jar的加载方式。要想使用jar包中的类,需要自己来写类加载器。spring-boot-maven-plugin(默认插件)实现了这一流程。

1.spring boot应用启动原理分析(这篇文章写于2015年为springboot1.0版本,不过原理仍然类似)

顺道参考:彻底透析SpringBoot jar可执行原理

Archive的概念:archive即归档文件,这个概念在linux下比较常见,通常就是一个tar/zip格式的压缩包,jar是zip格式。

在spring boot里,抽象出了Archive的概念。

一个archive可以是一个jar(JarFileArchive),也可以是一个文件目录(ExplodedArchive)。可以理解为Spring boot抽象出来的统一访问资源的层。

上面的demo-0.0.1-SNAPSHOT.jar 是一个Archive,然后demo-0.0.1-SNAPSHOT.jar里的/lib目录下面的每一个Jar包,也是一个Archive。

其加载原理则是通过自定义类加载器LaunchedURLClassLoader实现类加载。 流程图如下:JarLauncher

从MANIFEST.MF可以看到Main函数是JarLauncher,下面来分析它的工作流程。

1.以demo-0.0.1-SNAPSHOT.jar创建一个Archive:JarLauncher先找到自己所在的jar,即demo-0.0.1-SNAPSHOT.jar的路径,然后创建了一个Archive。

2.获取lib/下面的jar,并创建一个LaunchedURLClassLoader:JarLauncher创建好Archive之后,通过getNestedArchives函数来获取到demo-0.0.1-SNAPSHOT.jar/lib下面的所有jar文件,并创建为List。

3.创建好ClassLoader之后,再从MANIFEST.MF里读取到Start-Class,即com.example.SpringBootDemoApplication,然后创建一个新的线程来启动应用的Main函数。(Springboot2和Springboot1的最大区别在于,Springboo1会新起一个线程,来执行相应的反射调用逻辑,而SpringBoot2则去掉了构建新的线程这一步。)

spring boot应用启动流程总结:

spring boot应用打包之后,生成一个fat jar,里面包含了应用依赖的jar包,还有Spring boot loader相关的类
Fat jar的启动Main函数是JarLauncher,它负责创建一个LaunchedURLClassLoader来加载/lib下面的jar,并以一个新线程启动应用的Main函数。

Spring boot通过注册了一个自定义的Handler类来处理多重jar in jar的逻辑。这个Handler内部会用SoftReference来缓存所有打开过的JarFile。

对于一个ClassLoader,它需要哪些能力?1查找资源2读取资源。对应的API是:

public URL findResource(String name)
public InputStream getResourceAsStream(String name)

spring boot注册了一个Handler来处理"jar:"这种协议的URL
spring boot扩展了JarFile和JarURLConnection,内部处理jar in jar的情况
在处理多重jar in jar的URL时,spring boot会循环处理,并缓存已经加载到的JarFile
对于多重jar in jar,实际上是解压到了临时目录来处理,可以参考JarFileArchive里的代码
在获取URL的InputStream时,最终获取到的是JarFile里的JarEntryData

2maven插件的作用

想要知道fat jar是如何生成的,就必须知道spring-boot-maven-plugin工作机制,而spring-boot-maven-plugin属于自定义插件,因此我们又必须知道,Maven的自定义插件是如何工作的
Maven的自定义插件
Maven 拥有三套相互独立的生命周期: clean、default 和 site, 而每个生命周期包含一些phase阶段, 阶段是有顺序的, 并且后面的阶段依赖于前面的阶段。生命周期的阶段phase与插件的目标goal相互绑定,用以完成实际的构建任务。

 

 Spring Boot 项目中一个默认的插件配置 spring-boot-maven-plugin,这个打包插件存在 5 个方面的功能,从插件命令就可以看出:

五个功能分别是:

  • build-info:生成项目的构建信息文件 build-info.properties
  • repackage:这个是默认 goal,在 mvnpackage 执行之后,这个命令再次打包生成可执行的 jar,同时将 mvnpackage 生成的 jar 重命名为 *.origin
  • run:这个可以用来运行 Spring Boot 应用
  • start:这个在 mvn integration-test 阶段,进行 SpringBoot 应用生命周期的管理
  • stop:这个在 mvn integration-test 阶段,进行 SpringBoot 应用生命周期的管理

这里功能,默认情况下使用就是 repackage 功能,其他功能要使用,则需要开发者显式配置。

打包

repackage 功能的 作用,就是在打包的时候,多做一点额外的事情:

  1. 首先 mvnpackage 命令 对项目进行打包,打成一个 jar,这个 jar 就是一个普通的 jar,可以被其他项目依赖,但是不可以被执行
  2. repackage 命令,对第一步 打包成的 jar 进行再次打包,将之打成一个 可执行 jar ,通过将第一步打成的 jar重命名为 *.original 文件

打包成功之后, target 中的文件如下:这里有两个文件,第一个 restful-0.0.1-SNAPSHOT.jar 表示打包成的可执行 jar ,第二个 restful-0.0.1-SNAPSHOT.jar.original 则是在打包过程中 ,被重命名的 jar,这是一个不可执行 jar,但是可以被其他项目依赖的 jar。通过对这两个文件的解压,我们可以看出这两者之间的差异。

可以看到,可执行 jar 中,我们自己的代码是存在 于 BOOT-INF/classes/ 目录下,另外,还有一个 META-INF 的目录,该目录下有一个 MANIFEST.MF 文件,打开该文件,内容如下:

Manifest-Version: 1.0
Implementation-Title: restful
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: org.javaboy.restful.RestfulApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.6.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher

可以看到,这里定义了一个 Start-Class,这就是可执行 jar 的入口类, Spring-Boot-Classes 表示我们自己代码编译后的位置, Spring-Boot-Lib 则表示项目依赖的 jar 的位置。

换句话说,如果自己要打一个可执行 jar 包的话,除了添加相关依赖之外,还需要配置 META-INF/MANIFEST.MF 文件。

这是可执行 jar 的结构,那么不可执行 jar 的结构呢?

我们首先将默认的后缀 .original 除去,然后给文件重命名,重命名完成,进行解压:

解压后可以看到,不可执行 jar 根目录就相当于我们的 classpath,解压之后,直接就能看到我们的代码,它也有 META-INF/MANIFEST.MF 文件,但是文件中没有定义启动类等。

Manifest-Version: 1.0
Implementation-Title: restful
Implementation-Version: 0.0.1-SNAPSHOT
Build-Jdk-Spec: 1.8
Created-By: Maven Archiver 3.4.0

注意

这个不可以执行 jar 也没有将项目的依赖打包进来。

从这里我们就可以看出,两个 jar ,虽然都是 jar 包,但是内部结构是完全不同的,因此一个可以直接执行,另一个则可以被其他项目依赖。

一次打包两个 jar

一般来说,Spring Boot 直接打包成可执行 jar 就可以了,不建议将 Spring Boot 作为普通的 jar 被其他的项目所依赖。如果有这种需求,建议将被依赖的部分,单独抽出来做一个普通的 Maven 项目,然后在 Spring Boot 中引用这个 Maven 项目。

如果非要将 Spring Boot 打包成一个普通 jar 被其他项目依赖,技术上来说,也是可以的,给 spring-boot-maven-plugin 插件添加如下配置:

<build>
 <plugins>
 <plugin>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-maven-plugin</artifactId>
 <configuration>
 <classifier>exec</classifier>
 </configuration>
 </plugin>
 </plugins>
</build>

配置的 classifier 表示可执行 jar 的名字,配置了这个之后,在插件执行 repackage 命令时,就不会给 mvnpackage 所打成的 jar 重命名了,所以,打包后的 jar 如下:

第一个 jar 表示可以被其他项目依赖的 jar ,第二个 jar 则表示一个可执行 jar。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐