mvn spring-boot:run 是怎样运行 Spring Boot 项目的?

简介: 前言Spring Boot 项目的运行方式大概可以分为这么几种:IDE 中直接运行 main 方法、mvn spring-boot:run 命令启动、打包后通过 java -jar 方式启动、打包后通过 Tomcat 启动,其中前两种是开发环境下运行的主要方式。

前言


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 文件,这个文件内容基本如下。


63.png

引入 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 的方式启动应用,下篇将会介绍。


目录
相关文章
|
6天前
|
SQL XML Java
解决Spring Boot项目中的数据库迁移问题
解决Spring Boot项目中的数据库迁移问题
|
7天前
|
负载均衡 Java 开发者
如何在Spring Boot项目中实现微服务架构?
如何在Spring Boot项目中实现微服务架构?
|
12天前
|
关系型数据库 MySQL Java
基于SpringBoot+Vue旅游管理系统【源码(完整源码请私聊)+论文+演示视频+包运行成功】
基于SpringBoot+Vue旅游管理系统【源码(完整源码请私聊)+论文+演示视频+包运行成功】
14 0
基于SpringBoot+Vue旅游管理系统【源码(完整源码请私聊)+论文+演示视频+包运行成功】
|
12天前
|
安全 JavaScript Java
基于SpringBoot+Vue论坛管理系统【源码(完整源码请私聊)+论文+演示视频+包运行成功】
基于SpringBoot+Vue论坛管理系统【源码(完整源码请私聊)+论文+演示视频+包运行成功】
14 0
基于SpringBoot+Vue论坛管理系统【源码(完整源码请私聊)+论文+演示视频+包运行成功】
|
7天前
|
SQL XML Java
解决Spring Boot项目中的数据库迁移问题
解决Spring Boot项目中的数据库迁移问题
|
7天前
|
Java BI Spring
在Spring Boot项目中集成异步任务处理
在Spring Boot项目中集成异步任务处理
|
7天前
|
Java 测试技术 数据库
在Spring Boot项目中集成单元测试的策略
在Spring Boot项目中集成单元测试的策略
|
5天前
|
Java 应用服务中间件 开发者
Java面试题:解释Spring Boot的优势及其自动配置原理
Java面试题:解释Spring Boot的优势及其自动配置原理
28 0
|
13天前
|
Java 开发者 Spring
深入理解Spring Boot中的自动配置原理
深入理解Spring Boot中的自动配置原理
|
14天前
|
前端开发 Java 微服务
Spring Boot与微前端架构的集成开发
Spring Boot与微前端架构的集成开发