美文网首页SpringBoot系列
1) Springboot打包方式启动分析-自定义类加载器

1) Springboot打包方式启动分析-自定义类加载器

作者: 涣涣虚心0215 | 来源:发表于2021-02-20 12:36 被阅读0次

在正常的开发过程中,我们只需要提供一个包含了main()方法的类,并带上@SpringBootApplication注解,那么这个类就是Springboot的启动类。

@SpringBootApplication
public class ExceptionApplication {
    public static void main(String[] args) {
        SpringApplication.run(ExceptionApplication.class, args);
    }
}

按照这种方式启动,那么这些类都是通过应用类加载器加载的,但是Springboot应用发布的时候都是打包成jar然后通过Java命令启动jar,在这种方式下情况就不一样了。

Jar目录结构

├───BOOT-INF
│   ├───classes
│   │   │   application.properties
│   │   │
│   │   └───com
│   │       └───gpcoding
│   │           │   ExceptionApplication.class
│   │           │
│   │           ├───config
│   │           │       WebMvcConfig.class
│   │           │
│   │           ├───controller
│   │           │       UserController.class
│   │           │
│   │           ├───listener
│   │           │       CustomerInterceptor.class
│   │           │
│   │           └───util
│   │                   GlobleExceptionHandler.class
│   │                   WebLogAspect.class
│   │
│   └───lib
│           .......(省略)
│           spring-aop-5.1.2.RELEASE.jar
│           spring-beans-5.1.2.RELEASE.jar
│           spring-boot-2.1.0.RELEASE.jar
│           spring-boot-actuator-2.1.0.RELEASE.jar
│           spring-boot-actuator-autoconfigure-2.1.0.RELEASE.jar
│           spring-boot-autoconfigure-2.1.0.RELEASE.jar
│           spring-boot-starter-2.1.0.RELEASE.jar
│           spring-boot-starter-actuator-2.1.0.RELEASE.jar
│           spring-boot-starter-aop-2.1.0.RELEASE.jar
│           spring-boot-starter-json-2.1.0.RELEASE.jar
│           spring-boot-starter-logging-2.1.0.RELEASE.jar
│           spring-boot-starter-tomcat-2.1.0.RELEASE.jar
│           spring-boot-starter-web-2.1.0.RELEASE.jar
│           spring-context-5.1.2.RELEASE.jar
│           spring-core-5.1.2.RELEASE.jar
│           spring-expression-5.1.2.RELEASE.jar
│           spring-jcl-5.1.2.RELEASE.jar
│           spring-security-core-5.1.1.RELEASE.jar
│           spring-security-web-5.1.1.RELEASE.jar
│           spring-web-5.1.2.RELEASE.jar
│           spring-webmvc-5.1.2.RELEASE.jar
│           tomcat-embed-core-9.0.12.jar
│           tomcat-embed-el-9.0.12.jar
│           tomcat-embed-websocket-9.0.12.jar
│           validation-api-2.0.1.Final.jar
│
├───META-INF
│   │   MANIFEST.MF
│   │
│   └───maven
│       └───com.gpcoding
│           └───spring-boot-exception
│                   pom.properties
│                   pom.xml
│
└───org
    └───springframework
        └───boot
            └───loader
                │   ExecutableArchiveLauncher.class
                │   JarLauncher.class
                │   LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
                │   LaunchedURLClassLoader.class
                │   Launcher.class
                │   MainMethodRunner.class
                │   PropertiesLauncher$1.class
                │   PropertiesLauncher$ArchiveEntryFilter.class
                │   PropertiesLauncher$PrefixMatchingArchiveFilter.class
                │   PropertiesLauncher.class
                │   WarLauncher.class
                │
                ├───archive
                │       Archive$Entry.class
                │       。。。(省略)
                │
                ├───data
                │       RandomAccessData.class
                │       。。。(省略)
                ├───jar
                │       AsciiBytes.class
                │       。。。(省略)
                └───util
                        SystemPropertyUtils.class

从目录结构我们看出:

|--BOOT-INF 
    |--BOOT-INF\classes 该文件下的文件是我们最后需要执行的代码
    |--BOOT-INF\lib 该文件下的文件是我们最后需要执行的代码的依赖
|--META-INF
    |--MANIFEST.MF 该文件指定了版本以及Start-Class,Main-Class
|--org 该文件下的文件是一个spring loader文件,应用类加载器首先会加载执行该目录下的代码

MANIFEST.MF

BOOT-INF里面的内容不需要多看,就是跟我们应用代码相关的class以及依赖的jar。
MANIFEST.MF描述了该Jar文件的很多信息,详细请参考MANIFEST.MF详解
那么这里面需要关注的两个值:Main-Class和Start-Class

  • Main-Class:定义jar文件的入口类,该类必须是一个可执行的类,作为一个可执行的jar包,就必须提供这个属性,且对应的class必须提供main方法。
  • Start-Class:springboot应用实际启动类,即含有@SpringBootApplication注解的那个启动类。
Manifest-Version: 1.0
Implementation-Title: spring-boot-exception
Implementation-Version: 1.0.0-SNAPSHOT
Built-By: Gary_Chen1
Implementation-Vendor-Id: com.gpcoding
Spring-Boot-Version: 2.1.0.RELEASE
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.gpcoding.ExceptionApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_271
Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo
 ot-starter-parent/spring-boot-exception

到这,我们可以看出来一丝不同,开发所运行的启动类,并不设置在Main-Class,而是在Start-Class,而Main-Class提供的是"org.springframework.boot.loader.JarLauncher",所以通过jar包启动,我们需要分析JarLauncher这个类(需要在pom中加入spring-boot-loader依赖)。

JarLauncher

JDWP调试:

java -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=5005,suspend=y -jar spring-boot-exception.jar

main()方法创建了JarLauncher实例,调用了launch()方法。

public static void main(String[] args) throws Exception {
    new JarLauncher().launch(args);
}
//new JarLauncher()调用父类的构造方法,将成员变量 archive 初始化
public ExecutableArchiveLauncher() {
    try {
        this.archive = createArchive();
        this.classPathIndex = getClassPathIndex(this.archive);
    }
    catch (Exception ex) {
        throw new IllegalStateException(ex);
    }
}

launch()
该方法最主要的四件事:

  • 获取应用程序需要加载的类和jar的url
  • 根据这些URL创建LaunchedURLClassLoader
  • 通过LaunchedURLClassLoader加载MANIFEST.MF里面配置的Start-Class
  • 通过反射调用Start-Class对应类的main方法
protected void launch(String[] args) throws Exception {
    //注册"java.protocol.handler.pkgs" property值是"org.springframework.boot.loader"
    JarFile.registerUrlProtocolHandler();
    //创建ClassLoader,LaunchedURLClassLoader
    ClassLoader classLoader = createClassLoader(getClassPathArchives());
    //调用launch方法,调用getMainClass()获取Start-Class
    launch(args, getMainClass(), classLoader);
}
//调用ExecutableArchiveLauncher的getClassPathArchives方法,获取
protected List<Archive> getClassPathArchives() throws Exception {
    //这边获取到所有BOOT-INF/lib/的jar以及BOOT-INF/classes/下面class的url
    List<Archive> archives = new ArrayList<>(
            this.archive.getNestedArchives(this::isNestedArchive));
    postProcessClassPathArchives(archives);
    return archives;
}
//isNestedArchive是根据名称来判断的是BOOT-INF/classes/还是BOOT-INF/lib/
protected boolean isNestedArchive(Archive.Entry entry) {
    if (entry.isDirectory()) {
        return entry.getName().equals(BOOT_INF_CLASSES);
    }
    return entry.getName().startsWith(BOOT_INF_LIB);
}
//根据获得到的archives创建新的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]));
}
//最后是使用LaunchedURLClassLoader来加载BOOT-INF下面的类和jar包
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
    return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
//创建完ClassLoader,就调用launch方法
protected void launch(String[] args, String mainClass, ClassLoader classLoader)
        throws Exception {
    //设置线程上下文类加载器,默认是AppClassLoader,现在应该是LaunchedURLClassLoader
    Thread.currentThread().setContextClassLoader(classLoader);
    //拿到了Start-Class,就通过run()方法来调用Springboot的启动类。
    createMainMethodRunner(mainClass, args, classLoader).run();
}
//获取Start-Class
protected String getMainClass() throws Exception {
    //拿到MANIFEST.MF
    Manifest manifest = this.archive.getManifest();
    String mainClass = null;
    if (manifest != null) {
        //获取MANIFEST.MF里面配置的Start-Class
        mainClass = manifest.getMainAttributes().getValue("Start-Class");
    }
    if (mainClass == null) {
        throw new IllegalStateException(
                "No 'Start-Class' manifest entry specified in " + this);
    }
    return mainClass;
}
//这里肯定是通过反射来调用Start-Class的值
public void run() throws Exception {
    //通过新创建的LaunchedURLClassLoader来加载
    Class<?> mainClass = Thread.currentThread().getContextClassLoader()
            .loadClass(this.mainClassName);
    Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
    //通过反射调用main方法
    mainMethod.invoke(null, new Object[] { this.args });
}

最后验证一下可以看出ExceptionApplication是通过LaunchedURLClassLoader类加载加载的,而不是通过应用类加载器。

@SpringBootApplication
public class ExceptionApplication {
    public static void main(String[] args) {
        System.out.println("==========:"+ExceptionApplication.class.getClassLoader());
        SpringApplication.run(ExceptionApplication.class, args);
    }
}
LaunchedURLClassLoader

相关文章

网友评论

    本文标题:1) Springboot打包方式启动分析-自定义类加载器

    本文链接:https://www.haomeiwen.com/subject/vvvexltx.html