1. 程式人生 > 實用技巧 >maven 的外掛機制,mvn執行外掛的目錄

maven 的外掛機制,mvn執行外掛的目錄

https://stackoverflow.com/questions/21083170/how-to-configure-port-for-a-spring-boot-application

原文:https://www.colabug.com/2020/0205/6947647/

https://docs.spring.io/spring-boot/docs/current/maven-plugin/reference/htmlsingle/#goals

_____________________________________________

初學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
指令應該就是這個外掛提供的。按照之前寫的 《spring boot原始碼編譯踩坑記》
這篇文章把spring boot的原始碼專案匯入IDEA之後,在 spring-boot-project/spring-boot-tools/spring-boot-maven-plugin
找到了這個外掛的原始碼。

由於不懂maven外掛的開發機制,看不太懂,於是去找了下 maven的外掛開發文件
,根據官方的文件,一個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;
}

這裡的核心就是利用java的classloader,找到含有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外掛開發的相關資訊,以及類載入機制

______________________

mv 啟動spring boot修改埠

If you would like to run it locally, use this -

mvn spring-boot:run -Drun.jvmArguments='-Dserver.port=8085'

As ofSpring Boot 2.0, here's the command that works (clues werehere):

mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8085



___________________________


Assaid in docseither setserver.portas system property using command line option to jvm-Dserver.port=8090or addapplication.propertiesin/src/main/resources/with

server.port=8090

For random port use

server.port=0

Similarly addapplication.ymlin/src/main/resources/with

server:
  port : 8090