zoukankan      html  css  js  c++  java
  • spring-boot-run 指令是怎么运行 Spring Boot 项目的?

    作者:沙湖王
    https://segmentfault.com/a/1190000021687878

    初学 Spring Boot 的时候,按照官方文档,都是建立了一个项目之后,然后执行 mvn spring-boot:run 就能把这个项目运行起来。

    我就很好奇这个指令到底做了什么,以及为什么项目里包含了 main 方法的那个class,要加一个 @SpringBootApplication  的注解呢?

    为什么加了这个注解 @SpringBootApplication 之后,mvn spring-boot:run 指令就能找到这个class并执行它的main方法呢?

    首先我注意到,用maven新建的spring boot项目,pom.xml 里面有这么一条配置:

    <build>  
        <plugins>  
            <plugin>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-maven-plugin</artifactId>  
            </plugin>  
        </plugins>  
    </build>      
    
    

    看来mvn spring-boot:run 指令应该就是这个插件提供的。

    由于不懂maven插件的开发机制,看不太懂,于是去找了下 maven 的插件开发文档:

    http://maven.apache.org/guides/plugin/guide-java-plugin-development.html

    根据官方的文档,一个 maven 插件会有很多个目标,每个目标就是一个 Mojo 类,比如 mvn spring-boot:run 这个指令,spring-boot这部分是一个maven插件,run这部分是一个maven的目标,或者指令。

    根据maven插件的开发文档,定位到 spring-boot-maven-plugin 项目里的RunMojo.java,就是mvn spring-boot:run 这个指令所运行的java代码。

    关键方法有两个,一个是 runWithForkedJvm,一个是runWithMavenJvm,如果pom.xml是如上述配置,则运行的是 runWithForkedJvm,如果pom.xml里的配置如下,则运行runWithMavenJvm:

    <build>  
        <plugins>  
            <plugin>  
                <groupId>org.springframework.boot</groupId>  
                <artifactId>spring-boot-maven-plugin</artifactId>  
                <configuration>  
                    <fork>false</fork>  
                </configuration>  
            </plugin>  
        </plugins>  
    </build>      
    
    

    runWithForkedJvm 与 runWithMavenJvm 的区别,在于前者是起一个进程来运行当前项目,后者是起一个线程来运行当前项目。

    我首先了解的是 runWithForkedJvm

    private int forkJvm(File workingDirectory, List<String\> args, Map<String, String\> environmentVariables)    
          throws MojoExecutionException {    
       try {    
          RunProcess runProcess = new RunProcess(workingDirectory, new JavaExecutable().toString());    
      Runtime.getRuntime().addShutdownHook(new Thread(new RunProcessKiller(runProcess)));    
      return runProcess.run(true, args, environmentVariables);    
      }    
       catch (Exception ex) {    
          throw new MojoExecutionException("Could not exec java", ex);    
      }    
    }  
    
    

    根据这段代码,RunProcess是由spring-boot-loader-tools 这个项目提供的,需要提供的workingDirectory 就是项目编译后的 *.class 文件所在的目录,environmentVariables 就是解析到的环境变量,args里,对于spring-boot的那些sample项目,主要是main方法所在的类名,以及引用的相关类库的路径。

    workingDirectory 可以由maven的 ${project} 变量快速获得,因此这里的关键就是main方法所在的类是怎么找到的,以及引用的相关类库的路径是如何获得的。

    找main方法所在的类的实现是在 AbstractRunMojo.java 里面:

    mainClass = MainClassFinder.findSingleMainClass(this.classesDirectory, SPRING\_BOOT\_APPLICATION\_CLASS\_NAME);  
    
    

    MainClassFinder.java 是由spring-boot-loader-tools提供的,找到main方法所在的类主要是如下的代码:

    static <T> T doWithMainClasses(File rootFolder, MainClassCallback<T> callback) throws IOException {  
        if (!rootFolder.exists()) {  
            return null; // nothing to do  
        }  
        if (!rootFolder.isDirectory()) {  
            throw new IllegalArgumentException("Invalid root folder '" + rootFolder + "'");  
        }  
        String prefix = rootFolder.getAbsolutePath() + "/";  
        Deque<File> stack = new ArrayDeque<>();  
        stack.push(rootFolder);  
        while (!stack.isEmpty()) {  
            File file = stack.pop();  
            if (file.isFile()) {  
                try (InputStream inputStream = new FileInputStream(file)) {  
                    ClassDescriptor classDescriptor = createClassDescriptor(inputStream);  
                    if (classDescriptor != null && classDescriptor.isMainMethodFound()) {  
                        String className = convertToClassName(file.getAbsolutePath(), prefix);  
                        T result = callback.doWith(new MainClass(className, classDescriptor.getAnnotationNames()));  
                        if (result != null) {  
                            return result;  
                        }  
                    }  
                }  
            }  
            if (file.isDirectory()) {  
                pushAllSorted(stack, file.listFiles(PACKAGE_FOLDER_FILTER));  
                pushAllSorted(stack, file.listFiles(CLASS_FILE_FILTER));  
            }  
        }  
        return null;  
    }  
    
    

    这里的核心就是利用spring的asm框架,读取class文件的字节码并分析,找到含有main方法的类,然后再判断这个类有没有使用了 @SpringBootApplication 注解,有的话,就属于要执行的代码文件了。

    如果项目里面有多个含有main方法且被 @SpringBootApplication 注解的类的话,我看代码应该是直接选择找到的第一个开运行。

    读取依赖的库路径,在spring-boot-maven-plugin里有大量的代码来实现,还是利用maven本身的特性实现的。

    根据了解到的这些信息,我新建了一个普通的java项目bootexp,用一段简单的代码来运行起一个spring boot项目:

    package com.shahuwang.bootexp;  
      
    import java.io.File;  
    import java.io.IOException;  
    import java.util.ArrayList;  
    import java.util.HashMap;  
    import java.util.List;  
    import java.util.Map;  
      
    import org.springframework.boot.loader.tools.JavaExecutable;  
    import org.springframework.boot.loader.tools.MainClassFinder;  
    import org.springframework.boot.loader.tools.RunProcess;  
      
    public class Runner  
    {  
        public static void main( String[] args ) throws IOException {  
            String SPRING_BOOT_APPLICATION_CLASS_NAME = "org.springframework.boot.autoconfigure.SpringBootApplication";  
            File classesDirectory = new File("C:\share\bootsample\target\classes");  
            String mainClass = MainClassFinder.findSingleMainClass(classesDirectory, SPRING_BOOT_APPLICATION_CLASS_NAME);  
            RunProcess runProcess = new RunProcess(classesDirectory, new JavaExecutable().toString());  
            Runtime.getRuntime().addShutdownHook(new Thread(new RunProcessKiller(runProcess)));  
            List<String> params = new ArrayList<>();  
            params.add("-cp");  
            params.add("相关库路径")  
            params.add(mainClass);  
            Map<String, String> environmentVariables = new HashMap<>();  
            runProcess.run(true, params, environmentVariables);  
        }  
      
        private static final class RunProcessKiller implements Runnable {  
      
            private final RunProcess runProcess;  
      
            private RunProcessKiller(RunProcess runProcess) {  
                this.runProcess = runProcess;  
            }  
      
            @Override  
            public void run() {  
                this.runProcess.kill();  
            }  
      
        }  
    }  
    
    

    相关库的路径获取,都是spring-boot-maven-plugin这个项目里面的私有方法,所以我这里直接在 bootsample 这个spring boot项目下执行 mvn spring-boot:run -X, 输出classpath,把classpath复制过来即可。执行bootexp这个项目,即可运行起 bootsample 这个spring boot项目了。

    所以为什么spring boot的项目,main方法所在的类都要加上注解 @SpringBootApplication 这个疑问也得到了解决。

    综上,mvn spring-boot:run 这个指令为什么能运行起一个spring boot项目就没有那么神秘了,这里主要的难点就两个,一个是maven插件的开发,获得项目的配置信息,执行起指令;一个是类加载机制,以及注解分析。

    关注微信公众号:Java技术栈,在后台回复:boot,可以获取我整理的 N 篇 Spring Boot 教程,都是干货。

    推荐去我的博客阅读更多:

    1.Java JVM、集合、多线程、新特性系列教程

    2.Spring MVC、Spring Boot、Spring Cloud 系列教程

    3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程

    4.Java、后端、架构、阿里巴巴等大厂最新面试题

    觉得不错,别忘了点赞+转发哦!

  • 相关阅读:
    BZOJ3149 CTSC2013 复原 搜索
    BZOJ5016 SNOI2017 一个简单的询问 莫队、前缀和、容斥
    THUWC2019-1:Reach out
    Luogu4630 APIO2018 Duathlon 圆方树、树形DP
    Luogu4606 SDOI2018 战略游戏 圆方树、虚树、链并
    BZOJ3720 Gty的妹子树 询问分块、主席树
    CF809E Surprise me! 莫比乌斯反演、虚树
    LOJ2542 PKUWC2018 随机游走 min-max容斥、树上高斯消元、高维前缀和、期望
    LOJ2541 PKUWC2018 猎人杀 期望、容斥、生成函数、分治FFT
    CF797F Mice and Holes 贪心、栈维护DP
  • 原文地址:https://www.cnblogs.com/javastack/p/12895286.html
Copyright © 2011-2022 走看看