spring-boot-maven-plugin插件源码分析

Posted by JoshSu Blog on May 16, 2019

通过之前的Maven相关博客, 我们对Maven整体运行流程应该有一个大体认识, 对具体项目运行mvn构建命令时, 能够知道有哪些插件参与此构建任务.

接下来, 我将不断对我们经常使用的插件, 进行源码分析.

这篇博客, 将分析在Spring-Boot项目, 必须使用的一个插件 spring-boot-maven-plugin

spring-boot-maven-plugin 使用

初始化Spring-Boot项目, 在pom.xml文件里, 都需要一个插件.

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

在spring-boot-starter-parent里面, 能找到如下配置.

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <executions>
        <execution>
            <id>repackage</id>
            <goals>
                <goal>repackage</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <mainClass>${start-class}</mainClass>
    </configuration>
</plugin>

结合上一篇博客, 查看该插件目标对应的Mojo描述符, 可知, 该插件作用于 PACKAGE 阶段.

// Mojo描述符
@Mojo(name = "repackage", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true,
		threadSafe = true,
		requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
		requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
public class RepackageMojo extends AbstractDependencyFilterMojo {
    ......
    ......
    ......
}

对于默认的Spring-Boot项目, 执行mvn install后, 需要由如下插件完成.

针对 jar 打包方式

阶段 jar
validate  
initialize  
generate-sources  
process-sources  
generate-resources  
process-resources org.apache.maven.plugins:maven-resources-plugin:2.6:resources
compile org.apache.maven.plugins:maven-compiler-plugin:3.1:compile
process-classes  
generate-test-sources  
process-test-sources  
generate-test-resources org.apache.maven.plugins:maven-resources-plugin:2.6:testResources
process-test-resources  
test-compile org.apache.maven.plugins:maven-compiler-plugin:3.1:testCompile
process-test-classes  
test org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test
prepare-package  
package org.apache.maven.plugins:maven-jar-plugin:3.1.1:jar、org.springframework.boot:spring-boot-maven-plugin:2.1.4.RELEASE:repackage {execution: repackage}
pre-integration-test  
integration-test  
post-integration-test  
verify  
install org.apache.maven.plugins:maven-install-plugin:2.4:install
deploy org.apache.maven.plugins:maven-deploy-plugin:2.7:deploy

package接受编译好的代码,打包成可发布的格式,如JAR.

针对于同一个spring-boot项目, 引入插件 spring-boot-maven-plugin 与 不引入该插件, 打出来的jar包内容是不一样的. 其实最主要的区别就是 可执行jar包与不可执行jar包.

可执行jar包与不可执行jar包 唯一的区别就是jar包里面的MANIFEST.MF文件, 包含有Main-Class属性, 指定java -jar 时, 入口类是哪个?

引入插件 spring-boot-maven-plugin, MANIFEST.MF文件内容

Manifest-Version: 1.0
Implementation-Title: plugin-study
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.josh.pluginstudy.PluginStudyApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.5.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher

不引入插件 spring-boot-maven-plugin, MANIFEST.MF文件内容

Manifest-Version: 1.0
Implementation-Title: plugin-study
Implementation-Version: 0.0.1-SNAPSHOT
Build-Jdk-Spec: 1.8
Created-By: Maven Archiver 3.4.0

通过对比以上内容, 大体应该知道 spring-boot-maven-plugin repackage 的作用了.

接下来我们对该插件 repackage 的源码进行分析

@Mojo(name = "repackage", defaultPhase = LifecyclePhase.PACKAGE, requiresProject = true,
    threadSafe = true,
    requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME,
    requiresDependencyCollection = ResolutionScope.COMPILE_PLUS_RUNTIME)
public class RepackageMojo extends AbstractDependencyFilterMojo {

    @Parameter(property = "spring-boot.repackage.skip", defaultValue = "false")
    private boolean skip;

    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        if (this.project.getPackaging().equals("pom")) {
            getLog().debug("repackage goal could not be applied to pom project.");
            return;
        }
        if (this.skip) {
            getLog().debug("skipping repackaging as per configuration.");
            return;
        }
        repackage();
    }

}

核心逻辑在repackage()方法里面

private void repackage() throws MojoExecutionException {
    // source的值->com.josh:plugin-study:jar:0.0.1-SNAPSHOT
    Artifact source = getSourceArtifact();
    // target的值->项目target下面的jar包 /Users/sulin/project/java/study/plugin-study/target/plugin-study-0.0.1-SNAPSHOT.jar
    File target = getTargetFile();
    Repackager repackager = getRepackager(source.getFile());
    Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(), getFilters(getAdditionalFilters()));
    Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack, getLog());
    try {
        LaunchScript launchScript = getLaunchScript();
        repackager.repackage(target, libraries, launchScript);
    }
    catch (IOException ex) {
        throw new MojoExecutionException(ex.getMessage(), ex);
    }
    updateArtifact(source, target, repackager.getBackupFile());
}

核心逻辑为 repackager.repackage(target, libraries, launchScript);

public void repackage(File destination, Libraries libraries, LaunchScript launchScript) throws IOException {
    // 重新打包进行条件判断 start
    if (destination == null || destination.isDirectory()) {
      throw new IllegalArgumentException("Invalid destination");
    }
    if (libraries == null) {
      throw new IllegalArgumentException("Libraries must not be null");
    }
    if (this.layout == null) {
      this.layout = getLayoutFactory().getLayout(this.source);
    }
    destination = destination.getAbsoluteFile();
    File workingSource = this.source;
    if (alreadyRepackaged() && this.source.equals(destination)) {
      return;
    }
    // 重新打包进行条件判断 end

    // 这段代码, 解释了为什么spring-boot项目, 有 .jar.original 和 .jar两个文件
    // .jar.original 是备份文件, 备份了运行该插件前, 由maven-jar-plugin插件生成的jar包
    if (this.source.equals(destination)) {
        workingSource = getBackupFile();
        workingSource.delete();
        renameFile(this.source, workingSource);
    }

    destination.delete();
    try {
        try (JarFile jarFileSource = new JarFile(workingSource)) {
            // 核心逻辑
            repackage(jarFileSource, destination, libraries, launchScript);
        }
    }
    finally {
        if (!this.backupSource && !this.source.equals(workingSource)) {
            deleteFile(workingSource);
        }
    }
}

核心逻辑 repackage(jarFileSource, destination, libraries, launchScript);

private void repackage(JarFile sourceJar, File destination, Libraries libraries, LaunchScript launchScript) throws IOException {
    WritableLibraries writeableLibraries = new WritableLibraries(libraries);
    try (JarWriter writer = new JarWriter(destination, launchScript)) {
        // 核心代码, 将新的MANIFEST.MF写入jar包
        writer.writeManifest(buildManifest(sourceJar));
        writeLoaderClasses(writer);
        if (this.layout instanceof RepackagingLayout) {
          writer.writeEntries(sourceJar, new RenamingEntryTransformer(
              ((RepackagingLayout) this.layout).getRepackagedClassesLocation()),
              writeableLibraries);
        }
        else {
          writer.writeEntries(sourceJar, writeableLibraries);
        }
        writeableLibraries.write(writer);
    }
}

接下来, 代码细节, 不再贴出, 主要对MANIFEST.MF文件中, Main-Class、Start-Class两个属性的值进行说明.

Main-Class: return “org.springframework.boot.loader.JarLauncher”; 这是写死的

Start-Class: 逻辑稍微有点复杂

manifest = new Manifest(manifest);
String startClass = this.mainClass;
if (startClass == null) {
    // MAIN_CLASS_ATTRIBUTE -> "Main-Class"
    startClass = manifest.getMainAttributes().getValue(MAIN_CLASS_ATTRIBUTE);
}
if (startClass == null) {
    // 在给定的jar文件中查找单个主类。 使用给定{@code annotationName}的注释的主类将优先于没有此类注释的主类。
    // annotationName -> org.springframework.boot.autoconfigure.SpringBootApplication
    startClass = findMainMethodWithTimeoutWarning(source);
}

以上代码能说明该项目 repackage 目标的功能