美文网首页
【源码分析】springboot具体启动过程

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

作者: 我不是刺猬_ | 来源:发表于2019-10-28 10:35 被阅读0次

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

    • 将应用打包成jar文件

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

    image.png
    • 命令行运行一个jar项目
      进入 jar文件所在目录,运行一下命令启动项目
    java -jar demo-0.0.1-SNAPSHOT.jar
    
    • 在命令行运行命令,查看jar包文件目录路
    unzip demo-0.0.1-SNAPSHOT.jar
    

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

    • BOOT-INF
      • classes
        • com
        • application.properties
      • lib
    • META-INF
      • MANIFEST.MF
    • org
      注意:
      BOOT-INF:该目录是项目代码,源代码,配置文件和依赖包,这个可以理解
      META-INF:内容如下
      image.png
      meta一般都是项目元信息,表示项目版本,名称等 description信息。为什么会有这6个记录呢?先保留疑问。
      org:顺着目录打开,可以看到该org目录是org.springframework.boot.loader下的包,解压缩后被放置到了jar包中。

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

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

    • 在idea中扩展包中找到spring-boot-loader包,打开JarLancher类,
    /**
     * {@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文件的类加载器对象,具体过程如下:

    • 将class文件url放置到一个array中。
    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]));
        }
    
    • createClassLoader负责通过url加载,进入到这个方法中,一个参数是我们的jar url,另一个是系统类加载器的class对象。真正加载的类加载器就是new LaunchedURLClassLoader(),再进入到这个方法
    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方法。
    • MainMethodRunner只做了变量替换而已。
    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;
        }
    
    • run()[重点] :可以看到spring boot通过自定义classloader后,在Thread中获取到,通过反射的方式来运行对应的“main”方法,这种巧妙的办法加载应用入口其实不叫“main”也可以,只要入口名对应就行,之所以取“main”,因此可以在IDE中通过run的方式运行,否则取其他名字则不能。
        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却可以,就是自定义了类加载器。

    相关文章

      网友评论

          本文标题:【源码分析】springboot具体启动过程

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