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 of Spring Boot 2.0, here's the command that works (clues were here):
mvn spring-boot:run -Dspring-boot.run.arguments=--server.port=8085
___________________________
As said in docs either set server.port
as system property using command line option to jvm -Dserver.port=8090
or add application.properties
in /src/main/resources/
with
server.port=8090
For random port use
server.port=0
Similarly add application.yml
in /src/main/resources/
with
server:
port : 8090