SpringBoot项目的启动
当我们在IDE中新建(或导入)了一个SpringBoot项目之后,我们如果想要启动这个SpringBoot项目,我们可以找到相应的带有@SpringBootApplication注解的启动类,该启动类是一个带有main方法的类,这个类就是SpringBoot项目的入口。所以想要运行的话,只需要在IDE对这个类点击Run As Java Application既可以启动。
而当我们需要将写好的代码发布到相应的linux服务器上时。我们一般都是需要将我们的SpringBoot代码打成Jar包(即使是使用ops等自动化运维工具,其本质也是从git拉取到代码,然后mvn clean install打包代码进行发布),然后使用shell脚本执行jar包。我们打开所有的运行SPringBoot项目jar包的shell脚本,你都会发现在start的时候会执行:
java -jar ****/SpringBoot.jar
实际上这句话最后的效果跟在IDE里对启动类点击Run As Java Application是一样,都是为了执行启动类中的main方法,从而完成SpringBoot初始化过程。
那么直接点击启动的方式当然好理解,但是你有想过为什么执行了java -jar命令后就能够执行启动类的main方法吗?
接下来,我们就先来探究这个问题。
SpringBoot jar的启动原理分析
- 基础知识准备
0.java -jar命令到底做了什么
在jar包中,我们会在目录META-INF里发现一个叫做MANIFEST.MF的文件,在该文件中,有一个叫Main-Class的特殊条目。
而java -jar命令就是用来执行这个Main-Class指定的类。
- 解析SpringBoot jar的结构
我们在对SpringBoot项目进行打包的时候,我们会执行Maven的mvn clean install命令,打完包后,你会在target目录上发现如下几个文件:
xxxx.jar
xxx.jar.original
xxx-sources.jar
最后一个文件很好理解,就是这个项目的源码jar。而第一个文件是在spring boot插件的机制下,将一个普通的jar打成了一个可以执行的jar包,而xxx.jar.original则是maven打出的jar包,
jar包结构
我们接着来看下xxx.jar这个spring boot插件作用下打出来的jar包的结构,解压后,结构如下:
.
├── BOOT-INF
│ ├── classes
│ │ ├── application-dev.properties
│ │ ├── application-prod.properties
│ │ ├── application.properties
│ │ ├── com
│ │ │ └── weibangong
│ │ │ └── open
│ │ │ └── openapi
│ │ │ ├── SpringBootWebApplication.class
│ │ │ ├── config
│ │ │ │ ├── ProxyServletConfiguration.class
│ │ │ │ └── SwaggerConfig.class
│ │ │ ├── oauth2
│ │ │ │ ├── controller
│ │ │ │ │ ├── AccessTokenController.class
│ │ ├── logback-spring.xml
│ │ └── static
│ │ ├── css
│ │ │ └── guru.css
│ │ ├── images
│ │ │ ├── FBcover1200x628.png
│ │ │ └── NewBannerBOOTS_2.png
│ └── lib
│ ├── accessors-smart-1.1.jar
├── META-INF
│ ├── MANIFEST.MF
│ └── maven
│ └── com.weibangong.open
│ └── open-server-openapi
│ ├── pom.properties
│ └── pom.xml
└── org
└── springframework
└── boot
└── loader
├── ExecutableArchiveLauncher$1.class
├── ExecutableArchiveLauncher.class
├── JarLauncher.class
├── LaunchedURLClassLoader$1.class
├── LaunchedURLClassLoader.class
├── Launcher.class
├── archive
│ ├── Archive$Entry.class
│ ├── Archive$EntryFilter.class
│ ├── Archive.class
│ ├── ExplodedArchive$1.class
│ ├── ExplodedArchive$FileEntry.class
│ ├── ExplodedArchive$FileEntryIterator$EntryComparator.class
├── ExplodedArchive$FileEntryIterator.class
这个jar除了我们写的应用程序打出的class以外还有一个单独的org包,应该是spring boot应用在打包的使用spring boot插件将这个package打进来,也就是增强了mvn生命周期中的package阶段,而正是这个包在启动过程中起到了关键的作用,另外把jar中将应用所需的各种依赖都打进来,并且打入了spring boot额外的package,这种可以all-in-one的jar也被称之为fat.jar,下文我们将一直以fat.jar来代替打出的jar的名字。
MANIFEST.MF文件
之前在提到java -jar命令的时候已经提到了这个文件了,那我们现在赶紧来看看Main-class是哪个,会是SpringBoot的启动类吗,如果直接就是SpringBoot启动类的话,那一切就解决了。
Manifest-Version: 1.0
Implementation-Title: iyourcar-service-game-carshow
Implementation-Version: 1.1.1
Archiver-Version: Plexus Archiver
Built-By: cc
Implementation-Vendor-Id: com.iyourcar
Spring-Boot-Version: 1.5.10.RELEASE
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.PropertiesLauncher
Start-Class: com.iyourcar.App
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.3.9
Build-Jdk: 1.8.0_144
Implementation-URL: http://projects.spring.io/spring-boot/iyourcar-par
ent-app/iyourcar-service-game-carshow/
我们可以看到Main-Class不是SpringBoot启动类,但是Start-Class是,那么我们就可以猜想,在执行Main-Class: org.springframework.boot.loader.PropertiesLauncher的main方法的过程中,会去执行Start-Class:com.iyourcar.App。这样就可以进行SpringBoot项目启动过程。
到底是不是这样呢,我们分析源码
- 启动原理分析
从上面的分析中,我们已经得到了java -jar命令执行的类是org.springframework.boot.loader.PropertiesLauncher
我们来看看这个类的main方法。
public static void main(String[] args) throws Exception {
PropertiesLauncher launcher = new PropertiesLauncher();
args = launcher.getArgs(args);
launcher.launch(args);
}
launcher方法
这个方法在父类Launcher中,找到父类方法launch(String[] args)方法
protected void launch(String[] args)
throws Exception
{
JarFile.registerUrlProtocolHandler();
//找到自定义类加载器
ClassLoader classLoader = createClassLoader(getClassPathArchives());
//
launch(args, getMainClass(), classLoader);
}
launch(args, getMainClass(), classLoader);
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
throws Exception
{
//设置当前线程的ClassLoader为SpringBoot插件中指定的自定义ClassLoader
Thread.currentThread().setContextClassLoader(classLoader);
createMainMethodRunner(mainClass, args, classLoader).run();
}
我们查看源码可以发现,这个mainClass就是MANIFEST.MF文件中Start-Class的值。
getMainClass()
该方法是Launcher类中的一个抽象方法,实际实现在PropertiesLauncher类中。
protected String getMainClass()
throws Exception
{
String mainClass = getProperty("loader.main", "Start-Class");
if (mainClass == null) {
throw new IllegalStateException("No 'loader.main' or 'Start-Class' specified");
}
return mainClass;
}
private String getProperty(String propertyKey, String manifestKey)
throws Exception
{
return getProperty(propertyKey, manifestKey, null);
}
private String getProperty(String propertyKey, String manifestKey, String defaultValue)
throws Exception
{
if (manifestKey == null)
{
manifestKey = propertyKey.replace('.', '-');
manifestKey = toCamelCase(manifestKey);
}
String property = SystemPropertyUtils.getProperty(propertyKey);
if (property != null)
{
String value = SystemPropertyUtils.resolvePlaceholders(this.properties, property);
debug("Property '" + propertyKey + "' from environment: " + value);
return value;
}
if (this.properties.containsKey(propertyKey))
{
String value = SystemPropertyUtils.resolvePlaceholders(this.properties, this.properties
.getProperty(propertyKey));
debug("Property '" + propertyKey + "' from properties: " + value);
return value;
}
try
{
if (this.home != null)
{
Manifest manifest = new ExplodedArchive(this.home, false).getManifest();
if (manifest != null)
{
String value = manifest.getMainAttributes().getValue(manifestKey);
if (value != null)
{
debug("Property '" + manifestKey + "' from home directory manifest: " + value);
return SystemPropertyUtils.resolvePlaceholders(this.properties, value);
}
}
}
}
catch (IllegalStateException localIllegalStateException) {}
Manifest manifest = createArchive().getManifest();
if (manifest != null)
{
String value = manifest.getMainAttributes().getValue(manifestKey);
if (value != null)
{
debug("Property '" + manifestKey + "' from archive manifest: " + value);
return SystemPropertyUtils.resolvePlaceholders(this.properties, value);
}
}
return defaultValue == null ? defaultValue :
SystemPropertyUtils.resolvePlaceholders(this.properties, defaultValue);
}
我们接着回到launch(args, getMainClass(), classLoader);方法,它最终调用了createMainMethodRunner方法,后者实例化了MainMethodRunner对象并运行了run方法,我们转到MainMethodRunner源码中。
public class MainMethodRunner {
private final String mainClassName;
private final String[] args;
public MainMethodRunner(String mainClass, String[] args) {
this.mainClassName = mainClass;
this.args = args == null?null:(String[])args.clone();
}
public void run() throws Exception {
//这里将之前存入到当前线程的自定义ClassLoader取出来去加载com.iyourcar.App这个类
Class mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
Method mainMethod = mainClass.getDeclaredMethod("main", new Class[]{String[].class});
mainMethod.invoke((Object)null, new Object[]{this.args});
}
}
我们查看run方法,很明显的看到执行了Start-Class:com.iyourcar.App的main方法,到这里,SpringBoot jar启动的过程其实就分析完了。
SrpingBoot jar启动过程中的自定义ClassLoader
在之前的分析中,我们已经知道了SpringBoot项目入口启动类是由SpringBoot插件中的自定义ClassLoader进行类加载的。
//找到自定义类加载器
ClassLoader classLoader = createClassLoader(getClassPathArchives());
这行代码作为入口分析
protected ClassLoader createClassLoader(List<Archive> archives)
throws Exception
{
//将archives的url都添加到自定义ClassLoader中,我们要知道archives中都包含哪些URL
List<URL> urls = new ArrayList(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
return createClassLoader((URL[])urls.toArray(new URL[urls.size()]));
}
protected ClassLoader createClassLoader(URL[] urls)
throws Exception
{
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
先找到parent Archive
protected final Archive createArchive()
throws Exception
{
//获取JAR包所在路径
ProtectionDomain protectionDomain = getClass().getProtectionDomain();
CodeSource codeSource = protectionDomain.getCodeSource();
URI location = codeSource == null ? null : codeSource.getLocation().toURI();
String path = location == null ? null : location.getSchemeSpecificPart();
if (path == null) {
throw new IllegalStateException("Unable to determine code source archive");
}
File root = new File(path);
if (!root.exists()) {
throw new IllegalStateException("Unable to determine code source archive from " + root);
}
return root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root);
}
}
这段代码是通过得到JAR包(或者Java程序)所在路径,接着用路径来封装一个Archvie。最后,我们得到的是new ExplodedArchive(root);
protected List<Archive> getClassPathArchives()
throws Exception
{
//paths 当没有设置loader.path这个系统属性的时候,是一个空的List
List<Archive> lib = new ArrayList();
//paths是一个空的list
for (String path : this.paths) {
for (Archive archive : getClassPathArchives(path)) {
if ((archive instanceof ExplodedArchive))
{
List<Archive> nested = new ArrayList(archive.getNestedArchives(new ArchiveEntryFilter(null)));
nested.add(0, archive);
lib.addAll(nested);
}
else
{
lib.add(archive);
}
}
}
//到这里都是一个空的lib,实际上就是addNestedEntries这个方法添加了URL
addNestedEntries(lib);
return lib;
}
private void addNestedEntries(List<Archive> lib)
{
try
{
lib.addAll(this.parent.getNestedArchives(new Archive.EntryFilter()
{
public boolean matches(Archive.Entry entry)
{
if (entry.isDirectory()) {
return entry.getName().equals("BOOT-INF/classes/");
}
return entry.getName().startsWith("BOOT-INF/lib/");
}
}));
}
catch (IOException localIOException) {}
}
上述代码其实就是程序帮我们找到需要使用自定义ClassLoader加载的URL。
当我们没有设置loader.path这个系统属性的时候,我们拿到的paths其实是空的。
这种情况下,实际上注入URL只有addNestedEntries方法,结合ExplodedArchive源码,可以分析得到,添加的URL,包括BOOT-INF/classes下的所有文件(真正有效的是classes下的class文件),以及BOOT-INF/lib下的所有jar文件。
SpringBoot jar启动过程中的自定义ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
Handler.setUseFastConnectionExceptions(true);
try
{
try
{
definePackageIfNecessary(name);
}
catch (IllegalArgumentException ex)
{
if (getPackage(name) == null) {
throw new AssertionError("Package " + name + " has already been defined but it could not be found");
}
}
return super.loadClass(name, resolve);
}
finally
{
Handler.setUseFastConnectionExceptions(false);
}
}
private void definePackageIfNecessary(String className)
{
int lastDot = className.lastIndexOf('.');
if (lastDot >= 0)
{
String packageName = className.substring(0, lastDot);
if (getPackage(packageName) == null) {
try
{
definePackage(className, packageName);
}
catch (IllegalArgumentException ex)
{
if (getPackage(packageName) == null) {
throw new AssertionError("Package " + packageName + " has already been defined but it could not be found");
}
}
}
}
}
private void definePackage(final String className, final String packageName)
{
try
{
AccessController.doPrivileged(new PrivilegedExceptionAction()
{
public Object run()
throws ClassNotFoundException
{
String packageEntryName = packageName.replace('.', '/') + "/";
String classEntryName = className.replace('.', '/') + ".class";
for (URL url : LaunchedURLClassLoader.this.getURLs()) {
try
{
URLConnection connection = url.openConnection();
if ((connection instanceof JarURLConnection))
{
java.util.jar.JarFile jarFile = ((JarURLConnection)connection).getJarFile();
if ((jarFile.getEntry(classEntryName) != null) &&
(jarFile.getEntry(packageEntryName) != null) &&
(jarFile.getManifest() != null))
{
LaunchedURLClassLoader.this.definePackage(packageName, jarFile.getManifest(), url);
return null;
}
}
}
catch (IOException localIOException) {}
}
return null;
}
}, AccessController.getContext());
}
catch (PrivilegedActionException localPrivilegedActionException) {}
}
这一部分其实就是springboot扩展URLClassLoader实现嵌套jar加载,这部分需要找个时间另写一篇文章来梳理,内容还是挺多的。
在看这部分的时候,我一直带着一个疑问,BOOT-INF/classes下面的文件都是class文件,应该是可以使用AppClassLoader(java 命令 加了-jar 参数以后,AppClassloader就只关注xxx.jar范围内的class了)这个类加载器直接加载的,那为什么需要使用自定义加载器来加载SpringBoot项目的启动入口类呢?
其实答案是很简单的,只是我当时钻牛角尖了。因为启动了这个入口类之后,我们后续是会这个入口类去加载Spring等BooT-INF/lib下面的jar包里的class的,而要加载这些类,我们是必须要用到这个自定义ClassLoader才可以对这种jar-in-jar格式里的class进行类加载的。所以也就导致了入口类是必须使用这个ClassLoader来进行类加载的,否则将无法加载程序中使用到的jar包里的类,肯定会导致报错。
(如果类A中引用了类B,Java虚拟机将使用加载类A的类加载器来加载类B 。这些jar包是需要自定义加载器去处理的,就算没有使用jar-in-jar的结构,这些jar包也是需要使用自定义加载器去处理,因为这些jar包里的资源,AppClassLoader以及其祖先类加载器(ExtClassLoader ,BootStrapClassLoader )都是无法加载的(虽然说是祖先类加载器,但是并不是继承关系,只是通过parent属性注入关联了这个类加载树))。
另,在查看Jar包中的Manifest.MF文件的时候。我发现公司的两个项目的Mian-Class条目对应的值是不一样的。
project1
Main-Class: org.springframework.boot.loader.PropertiesLauncher
project2
Main-Class: org.springframework.boot.loader.JarLauncher
这就引起了我的疑问,为什么会不一样呢?
先给出导致这个结果的原因。
之所以我们打出的Jar包会和之前的Jar包的结构不一样,是因为打包的时候使用了springboot插件导致的。springboot插件增强了maven package(只对maven打包工具而言)。而这个springboot插件的设置其实就在pom文件中。
基本配置如下:
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
插件是可以在parent pom.xml中的<pluginManagement>节点进行统一配置的,我们公司也是如此。
而造成两个项目的Main-Class不一样的原因就是两个项目使用的parent pom文件的版本不一样。
project1中的parent pom.xml
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>${start-class}</mainClass>
<layout>ZIP</layout>
<addResources>true</addResources>
<executable>true</executable>
<fork>true</fork>
</configuration>
</plugin>
</plugins>
project2 的parent pom.xml
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
你会发现,project1中配置了<layout>ZIP</layout>
正是因为这个配置,更改了Main-Class的值为org.springframework.boot.loader.PropertiesLauncher。
那到底PropertiesLauncher和JarLauncher有什么区别呢?为什么要有这两种不同的配置呢?
通过源码分析,我们会发现,PropertiesLauncher可以算是JarLauncher的一种扩展,是一种功能上的增强(这两个都用来启动springboot项目)。PropertiesLauncher允许通过配置loader.path使得jar包可以去加载外部的jar。而JarLauncher只能处理jar-in-jar这种模式。通常来说使用JarLauncher作为Main-Class被称为fat-jar,而PropertiesLauncher则可以将fat-jar变成thin-jar,之所以可以这样做,正是应为PropertiesLauncher使得jar包可以去加载外部的jar。
当Java虚拟机要加载第一个类的时候,加载过程:
(1). 首先当前线程的类加载器去加载线程中的第一个类(当前线程的类加载器:Thread类中有一个get/setContextClassLoader(ClassLoader cl);方法,可以获取/指定本线程中的类加载器)
(2). 如果类A中引用了类B,Java虚拟机将使用加载类A的类加载器来加载类B 。
(3). 还可以直接调用ClassLoader.loadClass(String className)方法来指定某个类加载器去加载某个类
每个类加载器加载类时,又先委托给其上级类加载器当所有祖宗类加载器没有加载到类,回到发起者类加载器,还加载不了,则会抛出ClassNotFoundException,不是再去找发起者类加载器的儿子,因为没有getChild()方法。