一般开发Java应用都会使用Maven或Gradle管理依赖,SpringBoot应用也一样,创建的SpringBoot应用本身就是Maven或Gradle工程,只是引入了SpringBoot的相关依赖。
创建SpringBoot应用的途径或工具大概有三种,大部分时候会使用IDE(含STS)创建SpringBoot应用。
第一种是命令行
比如Maven命令行,因为SpringBoot应用本身就是Maven工程或Gradle工程。相较于后面两种创建方式,需要手动创建和编写pom文件。
第二种是IDE
基本上大部分IDE都有创建SpringBoot应用的插件可以下载安装,下载后,创建的过程种可以选择应用需要使用的依赖,然后生成工程并已经正确引入需要的依赖,不要人工再另外编辑。如果没有安装插件,也可以创建一般的工程(如Maven工程),然后手动引入SpringBoot或第三方库即可。
第三种是Spring官方提供的工具
包括Spring官方的STS(Spring Tools Suite,基于Eclipse开发的IDE)、以及Spring Initialzr网站在线创建。
如果是使用安装了创建SpringBoot应用的插件或Spring官方提供的工具创建,只需要按照提示操作即可。
如果不使用插件的IDE创建SpringBoot应用,大概可以分为三步
创建Maven/Gradle工程
基本上所有的IDE都支持创建Maven或Gradle工程,按照步骤提示操作
引入需要的SpringBoot或第三方库依赖
创建完工程后,引入应用所需的SpringBoot模块或第三方库。比如创建一个Web应用,为了减少Spring模块及其第三方库的版本管理,引入spring-boot-starter-parent作为parent;其次是引入spring-boot-starter-web模块,这是开发WEB应用所需的模块;接着,要将应用构建打包成为独立的Spring应用部署,需要引入spring-boot-maven-plugin插件;然后,如果要使用Spring的测试组件,可以引入spring-boot-starter-test模块;最后,按照应用需要引入其它第三方库。
如果创建的SpringBoot应用不能或无法使用spring-boot-starter-parent作为parent,则可以引入spring-boot-dependencies作为替代的依赖管理,此时spring-boot-maven-plugin插件需要明确指定版本信息且与spring-boot-dependencies保持一致,并且指定的执行目标goal为repackage;如果要打成war包,还需要另外引入apache的maven-war-plugin且版本需要符合或匹配SpringBoot所要求的版本。
创建引导类
引导类是启动SpringBoot应用的入口。如果是使用了插件或Spring官方工具创建的工程,会自动生成引导类。否则,需要手动创建。可以为引导类添加@SpringBootApplication做为引导启动SpringApplication的主要配置源。
接下来的,就是业务逻辑开发,和一般的Spring应用没有差别了。
下面,根据以上的创建步骤,演示如何使用IDEA社区版创建一个Hello,SpringBoot应用。首先它是一个Maven工程,属于Web应用,实现简单的业务逻辑,用户通过浏览器访问时,返回欢迎语,比如Hello,SpringBoot!。
使用IDEA创建一个Maven工程hello-spring-boot。初始的pom.xml如下
4.0.0
com.fandou.coffee.learning
hello-spring-boot
1.0-SNAPSHOT
修改pom.xml文件,引入spring-boot-starter-parent作为parent;因为创建的式Web应用需要使用SpringMVC模块,需要引入spring-boot-starter-web模块依赖;最后,添加spring-boot-maven-plugin插件用来打包部署;暂时不编写测试代码,不添加测试依赖。最终的pom.xml文件如下
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.3.3.RELEASE
com.fandou.coffee.learning
hello-spring-boot
1.0-SNAPSHOT
Hello,SpringBoot
SpringBoot开发入门示例
1.8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-maven-plugin
接着创建hello-spring-boot应用的引导类。创建应用的包com.fandou.coffee.learning.springboot.hello,然后包下创建引导类HelloSpringBootBootstrap。
/**
* HelloSpringBoot应用引导类
*
* 使用@SpringBootApplication开启自动装配
*/
@SpringBootApplication
public class HelloSpringBootBootstrap {
public static void main(String[] args) {
// HelloSpringBootBootstrap作为只要配置源
SpringApplication.run(HelloSpringBootBootstrap.class,args);
}
}
使用@SpringBootApplication开启自动装配,通过SpringApplication的静态run方法引导应用启动,引导类HelloSpringBootBootstrap作为主要配置源。
实现简单的打招呼功能,对每个来访客户,返回“Hello,{visitor}!Welcome to SpringBoot world!”问候语。先创建controller子包,然后创建HelloController类。
/**
* 打招呼业务
*
* 欢迎来访客户来到SpringBoot的世界
*/
@RestController
public class HelloController {
/**
* 欢迎来访的客户来到SpringBoot的世界
*
* @param visitor 来访客户名称
* @return 问候语
*/
@GetMapping("/hello/{visitor}")
public String hello(@PathVariable("visitor") String visitor){
return "Hello," + visitor + "! Welcome to SpringBoot world!";
}
}
在IDEA直接中运行引导类HelloSpringBootBootstrap,看到下面的输出表示成功启动
......
:: Spring Boot :: (v2.3.3.RELEASE)
......
2020-09-14 22:15:21.642 INFO 15340 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-09-14 22:15:21.642 INFO 15340 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-09-14 22:15:21.689 INFO 15340 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 47 ms
在浏览器访问地址http://localhost:8080/hello/Kim,看到浏览器返回的结果显示如下,说明应用成功运行,并正常提供服务了。
Hello,Kim! Welcome to SpringBoot world!
在IDEA中使用Maven工具的package打包hello-spring-boot。
Mode LastWriteTime Length Name
---- ------------- ------ ----
d----- 2020/9/14 22:14 classes
d----- 2020/9/14 22:14 generated-sources
d----- 2020/9/15 0:00 generated-test-sources
d----- 2020/9/15 0:00 maven-archiver
d----- 2020/9/15 0:00 maven-status
d----- 2020/9/15 0:00 test-classes
-a---- 2020/9/15 0:00 16520975 hello-spring-boot-1.0-SNAPSHOT.jar
-a---- 2020/9/15 0:00 4366 hello-spring-boot-1.0-SNAPSHOT.jar.original
package打包完成,在项目的target目录下生成了两个文件,一个是hello-spring-boot-1.0-SNAPSHOT.jar,一个是hello-spring-boot-1.0-SNAPSHOT.jar.original,前者大约16M,是接下来要用来单独部署的jar包;后者只有5K,不包含运行所需依赖的类包。
一般情况下,将可独立部署的jar包部署到服务器后,可以在控制台或shell脚本使用java -jar直接运行jar包
java -jar hello-spring-boot-1.0-SNAPSHOT.jar
执行命令后,可以看到和本地运行测试时一样的输出,然后在浏览器输入访问地址http://服务器IP:8080/hello/Kim,可以得到相同的结果,说明部署成功。
在Linux服务器上,也可以使用以下命令,&表示后台运行(不阻塞),nohub表示关闭终端也可以继续运行。
nohub java -jar hello-spring-boot-1.0-SNAPSHOT.jar &
以上的示例当中,没有任何配置文件,只要引入SpringBoot和相关的模块,启动时,自动会加载所需要的资源并提供缺省设置,应用便可以轻松的运行起来。
实际开发过程中,项目都会有自己的parent,可能无法使用spring-boot-starter-parent做为parent,此时可以使用spring-boot-dependencies代替。下面改造hello-spring-boot应用,使之更符合实际开发的中的场景。
主要的改造涉及三个部分,第一是添加项目代码的编译版本和项目文件编码,第二个就是添加spring-boot-dependencies依赖管理,第三个是配置spring-boot-maven-plugin打包插件的执行目标repackage。以下是改造后的pom.xml文件内容
4.0.0
com.fandou.coffee.learning
hello-spring-boot
1.0-SNAPSHOT
Hello,SpringBoot
SpringBoot开发入门示例
1.8
1.8
1.8
UTF-8
UTF-8
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-dependencies
2.3.3.RELEASE
pom
import
org.springframework.boot
spring-boot-maven-plugin
2.3.3.RELEASE
repackage
改造后,可以正常打包运行。在真实项目中,也可以把spring-boot-dependencies依赖管理等添加到parent项目中,这样就不需要所有子模块都另外配置。
在Java语言规范中,程序运行的入口是main方法,是程序运行的起点,运行jar文件时,实际上运行的入口也是main方法。
public static void main(String args[]) { ... }
main方法需要符合4个规则:修饰符为public和static、返回值为void、方法名为main、方法参数为字符串数组String[]。
java命令要运行一个jar文件形式的程序,只需在jar包中创建META-INF/MANIFEST.MF文件,并在MANIFEST.MF文件中设置Main-Class属性,指定程序入口main方法所在的类,告诉java命令(JVM)要运行含有main方法的具体类。这是Java对运行jar包的规范,只要jar包符合这样的规范,即java命令就可以运行该jar包程序。
下面以一个Hello,world!程序为例,查看打包后可运行的jar包是否符合以上规范。
用IDEA创建一个普通的Maven工程,不需要引入任何依赖,然后创建HellojarBootstrap类
package com.fandou.coffee.learning.hellojar.hello;
/**
* Hello,world!
*/
public class HellojarBootstrap {
/**
* 程序入口方法
*
* @param args 参数
*/
public static void main(String[] args) {
// 打印Hello,world!
System.out.println("Hello,world!");
}
}
接着在pom.xml文件中添加maven-jar-plugin插件,配置打包所需的MANIFEST.MF所需的信息,只需要配置mainClass即可
org.apache.maven.plugins
maven-jar-plugin
3.0.2
false
com.fandou.coffee.learning.hellojar.hello.HellojarBootstrap
package打包后,进入target目录,使用命令行运行java -jar hello-jar-1.0-SNAPSHOT.jar,可以看到正常输出
Hello,world!
解压hello-jar-1.0-SNAPSHOT.jar,看到只有两个文件,一个是HellojarBootstrap.class,一个是MANIFEST.MF
├─com
│ └─fandou
│ └─coffee
│ └─learning
│ └─hellojar
│ └─hello
│ HellojarBootstrap.class
│
└─META-INF
MANIFEST.MF
查看MANIFEST.MF文件
Manifest-Version: 1.0
Built-By: Coffee
Created-By: Apache Maven 3.6.2
Build-Jdk: 1.8.0_221
Main-Class: com.fandou.coffee.learning.hellojar.hello.HellojarBootstrap
可以看到,确实包含了Main-Class属性,当执行java命令-jar选项的时候,JVM加载jar包,然后运行Main-Class类的main方法,从而启动程序,打印输出了Hello,world!。这就是jar包的运行原理。
同样,java命令运行SpringBoot应用的jar包,也需要符合运行普通jar包的基本规范,就是通过运行jar包中的META-INF/MANIFEST.MF文件的Main-Class属性指定的入口类的(main方法)来实现的。解压hello-spring-boot-1.0-SNAPSHOT.jar文件后,其内部文件结构如下
├─BOOT-INF
│ │ classpath.idx
│ │
│ ├─classes
│ │ └─com
│ │ └─fandou
│ │ └─coffee
│ │ └─learning
│ │ └─springboot
│ │ └─hello
│ │ │ HelloSpringBootBootstrap.class
│ │ │
│ │ └─controller
│ │ HelloController.class
│ │
│ └─lib
│ jackson-annotations-2.11.2.jar
│ jackson-core-2.11.2.jar
│ jackson-databind-2.11.2.jar
│ jackson-datatype-jdk8-2.11.2.jar
│ jackson-datatype-jsr310-2.11.2.jar
│ jackson-module-parameter-names-2.11.2.jar
│ jakarta.annotation-api-1.3.5.jar
│ jakarta.el-3.0.3.jar
│ jul-to-slf4j-1.7.30.jar
│ log4j-api-2.13.3.jar
│ log4j-to-slf4j-2.13.3.jar
│ logback-classic-1.2.3.jar
│ logback-core-1.2.3.jar
│ slf4j-api-1.7.30.jar
│ snakeyaml-1.26.jar
│ spring-aop-5.2.8.RELEASE.jar
│ spring-beans-5.2.8.RELEASE.jar
│ spring-boot-2.3.3.RELEASE.jar
│ spring-boot-autoconfigure-2.3.3.RELEASE.jar
│ spring-boot-starter-2.3.3.RELEASE.jar
│ spring-boot-starter-json-2.3.3.RELEASE.jar
│ spring-boot-starter-logging-2.3.3.RELEASE.jar
│ spring-boot-starter-tomcat-2.3.3.RELEASE.jar
│ spring-boot-starter-web-2.3.3.RELEASE.jar
│ spring-context-5.2.8.RELEASE.jar
│ spring-core-5.2.8.RELEASE.jar
│ spring-expression-5.2.8.RELEASE.jar
│ spring-jcl-5.2.8.RELEASE.jar
│ spring-web-5.2.8.RELEASE.jar
│ spring-webmvc-5.2.8.RELEASE.jar
│ tomcat-embed-core-9.0.37.jar
│ tomcat-embed-websocket-9.0.37.jar
│
├─META-INF
│ │ MANIFEST.MF
│ │
│ └─maven
│ └─com.fandou.coffee.learning
│ └─hello-spring-boot
│ pom.properties
│ pom.xml
│
└─org
└─springframework
└─boot
└─loader
│ ClassPathIndexFile.class
│ ExecutableArchiveLauncher.class
│ JarLauncher.class
│ LaunchedURLClassLoader$DefinePackageCallType.class
│ LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
│ LaunchedURLClassLoader.class
│ Launcher.class
│ MainMethodRunner.class
│ PropertiesLauncher$1.class
│ PropertiesLauncher$ArchiveEntryFilter.class
│ PropertiesLauncher$ClassPathArchives.class
│ PropertiesLauncher$PrefixMatchingArchiveFilter.class
│ PropertiesLauncher.class
│ WarLauncher.class
│
├─archive
│ Archive$Entry.class
│ Archive$EntryFilter.class
│ Archive.class
│ ExplodedArchive$AbstractIterator.class
│ ExplodedArchive$ArchiveIterator.class
│ ExplodedArchive$EntryIterator.class
│ ExplodedArchive$FileEntry.class
│ ExplodedArchive$SimpleJarFileArchive.class
│ ExplodedArchive.class
│ JarFileArchive$AbstractIterator.class
│ JarFileArchive$EntryIterator.class
│ JarFileArchive$JarFileEntry.class
│ JarFileArchive$NestedArchiveIterator.class
│ JarFileArchive.class
│
├─data
│ RandomAccessData.class
│ RandomAccessDataFile$1.class
│ RandomAccessDataFile$DataInputStream.class
│ RandomAccessDataFile$FileAccess.class
│ RandomAccessDataFile.class
│
├─jar
│ AsciiBytes.class
│ Bytes.class
│ CentralDirectoryEndRecord$1.class
│ CentralDirectoryEndRecord$Zip64End.class
│ CentralDirectoryEndRecord$Zip64Locator.class
│ CentralDirectoryEndRecord.class
│ CentralDirectoryFileHeader.class
│ CentralDirectoryParser.class
│ CentralDirectoryVisitor.class
│ FileHeader.class
│ Handler.class
│ JarEntry.class
│ JarEntryFilter.class
│ JarFile$1.class
│ JarFile$JarEntryEnumeration.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
│
├─jarmode
│ JarMode.class
│ JarModeLauncher.class
│ TestJarMode.class
│
└─util
SystemPropertyUtils.class
与平常的jar包相比,SpringBoot应用jar包的一级目录下多了一个BOOT-INF目录,该目录下存放的应用的类文件以及依赖库;同时原本预期根目录下的应用中的类包(com.*)变成了SpringBoot的loader模块的类包目录(org.*)。
查看META-INF/MANIFEST.MF文件,其Main-Class指向的也并不是应用中的引导类HelloSpringBootBootstrap,而是SpringBoot的loader模块下的org.springframework.boot.loader.JarLauncher类,同时发现另一个属性Start-Class指向了应用的引导类。
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Archiver-Version: Plexus Archiver
Built-By: Coffee
Start-Class: com.fandou.coffee.learning.springboot.hello.HelloSpringBo
otBootstrap
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.3.3.RELEASE
Created-By: Apache Maven 3.6.2
Build-Jdk: 1.8.0_221
Main-Class: org.springframework.boot.loader.JarLauncher
在项目中引入spring-boot-loader模块
org.springframework.boot
spring-boot-loader
provided
查看JarLauncher类的源码
public class JarLauncher extends ExecutableArchiveLauncher {
// SpringBoot应用编译后的类在当前jar包中的目录
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
// SpringBoot应用依赖库在当前jar中的目录
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
// 判断给定的归档文件是否为BOOT-INF/classes/目录,或BOOT-INF/lib/下的jar文件
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
// 如果是目录
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
// 如果不是目录
return entry.getName().startsWith(BOOT_INF_LIB);
}
// java -jar 命令执行的入口
public static void main(String[] args) throws Exception {
// 调用了父类的launch方法
// 当new方式创建JarLauncher对象时,父类构造方法同时被调用,会加载当前jar包
new JarLauncher().launch(args);
}
}
查看父类Launcher的launch方法
public abstract class Launcher {
/**
* 加载SpringBoot应用。此方法由子类即JarLauncher或WarLauncher的main方法调用
* @param args 命令行参数
* @throws 应用加载失败抛出异常
*/
protected void launch(String[] args) throws Exception {
// 注册jar文件的url协议处理器,用于读取加载SpringBoot应用依赖的类
JarFile.registerUrlProtocolHandler();
// 创建SpringBoot应用类加载器:指定了需要加载的类文件列表
// 返回的是SpringBoot自定义的一个类加载器LaunchedURLClassLoader的实例,
// LaunchedURLClassLoader重写了loadClass等方法以实现加载SpringBoot应用的类或依赖,
// 即BOOT-INF/classes/和BOOT-INF/lib/中的类和依赖包
ClassLoader classLoader = createClassLoader(getClassPathArchives());
// 传递命令行参数,以及真正的入口程序类即Start-Class属性指定的HelloSpringBootBootstrap,
// 并设置当前线程使用自定义的类加载器加载类和依赖库,
// 然后通过反射,运行HelloSpringBootBootstrap类的main方法,从而启动SpringBoot应用
launch(args, getMainClass(), classLoader);
}
...
}
下面是运行引导类main方法的代码,其最终是通过运行MainMethodRunner实例的run方法完成。
/**
* main方法运行器,通过反射调用指定类的main方法
*/
public class MainMethodRunner {
// 即上面通过getMainClass()方法获取的引导类即HelloSpringBootBootstrap的名称
private final String mainClassName;
// 命令行参数
private final String[] args;
/**
* 创建一个main方法运行器实例对象
* @param mainClass 含有main方法的类
* @param args 命令行参数
*/
public MainMethodRunner(String mainClass, String[] args) {
this.mainClassName = mainClass;
this.args = (args != null) ? args.clone() : null;
}
/**
* 通过反射调用指定类的main方法
*/
public void run() throws Exception {
// 获取当前线程中的自定义类加载器实例加载指定的类
Class> mainClass = Thread.currentThread().getContextClassLoader()
.loadClass(this.mainClassName);
// 通过反射获取main方法并调用
Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
// 即调用了引导类HelloSpringBootBootstrap的main方法,从而启动SpringBoot应用
mainMethod.invoke(null, new Object[] { this.args });
}
}
到此,基本上可以了解SpringBoot应用独立运行部署的核心原理,本质上它依然遵循jar包的规范,然后通过自定义类加载器修改了加载规则来启动SpringBoot应用。
首先,通过自定义打包插件,修改jar包中的Main-Class属性,在真正启动SpringBoot应用前先启动自定义的加载类JarLauncher或WarLauncher类;
接着创建自定义类加载器实例,用来加载SpringBoot应用的引导类以及依赖的类库;
然后运行SpringBoot应用的引导类的main方法,从而启动SpringBoot应用的。
了解SpringBoot应用的生命周期和事件,可参考SpringBoot生命周期