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天前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
1月前
|
Java 微服务 Spring
SpringBoot+Vue+Spring Cloud Alibaba 实现大型电商系统【分布式微服务实现】
文章介绍了如何利用Spring Cloud Alibaba快速构建大型电商系统的分布式微服务,包括服务限流降级等主要功能的实现,并通过注解和配置简化了Spring Cloud应用的接入和搭建过程。
SpringBoot+Vue+Spring Cloud Alibaba 实现大型电商系统【分布式微服务实现】
|
1月前
|
XML JSON Java
使用IDEA+Maven搭建整合一个Struts2+Spring4+Hibernate4项目,混合使用传统Xml与@注解,返回JSP视图或JSON数据,快来给你的SSH老项目翻新一下吧
本文介绍了如何使用IntelliJ IDEA和Maven搭建一个整合了Struts2、Spring4、Hibernate4的J2EE项目,并配置了项目目录结构、web.xml、welcome.jsp以及多个JSP页面,用于刷新和学习传统的SSH框架。
32 0
使用IDEA+Maven搭建整合一个Struts2+Spring4+Hibernate4项目,混合使用传统Xml与@注解,返回JSP视图或JSON数据,快来给你的SSH老项目翻新一下吧
|
1月前
|
安全 Java 数据安全/隐私保护
基于SpringBoot+Spring Security+Jpa的校园图书管理系统
本文介绍了一个基于SpringBoot、Spring Security和JPA开发的校园图书管理系统,包括系统的核心控制器`LoginController`的代码实现,该控制器处理用户登录、注销、密码更新、角色管理等功能,并提供了系统初始化测试数据的方法。
31 0
基于SpringBoot+Spring Security+Jpa的校园图书管理系统
|
1月前
|
前端开发 JavaScript Java
spring boot+vue前后端项目的分离(我的第一个前后端分离项目)
该博客文章介绍了作者构建的第一个前后端分离项目,使用Spring Boot和Vue技术栈,详细说明了前端Vue项目的搭建、后端Spring Boot项目的构建过程,包括依赖配置、数据库连接、服务层、数据访问层以及解决跨域问题的配置,并展示了项目的测试结果。
spring boot+vue前后端项目的分离(我的第一个前后端分离项目)
|
1月前
|
安全 Java 数据库
|
1月前
|
JSON 安全 Java
|
1月前
|
前端开发 Java 测试技术
单元测试问题之在Spring MVC项目中添加JUnit的Maven依赖,如何操作
单元测试问题之在Spring MVC项目中添加JUnit的Maven依赖,如何操作
|
1月前
|
Dubbo Java Nacos
【实战攻略】破解Dubbo+Nacos+Spring Boot 3 Native打包后运行异常的终极秘籍——从零开始彻底攻克那些让你头疼不已的技术难题!
【8月更文挑战第15天】Nacos作为微服务注册与配置中心受到欢迎,但使用Dubbo+Nacos+Spring Boot 3进行GraalVM native打包后常遇运行异常。本文剖析此问题及其解决策略:确认GraalVM版本兼容性;配置反射列表以支持必要类和方法;采用静态代理替代动态代理;检查并调整配置文件;禁用不支持的功能;利用日志和GraalVM诊断工具定位问题;根据诊断结果调整GraalVM配置。通过系统排查方法,能有效解决此类问题,确保服务稳定运行。
52 0
|
1月前
|
Java Windows Spring
Spring Boot CMD 运行日志输出中文乱码
Spring Boot CMD 运行日志输出中文乱码
25 0