1. maven打包后的文件
进入springboot-jar/target目录,使用tree命令,目录结构如下:
$ tree
.
├── classes
│ └── com
│ └── gitee
│ └── funcy
│ └── maven
│ └── jar
│ ├── Main.class
│ └── controller
│ └── IndexController.class
├── generated-sources
│ └── annotations
├── maven-archiver
│ └── pom.properties
├── maven-status
│ └── maven-compiler-plugin
│ └── compile
│ └── default-compile
│ ├── createdFiles.lst
│ └── inputFiles.lst
├── springboot-jar-1.0.0.jar
└── springboot-jar-1.0.0.jar.original
14 directories, 7 files
注意springboot-jar-1.0.0.jar 与 springboot-jar-1.0.0.jar.original的区别:springboot-jar-1.0.0.jar.original属于原始Maven打包jar文件,该文件仅包含应用本地资源,如编译后的classes目录下的资源文件等,未引入第三方依赖资源;而springboot-jar-1.0.0.jar 引入了第三方依赖资源(主要为jar包)。
使用 unzip springboot-jar-1.0.0.jar -d tmp解压jar包,内容如下:
$ tree tmp/
tmp/
├── BOOT-INF
│ ├── classes
│ │ └── com
│ │ └── gitee
│ │ └── funcy
│ │ └── maven
│ │ └── jar
│ │ ├── Main.class
│ │ └── controller
│ │ └── IndexController.class
│ └── lib
│ ├── classmate-1.4.0.jar
│ ├── hibernate-validator-6.0.13.Final.jar
│ ├── jackson-annotations-2.9.0.jar
│ ├── jackson-core-2.9.7.jar
│ ├── jackson-databind-2.9.7.jar
│ ├── jackson-datatype-jdk8-2.9.7.jar
│ ├── jackson-datatype-jsr310-2.9.7.jar
│ ├── jackson-module-parameter-names-2.9.7.jar
│ ├── javax.annotation-api-1.3.2.jar
│ ├── jboss-logging-3.3.2.Final.jar
│ ├── jul-to-slf4j-1.7.25.jar
│ ├── log4j-api-2.11.1.jar
│ ├── log4j-to-slf4j-2.11.1.jar
│ ├── logback-classic-1.2.3.jar
│ ├── logback-core-1.2.3.jar
│ ├── slf4j-api-1.7.25.jar
│ ├── snakeyaml-1.23.jar
│ ├── spring-aop-5.1.3.RELEASE.jar
│ ├── spring-beans-5.1.3.RELEASE.jar
│ ├── spring-boot-2.1.1.RELEASE.jar
│ ├── spring-boot-autoconfigure-2.1.1.RELEASE.jar
│ ├── spring-boot-starter-2.1.1.RELEASE.jar
│ ├── spring-boot-starter-json-2.1.1.RELEASE.jar
│ ├── spring-boot-starter-logging-2.1.1.RELEASE.jar
│ ├── spring-boot-starter-tomcat-2.1.1.RELEASE.jar
│ ├── spring-boot-starter-web-2.1.1.RELEASE.jar
│ ├── spring-context-5.1.3.RELEASE.jar
│ ├── spring-core-5.1.3.RELEASE.jar
│ ├── spring-expression-5.1.3.RELEASE.jar
│ ├── spring-jcl-5.1.3.RELEASE.jar
│ ├── spring-web-5.1.3.RELEASE.jar
│ ├── spring-webmvc-5.1.3.RELEASE.jar
│ ├── tomcat-embed-core-9.0.13.jar
│ ├── tomcat-embed-el-9.0.13.jar
│ ├── tomcat-embed-websocket-9.0.13.jar
│ └── validation-api-2.0.1.Final.jar
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── com.gitee.funcy
│ └── springboot-jar
│ ├── pom.properties
│ └── pom.xml
└── org
└── springframework
└── boot
└── loader
├── ExecutableArchiveLauncher.class
├── JarLauncher.class
├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
├── LaunchedURLClassLoader.class
├── Launcher.class
├── MainMethodRunner.class
├── PropertiesLauncher$1.class
├── PropertiesLauncher$ArchiveEntryFilter.class
├── PropertiesLauncher$PrefixMatchingArchiveFilter.class
├── PropertiesLauncher.class
├── WarLauncher.class
├── archive
│ ├── Archive$Entry.class
│ ├── Archive$EntryFilter.class
│ ├── Archive.class
│ ├── ExplodedArchive$1.class
│ ├── ExplodedArchive$FileEntry.class
│ ├── ExplodedArchive$FileEntryIterator$EntryComparator.class
│ ├── ExplodedArchive$FileEntryIterator.class
│ ├── ExplodedArchive.class
│ ├── JarFileArchive$EntryIterator.class
│ ├── JarFileArchive$JarFileEntry.class
│ └── JarFileArchive.class
├── data
│ ├── RandomAccessData.class
│ ├── RandomAccessDataFile$1.class
│ ├── RandomAccessDataFile$DataInputStream.class
│ ├── RandomAccessDataFile$FileAccess.class
│ └── RandomAccessDataFile.class
├── jar
│ ├── AsciiBytes.class
│ ├── Bytes.class
│ ├── CentralDirectoryEndRecord.class
│ ├── CentralDirectoryFileHeader.class
│ ├── CentralDirectoryParser.class
│ ├── CentralDirectoryVisitor.class
│ ├── FileHeader.class
│ ├── Handler.class
│ ├── JarEntry.class
│ ├── JarEntryFilter.class
│ ├── JarFile$1.class
│ ├── JarFile$2.class
│ ├── JarFile$JarFileType.class
│ ├── JarFile.class
│ ├── JarFileEntries$1.class
│ ├── JarFileEntries$EntryIterator.class
│ ├── JarFileEntries.class
│ ├── JarURLConnection$1.class
│ ├── JarURLConnection$JarEntryName.class
│ ├── JarURLConnection.class
│ ├── StringSequence.class
│ └── ZipInflaterInputStream.class
└── util
└── SystemPropertyUtils.class
21 directories, 91 files
可以看到,文件中主要分为如下几个目录:
BOOT-INF/classes目录存放应用编译后的class文件;BOOT-INF/lib目录存放应用依赖的jar包;META-INF/目录存放应用依赖的jar包;org/目录存放spring boot相关的class文件。
2. java -jar 启动 springboot jar包
java官方规定,java -jar 命令引导的具体启动类必须配置在MANIFEST.MF文件中,而根据jar文件规范,MANIFEST.MF文件必须存放在/META-INF/目录下。因此,启动类配置在jar包的/META-INF/MANIFEST.MF文件中,查看该文件,内容如下:
$ cat MANIFEST.MF
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: chengyan
Start-Class: com.gitee.funcy.maven.jar.Main
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.1.1.RELEASE
Created-By: Apache Maven 3.6.0
Build-Jdk: 1.8.0_222
Main-Class: org.springframework.boot.loader.JarLauncher
发现Main-Class属性指向的Class 为 org.springframework.boot.loader.JarLauncher,而该类存放在jar包的org/springframework/boot/loader/目录下,并且项目的引导类定义在Start-Class属性性中,该属性并非java平台标准META-INF/MANIFEST.MF属性。
注:
org.springframework.boot.loader.JarLauncher是可执行jar的启动器,org.springframework.boot.loader.WarLauncher是可执行war的启动器。
org.springframework.boot.loader.JarLauncher所在的jar文件的Maven GAV信息为org.springframework.boot:spring-boot-loader:${springboot-version},通常情况下,这个依赖没有必要引入springboot项目的pom.xml文件。
查看 JarLauncher 源码,如下:
public class JarLauncher extends ExecutableArchiveLauncher {
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
}
可以发现,BOOT-INF/classes/ 与 BOOT-INF/lib/ 分别使用常量 BOOT_INF_CLASSES 和 BOOT_INF_LIB 表示,并且用于isNestedArchive(Archive.Entry) 方法判断,从该方法的实现分析,方法参数Archive.Entry看似为jar文件中的资源,比如application.properties。
Archive.Entry 有两种实现,其中一种为org.springframework.boot.loader.archive.JarFileArchive.JarFileEntry,基于java.util.jar.JarEntry,表示 FAT JAR 嵌入资源,另一种为org.springframework.boot.loader.archive.ExplodedArchive.FileEntry,基于文件系统实现。这也说明了JarLauncher支持JAR和文件系统两种启动方式。
文件系统启动方式如下:
- 解压jar包到
temp目录:unzip springboot-jar-1.0.0.jar -d tmp- 进入
temp目录,运行命令:java org.springframework.boot.loader.JarLauncher可以看到,项目同样能正常启动。
在 JarLauncher 作为引导类时,当执行java -jar 命令时,/META-INF 资源的Main-Class属性将调用其main(String[])方法,实际上调用的是JarLauncher#launch(args) 方法,而该方法继承于基类org.springframework.boot.loader.Launcher,它们之间的继承关系如下:
org.springframework.boot.loader.Launcherorg.springframework.boot.loader.ExecutableArchiveLauncherorg.springframework.boot.loader.JarLauncherorg.springframework.boot.loader.WarLauncher
简单来说,springboot jar启动过程如下:
java -jar xxx.jar运行的是JarLauncherJarLauncher#main(String[])方法会调用Launcher#launch(String[])方法,创建ClassLoader()及调用项目的main方法- 项目主类的获取实现位于
ExecutableArchiveLauncher#getMainClass(),主要是从/META-INF/MANIFEST.MF获取Start-Class属性 - 项目主类的main()方法调用位于
MainMethodRunner#run(),使用反射方式进行调用
- 项目主类的获取实现位于
3. java -jar 启动 springboot war包
从上面的分析,我们得到了启动jar包的org.springframework.boot.loader.JarLauncher以及启动war包的org.springframework.boot.loader.WarLauncher,这里我们来分析下WarLauncher上如何工作的。
WarLauncher 代码如下:
public class WarLauncher extends ExecutableArchiveLauncher {
private static final String WEB_INF = "WEB-INF/";
private static final String WEB_INF_CLASSES = WEB_INF + "classes/";
private static final String WEB_INF_LIB = WEB_INF + "lib/";
private static final String WEB_INF_LIB_PROVIDED = WEB_INF + "lib-provided/";
public WarLauncher() {
}
protected WarLauncher(Archive archive) {
super(archive);
}
@Override
public boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(WEB_INF_CLASSES);
}
else {
return entry.getName().startsWith(WEB_INF_LIB)
|| entry.getName().startsWith(WEB_INF_LIB_PROVIDED);
}
}
public static void main(String[] args) throws Exception {
new WarLauncher().launch(args);
}
}
可以看到,WEB-INF/classes/、WEB-INF/lib/、WEB-INF/lib-provided/均为WarLauncher的 Class Path,其中WEB-INF/classes/、WEB-INF/lib/是传统的Servlet应用的ClassPath路径,而 WEB-INF/lib-provided/属性springboot WarLauncher 定制实现。那么WEB-INF/lib-provided/ 究竟是干嘛的呢?看到provided,我们可以大胆猜想WEB-INF/lib-provided/存放的是pom.xml文件中,scope为provided的jar。
为了验证以上猜想,修改的pom.xml文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.gitee.funcy</groupId>
<artifactId>springboot-war</artifactId>
<version>1.0.0</version>
<!-- 指定打包方式为war包 -->
<packaging>war</packaging>
<name>springboot非parent war打包方式</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<spring-boot.version>2.1.1.RELEASE</spring-boot.version>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 测试war包中 WEB-INF/lib-provided/ 目录内容-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<!-- 打成war包时,需要添加该插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<!-- 保持与 spring-boot-dependencies 版本一致 -->
<version>3.2.2</version>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
这里我们添加了springboot的测试jar org.springframework.boot:spring-boot-test,并将其scope设置为provided.
运行maven打包命令mvn clean install -Dmaven.test.skip=true ,可以看到项目能正常打包。
打包完成后,进入target目录,运行java -jar springboot-war-1.0.0.war,项目能正常启动。
接下来,我们来看看springboot-war-1.0.0.war有些啥。首先使用unzip springboot-war-1.0.0.war -d tmp 命令解压,再使用tree -h 命令查看文件结构,结果如下
$ tree -h
.
├── [ 128] META-INF
│ ├── [ 311] MANIFEST.MF
│ └── [ 96] maven
│ └── [ 96] com.gitee.funcy
│ └── [ 128] springboot-war
│ ├── [ 95] pom.properties
│ └── [3.3K] pom.xml
├── [ 160] WEB-INF
│ ├── [ 96] classes
│ │ └── [ 96] com
│ │ └── [ 96] gitee
│ │ └── [ 96] funcy
│ │ └── [ 96] maven
│ │ └── [ 160] war
│ │ ├── [ 688] Main.class
│ │ ├── [ 891] StartApplication.class
│ │ └── [ 96] controller
│ │ └── [ 646] IndexController.class
│ ├── [1.2K] lib
│ │ ├── [ 65K] classmate-1.4.0.jar
│ │ ├── [1.1M] hibernate-validator-6.0.13.Final.jar
│ │ ├── [ 65K] jackson-annotations-2.9.0.jar
│ │ ├── [316K] jackson-core-2.9.7.jar
│ │ ├── [1.3M] jackson-databind-2.9.7.jar
│ │ ├── [ 33K] jackson-datatype-jdk8-2.9.7.jar
│ │ ├── [ 98K] jackson-datatype-jsr310-2.9.7.jar
│ │ ├── [8.4K] jackson-module-parameter-names-2.9.7.jar
│ │ ├── [ 26K] javax.annotation-api-1.3.2.jar
│ │ ├── [ 65K] jboss-logging-3.3.2.Final.jar
│ │ ├── [4.5K] jul-to-slf4j-1.7.25.jar
│ │ ├── [258K] log4j-api-2.11.1.jar
│ │ ├── [ 17K] log4j-to-slf4j-2.11.1.jar
│ │ ├── [284K] logback-classic-1.2.3.jar
│ │ ├── [461K] logback-core-1.2.3.jar
│ │ ├── [ 40K] slf4j-api-1.7.25.jar
│ │ ├── [294K] snakeyaml-1.23.jar
│ │ ├── [360K] spring-aop-5.1.3.RELEASE.jar
│ │ ├── [656K] spring-beans-5.1.3.RELEASE.jar
│ │ ├── [935K] spring-boot-2.1.1.RELEASE.jar
│ │ ├── [1.2M] spring-boot-autoconfigure-2.1.1.RELEASE.jar
│ │ ├── [ 413] spring-boot-starter-2.1.1.RELEASE.jar
│ │ ├── [ 421] spring-boot-starter-json-2.1.1.RELEASE.jar
│ │ ├── [ 423] spring-boot-starter-logging-2.1.1.RELEASE.jar
│ │ ├── [ 422] spring-boot-starter-tomcat-2.1.1.RELEASE.jar
│ │ ├── [ 421] spring-boot-starter-web-2.1.1.RELEASE.jar
│ │ ├── [1.0M] spring-context-5.1.3.RELEASE.jar
│ │ ├── [1.2M] spring-core-5.1.3.RELEASE.jar
│ │ ├── [274K] spring-expression-5.1.3.RELEASE.jar
│ │ ├── [ 23K] spring-jcl-5.1.3.RELEASE.jar
│ │ ├── [1.3M] spring-web-5.1.3.RELEASE.jar
│ │ ├── [782K] spring-webmvc-5.1.3.RELEASE.jar
│ │ ├── [3.1M] tomcat-embed-core-9.0.13.jar
│ │ ├── [244K] tomcat-embed-el-9.0.13.jar
│ │ ├── [257K] tomcat-embed-websocket-9.0.13.jar
│ │ └── [ 91K] validation-api-2.0.1.Final.jar
│ └── [ 96] lib-provided
│ └── [194K] spring-boot-test-2.1.1.RELEASE.jar
└── [ 96] org
└── [ 96] springframework
└── [ 96] boot
└── [ 544] loader
├── [3.5K] ExecutableArchiveLauncher.class
├── [1.5K] JarLauncher.class
├── [1.5K] LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
├── [5.6K] LaunchedURLClassLoader.class
├── [4.6K] Launcher.class
├── [1.5K] MainMethodRunner.class
├── [ 266] PropertiesLauncher$1.class
├── [1.4K] PropertiesLauncher$ArchiveEntryFilter.class
├── [1.9K] PropertiesLauncher$PrefixMatchingArchiveFilter.class
├── [ 19K] PropertiesLauncher.class
├── [1.7K] WarLauncher.class
├── [ 416] archive
│ ├── [ 302] Archive$Entry.class
│ ├── [ 437] Archive$EntryFilter.class
│ ├── [ 945] Archive.class
│ ├── [ 273] ExplodedArchive$1.class
│ ├── [1.1K] ExplodedArchive$FileEntry.class
│ ├── [1.5K] ExplodedArchive$FileEntryIterator$EntryComparator.class
│ ├── [3.7K] ExplodedArchive$FileEntryIterator.class
│ ├── [5.1K] ExplodedArchive.class
│ ├── [1.7K] JarFileArchive$EntryIterator.class
│ ├── [1.1K] JarFileArchive$JarFileEntry.class
│ └── [7.2K] JarFileArchive.class
├── [ 224] data
│ ├── [ 485] RandomAccessData.class
│ ├── [ 282] RandomAccessDataFile$1.class
│ ├── [2.6K] RandomAccessDataFile$DataInputStream.class
│ ├── [3.2K] RandomAccessDataFile$FileAccess.class
│ └── [3.9K] RandomAccessDataFile.class
├── [ 768] jar
│ ├── [4.9K] AsciiBytes.class
│ ├── [ 616] Bytes.class
│ ├── [3.0K] CentralDirectoryEndRecord.class
│ ├── [5.1K] CentralDirectoryFileHeader.class
│ ├── [4.5K] CentralDirectoryParser.class
│ ├── [ 540] CentralDirectoryVisitor.class
│ ├── [ 345] FileHeader.class
│ ├── [ 12K] Handler.class
│ ├── [3.5K] JarEntry.class
│ ├── [ 299] JarEntryFilter.class
│ ├── [2.0K] JarFile$1.class
│ ├── [1.2K] JarFile$2.class
│ ├── [1.3K] JarFile$JarFileType.class
│ ├── [ 15K] JarFile.class
│ ├── [1.6K] JarFileEntries$1.class
│ ├── [2.0K] JarFileEntries$EntryIterator.class
│ ├── [ 14K] JarFileEntries.class
│ ├── [ 702] JarURLConnection$1.class
│ ├── [4.2K] JarURLConnection$JarEntryName.class
│ ├── [9.6K] JarURLConnection.class
│ ├── [3.5K] StringSequence.class
│ └── [1.8K] ZipInflaterInputStream.class
└── [ 96] util
└── [5.1K] SystemPropertyUtils.class
22 directories, 93 files
相比于FAT JAR的解压目录,War 增加了 WEB-INF/lib-provided,并且该目录仅有一个jar文件,即spring-boot-test-2.1.1.RELEASE.jar,这正是我们在pom.xml文件中设置的scope为provided的jar包。
由此可以得出结论:WEB-INF/lib-provided存放的是scope为provided的jar包。
我们现来看下META-INF/MANIFEST.MF的内容:
$ cat META-INF/MANIFEST.MF
Manifest-Version: 1.0
Built-By: chengyan
Start-Class: com.gitee.funcy.maven.war.Main
Spring-Boot-Classes: WEB-INF/classes/
Spring-Boot-Lib: WEB-INF/lib/
Spring-Boot-Version: 2.1.1.RELEASE
Created-By: Apache Maven 3.6.0
Build-Jdk: 1.8.0_222
Main-Class: org.springframework.boot.loader.WarLauncher
可以看到,该文件与jar包中的META-INF/MANIFEST.MF很相似,在文件中同样定义了Main-Class与Start-Class,这也说明了该war可以使用java -jar xxx.jar 和 java org.springframework.boot.loader.WarLauncher 启动,这也与我们的验证结果一致。
4. tomcat等外部容器启动war包
在springboo刚开始推广的时候,我们还是习惯于将项目打成war包,然后部署到tomcat等web容器中运行。那springboot的war包是如何做到既能用java命令启动,又能放在tomcat容器中启动呢?这就是之前提到的WEB-INF/lib-provided目录的功能了。
传统的servlet应用的class path路径仅关注WEB-INF/classes/和WEB-INF/lib/,WEB-INF/lib-provided/目录下的jar包将被servlet容器忽略,如servlet api,该api由servlet容器提供。我们在打包时,可以把servlet相关jar包的scope设置成provided,这样就完美实现了servlet容器启动与java命令启动的兼容:
- 当部署到
servlet容器中时,WEB-INF/lib-provided/目录下的jar包就被容器忽略了(由于servlet容器本身就提供了servlet的相关jar包,如果不忽略,就会出现jar包重复引入问题); - 当使用
java命令执行时,此时无servlet容器提供servlet的相关jar包,而WarLauncher在运行过程中会加载WEB-INF/lib-provided/目录下的jar包。