【源码分析】springboot具体启动过程

2019-10-28  本文已影响0人  我不是刺猬_

1.将项目打包成jar包并运行

在终端中运行./gradlew bootRun将应用打包成一个jar文件:demo-0.0.1-SNAPSHOT.jar

image.png
java -jar demo-0.0.1-SNAPSHOT.jar
unzip demo-0.0.1-SNAPSHOT.jar

可以看到生成的jar包文件目录如下:

疑问:回到上边疑问,Start-Class在项目我们知道是main方法所在类,但是Main-Class呢?JarLancher的作用又是干什么?字面意思感觉也是项目开始类的意思。以及另外两个属性path也有疑问。

2. spring boot项目启动源码分析

/**
 * {@link Launcher} for JAR based archives. This launcher assumes that dependency jars are
 * included inside a {@code /BOOT-INF/lib} directory and that application classes are
 * included inside a {@code /BOOT-INF/classes} directory.
 *
 * @author Phillip Webb
 * @author Andy Wilkinson
 */
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);
    }

}

分析: 该类继承了ExecutableArchiveLauncher抽象类,两个静态成员变量属性,原来jar包部分目录是JarLancher指定的,最后main方法,实际上是JarLancher对象将main方法的参数通过lanch方法加载。我们再往上看ExecutableArchiveLauncher抽象类,及跳转launch方法。

**
 * Base class for executable archive {@link Launcher}s.
 *
 * @author Phillip Webb
 * @author Andy Wilkinson
 */
public abstract class ExecutableArchiveLauncher extends Launcher {

    private final Archive archive;

    public ExecutableArchiveLauncher() {
        try {
            this.archive = createArchive();
        }
        catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }

    protected ExecutableArchiveLauncher(Archive archive) {
        this.archive = archive;
    }

    protected final Archive getArchive() {
        return this.archive;
    }

    @Override
    protected String getMainClass() throws Exception {
        Manifest manifest = this.archive.getManifest();
        String mainClass = null;
        if (manifest != null) {
            mainClass = manifest.getMainAttributes().getValue("Start-Class");
        }
        if (mainClass == null) {
            throw new IllegalStateException(
                    "No 'Start-Class' manifest entry specified in " + this);
        }
        return mainClass;
    }

    @Override
    protected List<Archive> getClassPathArchives() throws Exception {
        List<Archive> archives = new ArrayList<>(
                this.archive.getNestedArchives(this::isNestedArchive));
        postProcessClassPathArchives(archives);
        return archives;
    }

    /**
     * Determine if the specified {@link JarEntry} is a nested item that should be added
     * to the classpath. The method is called once for each entry.
     * @param entry the jar entry
     * @return {@code true} if the entry is a nested item (jar or folder)
     */
    protected abstract boolean isNestedArchive(Archive.Entry entry);

    /**
     * Called to post-process archive entries before they are used. Implementations can
     * add and remove entries.
     * @param archives the archives
     * @throws Exception if the post processing fails
     */
    protected void postProcessClassPathArchives(List<Archive> archives) throws Exception {
    }

}

分析:该类也是继承一个Lancher的抽象类,launch()方法就是在最顶层抽象类实现,查看该类的实现类,如下,这时候忽然明白,原来gradle 打包工具,jar包和war包是在这里定义的。

image.png
**
 * Base class for launchers that can start an application with a fully configured
 * classpath backed by one or more {@link Archive}s.
 *
 * @author Phillip Webb
 * @author Dave Syer
 */
public abstract class Launcher {

    /**
     * Launch the application. This method is the initial entry point that should be
     * called by a subclass {@code public static void main(String[] args)} method.
     * @param args the incoming arguments
     * @throws Exception if the application fails to launch
     */
    protected void launch(String[] args) throws Exception {
        JarFile.registerUrlProtocolHandler();
        ClassLoader classLoader = createClassLoader(getClassPathArchives());
        launch(args, getMainClass(), classLoader);
    }

    /**
     * Create a classloader for the specified archives.
     * @param archives the archives
     * @return the classloader
     * @throws Exception if the classloader cannot be created
     */
    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]));
    }

    /**
     * Create a classloader for the specified URLs.
     * @param urls the URLs
     * @return the classloader
     * @throws Exception if the classloader cannot be created
     */
    protected ClassLoader createClassLoader(URL[] urls) throws Exception {
        return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
    }

    /**
     * Launch the application given the archive file and a fully configured classloader.
     * @param args the incoming arguments
     * @param mainClass the main class to run
     * @param classLoader the classloader
     * @throws Exception if the launch fails
     */
    protected void launch(String[] args, String mainClass, ClassLoader classLoader)
            throws Exception {
        Thread.currentThread().setContextClassLoader(classLoader);
        createMainMethodRunner(mainClass, args, classLoader).run();
    }

    /**
     * Create the {@code MainMethodRunner} used to launch the application.
     * @param mainClass the main class
     * @param args the incoming arguments
     * @param classLoader the classloader
     * @return the main method runner
     */
    protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args,
            ClassLoader classLoader) {
        return new MainMethodRunner(mainClass, args);
    }

    /**
     * Returns the main class that should be launched.
     * @return the name of the main class
     * @throws Exception if the main class cannot be obtained
     */
    protected abstract String getMainClass() throws Exception;

    /**
     * Returns the archives that will be used to construct the class path.
     * @return the class path archives
     * @throws Exception if the class path archives cannot be obtained
     */
    protected abstract List<Archive> getClassPathArchives() throws Exception;

    protected final Archive createArchive() throws Exception {
        ProtectionDomain protectionDomain = getClass().getProtectionDomain();
        CodeSource codeSource = protectionDomain.getCodeSource();
        URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
        String path = (location != null) ? location.getSchemeSpecificPart() : null;
        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));
    }

}

解析:launch()中,ClassLoader classLoader = createClassLoader(getClassPathArchives()); 该行代码主要做的就是将类归档文件作为参数,并加载到类加载器中。

    @Override
    protected List<Archive> getClassPathArchives() throws Exception {
        List<Archive> archives = new ArrayList<>(
                this.archive.getNestedArchives(this::isNestedArchive));
        postProcessClassPathArchives(archives);
        return archives;
    }

解析:Archiev代表jar文件归档对象,此时就是获取嵌套的归档对象即我们的项目文件,然后作为classloader的参数。
我们看到spring-boot-loader并没有直接将jar包复制到目录中,因为jar打包规范是将所有的jar包都解压缩后,不允许嵌套,如果将class文件打包成一个jar文件,该方式的缺点是文件目录混乱,重名时就会进行覆盖。但是我们看到在项目源文件中,有lib目录,这些是spring boot通过classloader达到了目的。

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

自定义类加载器加载BOOT-INF文件夹过程
1)getClassPathArchieves() 将子文件夹中的嵌套的jar文件获取到硬盘位置url
2)ClassLoader classLoader = createClassLoader(getClassPathArchives());装载到自定义类加载器中,返回jar文件的类加载器对象,具体过程如下:

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]));
    }
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
        return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
    }
public LaunchedURLClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }
super进入:
   public URLClassLoader(URL[] urls, ClassLoader parent) {
        super(parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        this.acc = AccessController.getContext();
        ucp = new URLClassPath(urls, acc);
    }
再进入super:
   protected SecureClassLoader(ClassLoader parent) {
        super(parent);
        // this is to make the stack depth consistent with 1.1
        SecurityManager security = System.getSecurityManager();
        if (security != null) {
            security.checkCreateClassLoader();
        }
        initialized = true;
    }
再进入super,最终是系统类加载器
 protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }

3)拿到加载class的classLoader对象后,下一步如何将这个加载器对象执行呢?就是第三行代码,launch()方法
参数:args,MANIFEST.MF属性的·Start-Class·属性,即我们main方法入口类;自定义类加载器

    protected void launch(String[] args, String mainClass, ClassLoader classLoader):
            throws Exception {
        Thread.currentThread().setContextClassLoader(classLoader); ##这句话就是将类加载器替换为自定义类加载器
        createMainMethodRunner(mainClass, args, classLoader).run();
    }

获取到MANIFEST.MFStart-Class属性,作为main class参数,

    @Override
    protected String getMainClass() throws Exception {
        Manifest manifest = this.archive.getManifest();
        String mainClass = null;
        if (manifest != null) {
            mainClass = manifest.getMainAttributes().getValue("Start-Class");
        }
        if (mainClass == null) {
            throw new IllegalStateException(
                    "No 'Start-Class' manifest entry specified in " + this);
        }
        return mainClass;
    }

4)Thread.currentThread().setContextClassLoader(classLoader); 这句就是将类加载器替换为自定义类加载器

  1. createMainMethodRunner(mainClass, args, classLoader).run();做了什么呢?主要在run方法。
protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args,
            ClassLoader classLoader) {
        return new MainMethodRunner(mainClass, 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 });
    }

以上就是spring boot加载类的启动过程,这也就是为什么org下是将spring-boot-loader复制到了org文件夹下,不能嵌套jar文件,而BOOT-INF却可以,就是自定义了类加载器。

上一篇下一篇

猜你喜欢

热点阅读