前言
Spring Boot 项目的运行方式大概可以分为这么几种:IDE 中直接运行 main 方法、mvn spring-boot:run 命令启动、打包后通过 java -jar 方式启动、打包后通过 Tomcat 启动,其中前两种是开发环境下运行的主要方式。
今天我们先来探讨下 mvn spring-boot:run 命令运行 Spring Boot 项目的原理,通过这篇文章你能学到的是 maven 插件基本内容以及 Spring Boot 对 maven 插件的应用。
maven 插件
初学 Spring Boot 时,我们最先接触到的是一个 pom 文件,这个文件内容基本如下。
引入 spring-boot-starter-parent、spring-boot-starter-web、spring-boot-maven-plugin 之后我们就可以通过 mvn spring-boot:run 的方式运行了,当时的我由于对 Spring Boot 了解较少,只能依葫芦画瓢,如今来看,mvn spring-boot:run 运行 Spring Boot 就是使用了自定义的 maven 插件 spring-boot-maven-plugin,想要深入理解这个插件我们需要先对 maven 的插件机制具有一定认识。
认识 maven 插件
maven 作为一个项目管理工具,将项目分成了三个生命周期 clean、default、site,每个生命周期又包括多个阶段,例如 clean 生命周期的阶段包含 pre-clean、clean、post-clean,每个阶段绑定了 0 个或多个插件,功能都是由插件来实现的,例如对于 clean 生命周期的 clean 阶段就是由 maven-clean-plugin 插件来执行的。我们可以通过一个示例来看下,还是使用上面 pom 文件对应的项目,当执行 mvn clean 时可以看到控制台打印如下的日志。
➜ spring-boot-demo mvn clean [INFO] Scanning for projects... [INFO] [INFO] ---------------------< com.zzuhkp:project-parent >---------------------- [INFO] Building project-parent 1.0-SNAPSHOT [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- maven-clean-plugin:3.1.0:clean (default-clean) @ project-parent --- [INFO] Deleting /Users/zzuhkp/hkp/project/spring-boot-demo/target [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 0.434 s [INFO] Finished at: 2022-03-29T21:14:31+08:00 [INFO] ------------------------------------------------------------------------ ➜ spring-boot-demo
注意观察控制台出现了 maven-clean-plugin:3.1.0:clean 字样,maven-clean-plugin 为插件的 artifactId,3.1.0 为插件的版本号,clean 为插件的目标,一个插件可以包含多个目标,生命周期阶段具体是与插件的目标绑定的,插件可以简单理解为一个 Java 类,目标可以简单理解为这个类的方法,到了某个生命周期阶段就会调用该生命周期阶段对应的目标方法。
插件调用方式
插件有如下几种调用方式:
mvn 生命周期阶段:如 mvn clean,maven 会自动执行与该生命周期绑定的插件目标。
mvn groupId:artifactId[:version]:goal,如 mvn org.apache.maven.plugins:maven-clean-plugin:3.1.0:clean,对于版本号 version 来说是可以省略的,如果省略 maven 会使用本地仓库中最新的版本。
mvn 插件前缀:goal:插件前缀可以理解为插件的标识,用于简化插件的调用,例如 mvn spring-boot:run 中的 spring-boot 就是 spring-boot-maven-plugin 插件的前缀,自定义插件如果遵循 xxx-maven-plugin 的形式,maven 默认会将 maven-plugin 前面的内容作为插件前缀。
自定义 maven 插件
maven 将插件的目标称为 MOJO,即 Plain-old-Java-object,表示 maven 中的 POJO,每个目标使用一个实现 Mojo 接口的类表示。当然了自定义插件需要先引入一些依赖,必选的依赖是 maven-plugin-api,这个依赖允许使用 Java doc 作为目标的元数据,坐标如下:
<dependency> <groupId>org.apache.maven</groupId> <artifactId>maven-plugin-api</artifactId> <version>3.8.5</version> </dependency>
Java doc 是最早定义 maven 插件目标的元数据的方式,出现于注解之前,目前使用注解较多,因此还需要引入 maven-plugin-annotations
依赖。
<dependency> <groupId>org.apache.maven.plugin-tools</groupId> <artifactId>maven-plugin-annotations</artifactId> <version>3.6.4</version> <scope>provided</scope> </dependency>
插件编译后会根据 Java doc 或注解生成 META-INF/maven/plugin.xml 文件,这个文件中包含插件的一些基本信息,如插件前缀是什么、有哪些目标等。
maven 提供了一个实现 Mojo 的抽象类 AbstractMojo,自定义插件目标直接继承这个类即可,下面看下我们自定义的插件目标。
@Mojo(name = "custom", defaultPhase = LifecyclePhase.CLEAN) @Execute(phase = LifecyclePhase.CLEAN) public class CustomMojo extends AbstractMojo { @Override public void execute() throws MojoExecutionException, MojoFailureException { getLog().info("自定义插件执行"); } }
@Mojo 注解表示这个类是一个插件目标,name 属性用于指定目标的名称,defaultPhase 用于指定这个目标默认绑定的生命周期阶段。此外自定义的插件目标还添加了一个 @Execute 注解,并使用 phase 属性指定了生命周期阶段。我们自定义插件仅打印了表示已执行的日志。
defaultPhase 和 phase 属性表示生命周期阶段,有何不同呢?
区别主要在于 defaultPhase 仅用于简化使用插件时对插件绑定生命周期阶段的配置,具体如下:
<plugin> <groupId>com.zzuhkp</groupId> <artifactId>custom-maven-plugin</artifactId> <version>1.0-SNAPSHOT</version> <executions> <execution> <!--<phase>clean</phase>--> <goals> <goal>custom</goal> </goals> </execution> </executions> </plugin>
由于我们自定义插件 custom-maven-plugin
目标 custom
使用 defaultPhase 指定了默认绑定的生命周期阶段,那么就不必在 execution
中指定了,添加上面的配置后执行 maven clean
可以自动执行我们自定义的插件目标 custom。
➜ demo mvn clean [INFO] Scanning for projects... [INFO] [INFO] --------------------------< com.zzuhkp:bean >--------------------------- [INFO] Building demo 1.0-SNAPSHOT [INFO] ----------------------------[ maven-plugin ]---------------------------- [INFO] [INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ bean --- [INFO] Deleting /Users/zzuhkp/hkp/project/demo/target [INFO] [INFO] >>> custom-maven-plugin:1.0-SNAPSHOT:custom (default) > clean @ bean >>> [INFO] [INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ bean --- [INFO] [INFO] <<< custom-maven-plugin:1.0-SNAPSHOT:custom (default) < clean @ bean <<< [INFO] [INFO] [INFO] --- custom-maven-plugin:1.0-SNAPSHOT:custom (default) @ bean --- [INFO] 自定义插件执行 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 0.273 s [INFO] Finished at: 2022-03-29T22:06:11+08:00 [INFO] ------------------------------------------------------------------------ ➜ demo
而 @Execute
注解中的 phase
指定的生命周期阶段用于直接执行该插件目标时,等该生命周期阶段对应的插件执行后再执行该插件,执行 maven custom:custom
可以看到如下的控制台日志。
➜ demo mvn custom:custom [INFO] Scanning for projects... [INFO] [INFO] --------------------------< com.zzuhkp:bean >--------------------------- [INFO] Building demo 1.0-SNAPSHOT [INFO] ----------------------------[ maven-plugin ]---------------------------- [INFO] [INFO] >>> custom-maven-plugin:1.0-SNAPSHOT:custom (default-cli) > clean @ bean >>> [INFO] [INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ bean --- [INFO] [INFO] <<< custom-maven-plugin:1.0-SNAPSHOT:custom (default-cli) < clean @ bean <<< [INFO] [INFO] [INFO] --- custom-maven-plugin:1.0-SNAPSHOT:custom (default-cli) @ bean --- [INFO] 自定义插件执行 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 0.272 s [INFO] Finished at: 2022-03-29T22:11:23+08:00 [INFO] ------------------------------------------------------------------------ ➜ demo
@Execute 注解 phase 指定的生命周期阶段是 clean,因此这个阶段对应的 maven-clean-plugin 执行后才执行我们自定义的插件。
spring-boot-maven-plugin
了解 maven 插件的机制后就可以看在 Spring Boot 项目引入的 spring-boot-maven-plugin 插件了。在引入 spring-boot-maven-plugin 插件的项目下执行命令 mvn help:describe -Dplugin=org.springframework.boot:spring-boot-maven-plugin -Ddetail 查看插件帮助信息,部分结果如下:
➜ spring-boot-demo mvn help:describe -Dplugin=org.springframework.boot:spring-boot-maven-plugin -Ddetail [INFO] Scanning for projects... [INFO] [INFO] ---------------------< com.zzuhkp:project-parent >---------------------- [INFO] Building project-parent 1.0-SNAPSHOT [INFO] --------------------------------[ jar ]--------------------------------- [INFO] [INFO] --- maven-help-plugin:3.2.0:describe (default-cli) @ project-parent --- [INFO] org.springframework.boot:spring-boot-maven-plugin:2.2.7.RELEASE Name: Spring Boot Maven Plugin Description: Spring Boot Maven Plugin Group Id: org.springframework.boot Artifact Id: spring-boot-maven-plugin Version: 2.2.7.RELEASE Goal Prefix: spring-boot This plugin has 6 goals: spring-boot:run Description: Run an executable archive application. Implementation: org.springframework.boot.maven.RunMojo Language: java Bound to phase: validate Before this goal executes, it will call: Phase: 'test-compile' Available parameters: mainClass User property: spring-boot.run.main-class The name of the main class. If not specified the first compiled class found that contains a 'main' method will be used.
Goal Prefix: spring-boot 表明了 spring-boot-maven-plugin 的插件前缀是 spring-boot,我们还得到信息这个插件拥有 6 个目标。
对于我们这篇分析的 spring-boot:run 目标来说,它用于执行应用程序,实现类是 RunMojo,默认绑定了生命周期 validate,并且在目标执行时将会先调用 test-compile 生命周期阶段。此外目标还具有一些参数用于调整自身的行为,例如对于 mainClass 来说可以手动指定主类。
mvn spring-boot:run 分析
那么 spring-boot:run 目标到底如何执行 Spring Boot 应用程序的呢?根据帮助信息的描述,我们找到 org.springframework.boot.maven.RunMojo 类的代码。
@Mojo(name = "run", requiresProject = true, defaultPhase = LifecyclePhase.VALIDATE, requiresDependencyResolution = ResolutionScope.TEST) @Execute(phase = LifecyclePhase.TEST_COMPILE) public class RunMojo extends AbstractRunMojo { }
这个目标类继承了 AbstractRunMojo
类,execute
方法由这个父类实现,因此我们看下这个父类执行目标的核心代码。
public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { @Parameter(property = "spring-boot.run.skip", defaultValue = "false") private boolean skip; @Override public void execute() throws MojoExecutionException, MojoFailureException { if (this.skip) { getLog().debug("skipping run as per configuration."); return; } run(getStartClass()); } }
核心方法比较简单,如果不需要跳过则获取并运行启动类,先看下如何获取启动类的吧。
public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { @Parameter(property = "spring-boot.run.main-class") private String mainClass; private String getStartClass() throws MojoExecutionException { String mainClass = this.mainClass; if (mainClass == null) { try { // 如果没有配置主类,则从输出路径中查找标注了 @SpringBootApplication 注解且存在 main 方法的类 mainClass = MainClassFinder.findSingleMainClass(this.classesDirectory, SPRING_BOOT_APPLICATION_CLASS_NAME); } catch (IOException ex) { throw new MojoExecutionException(ex.getMessage(), ex); } } if (mainClass == null) { throw new MojoExecutionException("Unable to find a suitable main class, please add a 'mainClass' property"); } return mainClass; } }
获取主类时优先使用了配置的主类,如果没有配置则会查找标注了 @SpringBootApplication
注解且存在 main 方法的类作为主类。如果存在多个主类时为了保证使用自己想要的主类只能手动进行配置了。示例如下。
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.zzuhkp.DemoApplication</mainClass> </configuration> </plugin>
获取到主类后下一步就是运行主类了。
public abstract class AbstractRunMojo extends AbstractDependencyFilterMojo { @Parameter(property = "spring-boot.run.fork", defaultValue = "true") private boolean fork; private void run(String startClassName) throws MojoExecutionException, MojoFailureException { boolean fork = isFork(); this.project.getProperties().setProperty("_spring.boot.fork.enabled", Boolean.toString(fork)); if (fork) { doRunWithForkedJvm(startClassName); } else { logDisabledFork(); runWithMavenJvm(startClassName, resolveApplicationArguments().asArray()); } } }
如果需要 fork 则创建新的进程运行应用,否则在当前 JVM 进程中运行应用,默认需要 fork,因此 spring-boot:run 会启一个新的进程。
总结
本文先提出了几种执行 Spring Boot 的方式,然后介绍 maven 插件的机制以及 spring-boot-maven-plugin 如何利用 maven 插件运行 Spring Boot 应用。除了插件执行,Spring Boot 最重要的特性之一是使用 java -jar 的方式启动应用,下篇将会介绍。