mvn spring-boot:run 指令是怎麼運行起spring boot項目的

初學spring boot的時候,按照官方文檔,都是創建了一個項目以後,而後執行 mvn spring-boot:run 就能把這個項目運行起來,我就很好奇這個指令到底作了什麼,以及爲何項目裏包含了main方法的那個class,要加一個 @SpringBootApplication 的註解呢?爲何加了這個註解@SpringBootApplication以後,mvn spring-boot:run 指令就能找到這個class並執行它的main方法呢?html

首先我注意到,用maven新建的spring boot項目,pom.xml 裏面有這麼一條配置:java

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

看來mvn spring-boot:run 指令應該就是這個插件提供的。按照以前寫的《spring boot源碼編譯踩坑記》這篇文章把spring boot的源碼項目導入IDEA以後,在 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin 找到了這個插件的源碼。git

因爲不懂maven插件的開發機制,看不太懂,因而去找了下maven的插件開發文檔,根據官方的文檔,一個maven插件會有不少個目標,每一個目標就是一個 Mojo 類,好比 mvn spring-boot:run 這個指令,spring-boot這部分是一個maven插件,run這部分是一個maven的目標,或者指令。github

根據maven插件的開發文檔,定位到 spring-boot-maven-plugin 項目裏的RunMojo.java,就是mvn spring-boot:run 這個指令所運行的java代碼。關鍵方法有兩個,一個是 runWithForkedJvm,一個是runWithMavenJvm,若是pom.xml是如上述配置,則運行的是 runWithForkedJvm,若是pom.xml裏的配置以下,則運行runWithMavenJvm:spring

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

runWithForkedJvmrunWithMavenJvm 的區別,在於前者是起一個進程來運行當前項目,後者是起一個線程來運行當前項目。apache

我首先了解的是 runWithForkedJvmsegmentfault

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方法所在的類名,以及引用的相關類庫的路徑。springboot

workingDirectory 能夠由maven的 ${project} 變量快速得到,所以這裏的關鍵就是main方法所在的類是怎麼找到的,以及引用的相關類庫的路徑是如何得到的。框架

找main方法所在的類的實現是在 AbstractRunMojo.java 裏面:maven

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項目,這個spring boot項目就是spring官方給出的<<Build a Restful Web Service>>。個人普通的java項目放在github上,springboot_run_v1 這個tag即爲可運行的代碼。

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插件的開發,得到項目的配置信息,執行起指令;一個是類加載機制,以及註解分析。

後續繼續看maven插件開發的相關信息,以及類加載機制

相關文章
相關標籤/搜索