SpringBoot 部署打包成 jar 和 war 有什么不同?
jar包和war包的选择
JAR包(Java Archive):JAR包主要用于打包Java应用程序的可执行代码和相关的库文件。它可以包含类、资源文件、配置文件等,是一种用于打包和分发独立的Java应用程序的常见格式。JAR包通常用于桌面应用、命令行工具、Java库等。它可以在任何支持Java的平台上运行。
WAR包(Web Archive):WAR包主要用于打包和部署基于Java的Web应用程序。它包含了Web应用的静态资源(如HTML、CSS、JavaScript文件等),Java类文件、配置文件和其他相关资源。WAR包可以方便地部署到Java Web服务器(如Tomcat、Jetty等)中,并可以通过HTTP协议访问。
这种基于war部署和运行的方式,是“微服务”的反面例子,即一个包,不仅包含前后端,而且还包含后端里的所有业务。这种项目一般都是有些年头的历史项目。
现在由于Spring Boot内嵌了Tomcat,一般是直接打成jar包,里面不包含任何前端代码,当下比较流行的是用vue.js等做前端,前后端分别部署,jar 包放到 Linux 服务器以后,再用sh命令启动,然后再让前端通过 HTTP 等协议交互。在打成jar包的基础上,还可以拆分业务,比如把一个大项目的多个模块拆成若干个jar包,这样不同业务能单独部署,不同的业务之间还能用dubbo等方式调用,而且在业务jar包的基础上,还能引入负载均衡和分布式等组件,比如是引入Spring Cloud Alibaba组件实现微服务。
【WAR包建立的条件】
- 需要建立正确的Web应用程序的目录层次结构。
- 建立
WEB-INF
子目录,并在该目录下建立classes与lib两个子目录。 - 将Servlet类文件放到
WEB-INF\classes
目录下,将Web应用程序所使用Java类库文件(即JAR文件)放到WEB-INF\lib
目录下。 - 将JSP页面或静态HTML页面放到上下文根路径下或其子目录下。
- 建立
META-INF
目录,并在该目录下建立context.xml
文件。
将Spring Boot项目打包成jar和war的操作
先介绍将SpringBoot打包成jar包的方式:(以下示例是在idea中演示)
一、打包成jar
1)先new 一个Spring Starter Project
这里注意packaging默认为jar,不用修改.
2)创建完成后项目的pom如下:
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3)打成jar包(通过maven命令的方式):
在Terminal窗口,使用 mvn clean package
命令打包:
然后在target目录下就能看到打包好的jar包了
二、打包成war包形式
1)可以在刚才创建的项目上做改动,首先打包成war需要一个ServletInitializer
类,这个类的位置需要和启动类在同一个文件下
如果一开始选择war包形式,会自动创建此类
2)修改pom.xml
修改pom.xml
的war将原先的jar改为war;
3)如果我们的SpringBoot是使用html作为前端页面开发没有问题,但是如果我们想用jsp开发,这个时候就需要配置一些依赖了:主要是排除SpringBoot的内置Tomcat,添加javax.servlet-api
和tomcat-servlet-api
(SpringMVC还需要配置后缀);
最后的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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-servlet-api</artifactId>
<version>8.0.36</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
因为SpringBoot默认推荐的是html,而不是jsp;经过上面的修改就可以使用jsp进行开发了;
4)打包成war:使用mvn clean package
如下:
打包成功后,就可以将war包放在tomcat下的webapps下,然后运行tomcat,启动项目了;
当然了,在创建项目的时候直接选择package为war,直接就能打成war包了
当选择war为打包方式创建项目时,ServletInitializer
是默认直接创建的
此时,pom文件如下
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.4.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>war</packaging>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
直接mvn clean package
就能打包成功
SpringBoot应用jar包启动原理详解
maven打包
Spring Boot项目的pom.xml文件中默认使用spring-boot-maven-plugin插件进行打包:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
在执行完maven clean package之后,会生成两个jar相关文件:
- test-0.0.1-SNAPSHOT.jar
- test-0.0.1-SNAPSHOT.jar.original
jar包目录结构
以笔者的test-0.0.1-SNAPSHOT.jar为例,来看一下jar的目录结构,其中都包含哪些目录和文件?
可以概述为:
spring-boot-learn-0.0.1-SNAPSHOT
├── META-INF
│ └── MANIFEST.MF
├── BOOT-INF
│ ├── classes
│ │ └── 应用程序
│ └── lib
│ └─── 第三方依赖jar
└── org
└─── springframework
└──── boot
└──────loader
└─────── springboot启动程序
其中主要包括三大目录:META-INF、BOOT-INF、org。
META-INF内容
META-INF记录了相关 jar 包的基础信息,包括:入口程序。具体内容如下:
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: tms-start
Implementation-Version: 0.0.1-SNAPSHOT
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.saint.StartApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.4.5
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher
Main-Class是org.springframework.boot.loader.JarLauncher,即jar启动的Main函数;Start-Class是com.saint.StartApplication,即我们自己SpringBoot项目的启动类;也是下文提到的项目的引导类。
BOOT-INF内容
- BOOT-INF/classes目录:存放应用编译后的class文件源码;
- BOOT-INF/lib目录:存放应用依赖的所有三方jar包文件;
org目录内容
org目录下存放着所有SpringBoot相关的class文件,比如:JarLauncher、LaunchedURLClassLoader。
可执行Jar(JarLauncher)
从jar包内META-INF/MANIFEST.MF文件中的Main-Class属性值为org.springframework.boot.loader.JarLauncher,可以看出main函数是JarLauncher,即:SpringBoot应用中的Main-class属性指向的class为org.springframework.boot.loader.JarLauncher。
其实吧,主要是 Java官方文档规定:java -jar命令引导的具体启动类必须配置在MANIFEST.MF资源的Main-class属性中;又根据“JAR文件规范”,MANIFEST.MF资源必须存放在/META-INF/目录下。所以main函数才是JarLauncher。JarLauncher类继承图如下:
从JarLauncher的类注释我们看出JarLauncher的作用:
- 加载内部/BOOT-INF/lib下的所有三方依赖jar;
- 加载内部/BOOT-INF/classes下的所有应用class;
JarLauncher的运行步骤?
- 在解压jar包后的根目录下运行 java org.springframework.boot.loader.JarLauncher。项目引导类(META-INF/MANIFEST.MF文件中的Start-Class属性)被JarLauncher加载并执行。
- 如果直接运行Start-Class(示例的StartApplication)类,会报错ClassNotFoundException。
- Spring Boot依赖的jar文件均存放在BOOT-INF/lib目录下。JarLauncher会将这些jar文件作为Start-Class的类库依赖。
这也是为什么JarLauncher能够引导,而直接运行Start-Class却不行。
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);
}
}
JarLauncher#main()中新建了JarLauncher并调用父类Launcher中的launch()方法启动程序;
- BOOT_INF_CLASSES、BOOT_INF_LIB变量对应BOOT-INF/classes和lib路径;
- isNestedArchive(Archinve.Entry entry)方法用于判断FAT JAR资源的相对路径是否为nestedArchive嵌套文档。进而决定这些FAT JAR是否会被launch。当方法返回false时,说明FAT JAR被解压至文件目录。
Archive的概念
archive即归档文件,这个概念在linux下比较常见;通常就是一个tar/zip格式的压缩包;而jar正是zip格式的。SpringBoot抽象了Archive的概念,一个Archive可以是jar(JarFileArchive),也可以是文件目录(ExplodedArchive);这样也就统一了访问资源的逻辑层;
public interface Archive extends Iterable<Archive.Entry>, AutoCloseable {
....
}
Archive继承自Archive.Entry,Archive.Entry有两种实现:
- JarFileArchive.JarFileEntry --> 基于java.util.jar.JarEntry实现,表示FAT JAR嵌入资源。
- ExplodedArchive.FileEntry --> 基于文件系统实现;
两者的主要差别是ExplodedArchive相比于JarFileArchive多了一个获取文件的getFile()方法;
public File getFile() {
return this.file;
}
也就是说一个在jar包环境下寻找资源,一个在文件夹目录下寻找资源;所以从实现层面证明了JarLauncher支持JAR和文件系统两种启动方式。当执行java -jar命令时,将调用/META-INF /MANIFEST.MF文件的Main-Class属性的main()方法,实际上调用的是JarLauncher#launch(args)方法;
Launcher#launch(args)方法
protected void launch(String[] args) throws Exception {
if (!isExploded()) {
// phase1:注册jar URL处理器
JarFile.registerUrlProtocolHandler();
}
// phase2:创建ClassLoader
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
String jarMode = System.getProperty("jarmode");
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
// phase3:调用实际的引导类launch
launch(args, launchClass, classLoader);
}
launch()方法分三步:
- 注册jar URL处理器;
- 为所有的Archive创建可以加载jar in jar目录的ClassLoader;
- 调用实际的引导类(Start-Class);
phase1 注册jar URL处理器
private static final String PROTOCOL_HANDLER = "java.protocol.handler.pkgs";
private static final String HANDLERS_PACKAGE = "org.springframework.boot.loader";
public static void registerUrlProtocolHandler() {
String handlers = System.getProperty(PROTOCOL_HANDLER, "");
System.setProperty(PROTOCOL_HANDLER, ("".equals(handlers) ? HANDLERS_PACKAGE
: handlers + "|" + HANDLERS_PACKAGE));
// 重置缓存的UrlHandlers;
resetCachedUrlHandlers();
}
private static void resetCachedUrlHandlers() {
try {
// 由URL类实现:通过URL.setURLStreamHandlerFactory()获得URLStreamHandler。
URL.setURLStreamHandlerFactory(null);
}
catch (Error ex) {
// Ignore
}
}
JarFile#resetCachedUrlHandlers()方法利用java.net.URLStreamHandler扩展机制,实现由URL#getURLStreamHandler(String)提供。
URL#getURLStreamHandler(String protocol)方法:
首先,URL的关联协议(Protocol)对应一种URLStreamHandler实现类。JDK内建了一些协议的实现,这些实现均存放在sun.net.www.protocol包下,并且类名必须为Handler,其类全名模式为sun.net.www.protocol.{protocol}.Handler(包名前缀.协议名.Handler),其中{protocol}表示协议名。
如果需要扩展,则必须继承URLStreamHandler类,通过配置Java系统属性java.protocol.handler.pkgs,追加URLStreamHandler实现类的package,多个package以“|”分割。
所以对于SpringBoot的JarFile,registerURLProtocolHandler()方法将package org.springframework.boot.loader追加到java系统属性java.protocol.handler.pkgs中。
也就是说,org.springframework.boot.loader包下存在协议对应的Handler类,即org.springframework.boot.loader.jar.Handler;并且按照类名模式,其实现协议为JAR。
另外:在URL#getURLStreamHandler()方法中,处理器先读取Java系统属性java.protocol.handler.pkgs,无论其是否存在,继续读取sun.net.www.protocol包;所以JDK内建URLStreamHandler实现是兜底的。
为什么SpringBoot要选择覆盖URLStreamHandler?
- Spring BOOT FAT JAR除包含传统Java Jar资源之外,还包含依赖的JAR文件;即存在jar in jar的情况;
- 默认情况下,JDK提供的ClassLoader只能识别jar中的class文件以及加载classpath下的其他jar包中的class文件,对于jar in jar的包无法加载;
- 当SpringBoot FAT JAR被java -jar命令引导时,其内部的JAR文件无法被内嵌实现sun.net.www.protocol.jar.Handler当做class Path,故需要定义了一套URLStreamHandler实现类和JarURLConnection实现类,用来加载jar in jar包的class类文件;
phase2 创建可以加载jar in jar目录的ClassLoader
获取所有的Archive,然后针对每个Archive分别创建ClassLoader;
ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
/**
* 获取所有的Archive(包含jar in jar的情况)
*/
protected Iterator<Archive> getClassPathArchivesIterator() throws Exception {
return getClassPathArchives().iterator();
}
/**
* 针对每个Archive分别创建ClassLoader
*/
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
return createClassLoader(urls.toArray(new URL[0]));
}
phase3 调用实际的引导类(Start-Class)
// case1: 通过ExecutableArchiveLauncher#getMainClass()获取MainClass
String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
// 2、运行实际的引导类
launch(args, launchClass, classLoader);
对于phase3,大致可以分为两步:
- 首先通过ExecutableArchiveLauncher#getMainClass()获取mainClass(即:/META-INF/MANIFEST.MF资源中的Start-Class属性);
- 利用反射获取mainClass类中的main(Stirng[])方法并调用;
<1> 获取mainClass:
Start-Class属性来自/META_INF/MANIFEST.MF资源中。Launcher的子类JarLauncher或WarLauncher没有实现getMainClass()方法。所以无论是Jar还是War,读取的SpringBoot启动类均来自此属性。
<2> 执行mainClass的main()方法:
获取mainClass之后,MainMethodRunner#run()方法利用反射获取mainClass类中的main(Stirng[])方法并调用。
运行JarLauncher实际上是在同进程、同线程内调用Start-Class类的main(Stirng[])方法,并且在调用前准备好Class Path。
WarLauncher
WarLauncher是可执行WAR的启动器。WarLauncher与JarLauncher的差异很小,主要区别在于项目文件和JAR Class Path路径的不同。
- 相比于FAT Jar的目录,WAR增加了WEB-INF/lib-provided,并且该目录仅存放provided的JAR文件。
- 传统的Servlet应用的Class Path路径仅关注WEB-INF/classes/和WEB-INF/lib/目录,因此WEB-INF/lib-provided/中的JAR将被Servlet忽略。
好处:打包后的WAR文件能够在Servlet容器中兼容运行。所以JarLauncher和WarLauncher并无本质区别。
总结
**Spring Boot应用Jar/War的启动流程:**Spring Boot应用打包之后,生成一个Fat jar,包含了应用依赖的所有三方jar包和SpringBoot Loader相关的类。Fat jar的启动Main函数是JarLauncher,它负责创建一个LaunchedURLClassLoader来加载BOOT-INF/classes目录以及/BOOT-INF/lib下面的jar,并利用反射获取mainClass类中的main(Stirng[])方法并调用。即:运行JarLauncher实际上是在同进程、同线程内调用Start-Class类的main(Stirng[])方法,并且在调用前准备好Class Path。
其他点:
SpringBoot通过扩展JarFile、JarURLConnection及URLStreamHandler,实现了jar in jar中资源的加载。
SpringBoot通过扩展URLClassLoader --> LauncherURLClassLoader,实现了jar in jar中class文件的加载。
WarLauncher相比JarLauncher只是多加载WEB-INF/lib-provided目录下的jar文件。