初学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>
runWithForkedJvm
与 runWithMavenJvm
的区别,在于前者是起一个进程来运行当前项目,后者是起一个线程来运行当前项目。apache
我首先了解的是 runWithForkedJvm
:segmentfault
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插件开发的相关信息,以及类加载机制