Spring-Boot启动之前做了哪些事?

Spring Boot Jar文件探究

初始化一个Spring 应用,添加如下依赖

创新互联公司专注于企业全网营销推广、网站重做改版、东风网站定制设计、自适应品牌网站建设、H5页面制作成都商城网站开发、集团公司官网建设、外贸营销网站建设、高端网站制作、响应式网页设计等建站业务,价格优惠性价比高,为东风等各大城市提供网站开发制作服务。



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.1.6.RELEASE
         
    
    com.fxipp.spring
    first-app-by-gui
    0.0.1-SNAPSHOT
    first-app-by-gui
    Demo project for Spring Boot

    
        1.8
    

    
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

执行mvn package命令打包,查看jar包的目录结构

.
├── BOOT-INF
│   ├── classes
│   │   ├── application.properties
│   │   └── com
│   │       └── fxipp
│   │           └── spring
│   │               └── FirstAppByGuiApplication.class
│   └── lib
│       ├── classmate-1.4.0.jar
│       ├── hibernate-validator-6.0.17.Final.jar
│       ├── jackson-annotations-2.9.0.jar
│       ├── jackson-core-2.9.9.jar
│       ......
├── META-INF
│   ├── MANIFEST.MF
│   └── maven
│       └── com.fxipp.spring
│           └── first-app-by-gui
│               ├── pom.properties
│               └── pom.xml
└── org
    └── springframework
        └── boot
            └── loader
                ├── ExecutableArchiveLauncher.class
                ├── JarLauncher.class
                ├── LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
                ├── LaunchedURLClassLoader.class
                ├── ......
                ├── archive
                │   ├── Archive$Entry.class
                │   ├── Archive$EntryFilter.class
                │   ├── Archive.class
                │   ├── ......
                ├── data
                │   ├── RandomAccessData.class
                │   ├── RandomAccessDataFile$1.class
                │   ├──......
                ├── jar
                │   ├── AsciiBytes.class
                │   ├── Bytes.class
                │   ├── ......
                └── util
                    └── SystemPropertyUtils.class
18 directories, 91 files

文件结构比较复杂,解释一下

  • BOOT-INF/classes: 存放应用编译后的class文件;
  • BOOT-INF/lib:class path目录, 存放应用依赖的jar包;
  • META-INF: 存放应用的元信息,如MANIFEST.MF文件;
  • org:存放Spring Boot自身的class文件;

Jar文件的执行器: Spring Boot Loader

我们先从MANIFEST.MF文件查看

Manifest-Version: 1.0
Implementation-Title: first-app-by-gui
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.fxipp.spring.FirstAppByGuiApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.6.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher

里面记录了应用的元信息,Spring的版本,应用的版本,Maven的版本,Main-Class等信息。不难发现,MainClass指向的是org.springframework.boot.loader.JarLauncher(以下简称JarLauncher),而不是我们自己编写的com.fxipp.spring.FirstAppByGuiApplication

JarLauncher从名字看出是一个jar的执行器,他的class文件位于org.springframework.boot.loader目录下,可见它是Spring自身的class文件。

JarLauncher的GAV org.springframework.boot:spring-boot-loader:2.1.6.RELEASE

通常情况下,他会在spring-boot-starter-parent引入到应用中,既然main-class指向到是JarLauncher,那我们也可以直接执行java org.springframework.boot.loader.JarLauncher,也可以启动Spring项目的。

java org.springframework.boot.loader.JarLauncher

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.6.RELEASE)

2019-06-19 20:30:52.202  INFO 3094 --- [           main] c.fxipp.spring.FirstAppByGuiApplication  : Starting FirstAppByGuiApplication on fangxideMacBook-Pro.local with PID 3094 (/Users/fangxi/Java/workspace/default/spring-boot/first-app-by-gui/target/temp/BOOT-INF/classes started by fangxi in /Users/fangxi/Java/workspace/default/spring-boot/first-app-by-gui/target/temp)

既然可以执行,那就说明了,JarLauncher这个类才是Spring项目真正的入口。如果我们执行自己写的com.fxipp.spring.FirstAppByGuiApplication会怎么样?

➜  classes java com.fxipp.spring.FirstAppByGuiApplication
Exception in thread "main" java.lang.NoClassDefFoundError: org/springframework/boot/SpringApplication
    at com.fxipp.spring.FirstAppByGuiApplication.main(FirstAppByGuiApplication.java:10)
Caused by: java.lang.ClassNotFoundException: org.springframework.boot.SpringApplication
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 1 more

启动报错,原因是找不到org.springframework.boot.SpringApplication这个类,说白了就是没有指定Class Path,Spring Boot应用的Class Path目录是BOOT-INF/lib

也就是说,JarLauncher可以执行成功,是因为Spring Boot知道了Class Path的路径,说明JarLauncher在启动调用com.fxipp.spring.FirstAppByGuiApplication之前,指定了Class Path的位置。

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);
   }

}
  • Archive.Entry:这个类对对象,代编jar包中的资源文件。

isNestedArchive方法判断entry对象是不是位于jar包内,如果在jar内部,返回true。如果不在jar包里面,也就是我们解压了jar包,返回false。

重点看launch(String[])方法

    protected void launch(String[] args) throws Exception {
    // 1
        JarFile.registerUrlProtocolHandler();
    // 2
        ClassLoader classLoader = createClassLoader(getClassPathArchives());
    // 3
        launch(args, getMainClass(), classLoader);
    }

这个方法一共3步

  1. 扩展JAR协议
    1. JDK默认支持file、http、jar等协议,所以JDK内部有默认的实现,位于sun.net.www.protocol包下。
    2. JarFile.registerUrlProtocolHandler();这个方法将org.springframework.boot.loader包下对应的JAR协议实现,覆盖原有的JAR实现。
    3. 因为原有的JAR实现,ClassPath是我们自己配置环境变量的时候制定的,不是BOOT-INF/lib
  2. 创建一个classloader,用于加载JarLauncher类,因为jar包可能会被解压,解压前和解压后的的ClassLoader是不同的。
  3. 调用launch方法,将参数传递。
    1. args是我们自己指定的参数。
    2. getMainClass()是获取MANIFEST.MF文件里面Statr-Class属性,也就是获取我们自定义主类的Class 文件地址。
    3. 传递推出的类加载器

launch方法

protected void launch(String[] args, String mainClass, ClassLoader classLoader) throws Exception {
   Thread.currentThread().setContextClassLoader(classLoader);
   createMainMethodRunner(mainClass, args, classLoader).run();
}

protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args, ClassLoader classLoader) {
   return new MainMethodRunner(mainClass, args);
}
public class MainMethodRunner {

   private final String mainClassName;

   private final String[] args;

   public MainMethodRunner(String mainClass, String[] args) {
      this.mainClassName = mainClass;
      this.args = (args != null) ? args.clone() : null;
   }

   public void run() throws Exception {
      Class mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
      Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
      mainMethod.invoke(null, new Object[] { this.args });
   }

}

launch方法分析:

  1. 将ClassLoader放入当前线程里面的ClassLoader里面
  2. 创建MainMethodRunner对象,调用里面的run()方法。
    1. run()方法先获取到之前设定的ClassLoader。
    2. 利用ClassLoader加载Start-Class之类的类,也就是我们自己的主类。
    3. 获取主类里面的main方法,通过反射执行。

总结

通过分析,我们可以看出,Spring Boot Loader在调用我们自己的主类之前,主要做了三件事

  1. 扩展JDK默认的支持JAR对应的协议,因为Spring Boot启动不仅仅需要JDK半身的JAR文件,还需要BOOT-INF/lib这个目录下的文件。默认实现无法将BOOT-INF/lib这个目录当作ClassPath,故需要替换实现。
  2. 判断当前的介质,是java -jar启动,还是java org.springframework.boot.loader.JarLauncher启动。以便获取对应的ClassLoader。
  3. 获取MANIFEST.MF文件中的Start-Class属性,也就是我们自定义的主类。通过第二步获取的ClassLoader加载获取到Class文件,通过反射调用main方法,启动应用。

新闻标题:Spring-Boot启动之前做了哪些事?
本文链接:http://pwwzsj.com/article/ieshjh.html