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、后端、架構、阿里巴巴等大廠最新面試題

覺得不錯,別忘了點贊+轉發哦!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM