想聊明白这个问题,需要补充一些前提条件,比如Fat jar、类加载机制等
1、Fat jar
我们在开发业务程序的时候,经常需要引用第三方的jar包,最终程序开发完成之后,通过打包程序,会把自己的代码和三方jar包一起打成同一个jar包,这种jar就称之为Fat jar
SpringBoot的maven插件,打包方式就是把整体项目打包成一个Fat jar,其中把应用程序代码打包到BOOT-INF/classes中,把三方jar包打包到BOOT-INF/lib中,把jar包的详细信息(启动入口等)放置在META-INF/MANIFEST.MF文件中。
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>xxxxApplication</mainClass>
</configuration>
</plugin>
</plugins>
整体Fat jar如图所示:
2、jar启动入口
如果一个jar内部有多个类都有main方法,那么java -jar启动的话会调用哪个main方法呢,换句话说就是一个jar启动的入口main方法是怎么定义的?
两种方式:
- 命令行直接指定,java -jar xxx.jar com.xx.xxxClass(不常用)
- maven打包的时候插件生成META-INF/MANIFEST.MF文件,文件中的Main-Class:后面定义了当前jar的启动入口
那么SpringBoot启动的入口是我们应用程序里面写得加了@SpringBootApplication的main方法吗? 显然不是,打开jar查看MANIFEST.MF文件,里面赫然写着Main-Class: org.springframework.boot.loader.JarLauncher
所以说,通过java -jar 启动一个Springboot项目,启动的入口是 org.springframework.boot.loader.JarLauncher类里面的main方法。
3、类加载
根据刚才看到的jar内部的目录结构,应用程序依赖的三方jar包都不在classpath目录下,按照已有的类加载器的职能来看,这些jar都不能被加载到JVM中,SpringBoot需要自定义类加载器来加载这些包,具体是怎么做的,还是那句话,源码之下无秘密。打开JarLauncher类,如图所示:
main方法很简单,就一行代码, (new JarLauncher()).launch(args); 跟代码进入launch方法。
由代码得知,SpringBoot是自定义了一个LaunchedURLClassLoader来加载SpringBoot应用的所有类
这个结论也可以程序实现得知:IDEA启动输出类加载和jar启动输出类加载器 ,结果一目了然
4、结论
Java -Jar是以FAT JAR的方式用LaunchedURLClassLoader来load class。而在IDE中则是直接以ApplicationClassLoader来load的。这种差别会导致调用classloader.getResourceAsStream()得到不一样的结果,这是因为FAT JAR启动时,LaunchedURLClassLoader的load的urls并没有FAT JAR本身,如abc-0.0.1-SNAPSHOT.jar, 但是应用中的src/main/resources/META-INF/resources目录被打包到了FAT JAR里,也就是abc-0.0.1-SNAPSHOT.jar!/META-INF/resources,这样这些resource也就不会被访问到了。
这也就是为什么有时候在IDE里能读到的resource在Run FAT JAR的情况下读不到了