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()方法。