玩转Gradle构建工具(七)、SpringBoot插件源码分析

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情

前言

本系列目录

  1. Task
  2. Project、Task常用API
  3. 文件操作
  4. 依赖管理
  5. 多模块构建
  6. 插件编写
  7. SpringBoot插件源码分析
  8. 过度到Kotlin

SpringBoot提供的Gradle插件用来打包SpringBoot项目,我们知道SpringBoot项目打包后的jar有几个特点,他会把我们的class放在BOOT-INF/classes下,并把项目用到的所有库,放在BOOT-INF/lib下,并设置Main方法入口为org.springframework.boot.loader.JarLauncher,由JarLauncher启动我们自己的Main。

而Gradle插件就是做这个事情的,但这篇文章不会很详细的介绍他源码,因为以我现在的功力,无法深入到Gradle,加上网上没有找到一篇关于他的文章,所以这里只介绍个大概。

而且调试过程,也非常心累,我尝试把这个插件重新编译后,放在Gradle缓存目录下,也就是替换掉原来的插件,Linux下位于路径/home/hxl/.gradle/caches/modules-2/files-2.1/org.springframework.boot/spring-boot-gradle-plugin/x.x.x

由于在源码中增加了一些日志,所以我期望的是在项目中使用bootJar时,会出现这些日志,但是绝望的是,这不一定可行,因为有两个缓存位置不能确定,当我在终端执行bootJar时,时而会打印,时而不会(日志所写的位置是task被执行时,如果被执行,一定会打印),而在IDEA里面也是如此,但是神奇的是,只要IDEA重启后,新编译的插件代码才会生效,而在项目打开时,重新编译插件在放入原来目录下,是不行的,必须重启IDEA。

不知道是什么原因引起,但这样极大拖慢了调试速度。

但没有办法,找不到原因。

源码

SpringBoot的这个插件源码并不多,但是关联性很强,几乎每句都是使用Gradle提供的功能,这就导致不熟悉Gradle底层API,很难看懂。

这个插件源码并不是单独的项目,而是在SpringBoot源码下的一个小模块,位于下面这个路径。

image.png

我们首先打开他的build.gradle,可以看到他对插件的配置,比如id为org.springframework.boot,插件的实现类是org.springframework.boot.gradle.plugin.SpringBootPlugin。

gradlePlugin {
   plugins {
      springBootPlugin {
         id = "org.springframework.boot"
         displayName = "Spring Boot Gradle Plugin"
         description = "Spring Boot Gradle Plugin"
         implementationClass = "org.springframework.boot.gradle.plugin.SpringBootPlugin"
      }
   }
}
复制代码

所以,我们应该从SpringBootPlugin下的apply下开始看,这是Gradle进行回调的地方,也就是入口。

@Override
public void apply(Project project) {
   verifyGradleVersion();
   createExtension(project);
   Configuration bootArchives = createBootArchivesConfiguration(project);
   registerPluginActions(project, bootArchives);
}
复制代码

第一句是验证版本,就不看了,第二句是创建一个扩展,在上一篇文章我们演示扩展是如何使用的,在这里SpringBoot创建了一个名为springBoot的扩展,实例是SpringBootExtension。

private void createExtension(Project project) {
   project.getExtensions().create("springBoot", SpringBootExtension.class, project);
}
复制代码

查看SpringBootExtension后,可以发现能配置一个mainClass属性,还有buildInfo,他用来生成META-INF/build-info.properties文件,用的不多,就不说了,如下,是他的基本用法,之后执行bootJar任务后就会生成上面这个文件。

springBoot{
    mainClass="com.xh"
    buildInfo {
        println(this.destinationDir)
        properties.group="com.h"
    }
}
复制代码

createBootArchivesConfiguration方法用来创建一个名为bootArchives的Configuration。

最后就是registerPluginActions,用来注册任务,bootJar任务就是从这里注册的。

private void registerPluginActions(Project project, Configuration bootArchives) {
   SinglePublishedArtifact singlePublishedArtifact = new SinglePublishedArtifact(bootArchives.getArtifacts());
   @SuppressWarnings("deprecation")
   List<PluginApplicationAction> actions = Arrays.asList(new JavaPluginAction(singlePublishedArtifact),
         new WarPluginAction(singlePublishedArtifact), new MavenPluginAction(bootArchives.getUploadTaskName()),
         new DependencyManagementPluginAction(), new ApplicationPluginAction(), new KotlinPluginAction());
   for (PluginApplicationAction action : actions) {
      withPluginClassOfAction(action,
            (pluginClass) -> project.getPlugins().withType(pluginClass, (plugin) -> action.execute(project)));
   }
}
复制代码

上面代码就是依次调用实现类中的execute方法,比如bootJar任务是由JavaPluginAction实现,除了bootJar任务,还有bootWar任务等,但我们主要分析的是bootJar任务,所以直接看JavaPluginAction.execute方法。

JavaPluginAction

在JavaPluginAction.execute方法下做了很多事,最关键的一步就是调用configureBootJarTask配置bootJar任务,如下。

private TaskProvider<BootJar> configureBootJarTask(Project project) {
 ....
   return project.getTasks().register(SpringBootPlugin.BOOT_JAR_TASK_NAME, BootJar.class, (bootJar) -> {
      bootJar.setDescription(
            "Assembles an executable jar archive containing the main classes and their dependencies.");
      bootJar.setGroup(BasePlugin.BUILD_GROUP);
      bootJar.classpath(classpath);
      Provider<String> manifestStartClass = project
            .provider(() -> (String) bootJar.getManifest().getAttributes().get("Start-Class"));
      bootJar.getMainClass().convention(resolveMainClassName.flatMap((resolver) -> manifestStartClass.isPresent()
            ? manifestStartClass : resolveMainClassName.get().readMainClassName()));
   });
}
复制代码

SpringBoot这个插件打包Jar并不是从0开始打包,而是继承了Gradle提供好的一个Jar任务,只需要配置几个值就可以了,比如main方法所在类,还有jar文件中目录结构是怎样的,需要放入哪些文件等,如上面,SpringBoot自己实现了一个BootJar,继承自Gradle提供的Jar任务,并向manifest文件中配置一个Start-Classs属性,这个属性的值是我们自己的main方法入口,在运行时,首先启动的是org.springframework.boot.loader.JarLauncher由他通过反射启动Start-Classs所指向的类。

BootJar

核心还是在BootJar中的配置,其构造方法中调用了下面这个方法。

private void configureBootInfSpec(CopySpec bootInfSpec) {
   bootInfSpec.into("classes", fromCallTo(this::classpathDirectories));
   bootInfSpec.into("lib", fromCallTo(this::classpathFiles)).eachFile(this.support::excludeNonZipFiles);
   this.support.moveModuleInfoToRoot(bootInfSpec);
   this.support.moveMetaInfToRoot(bootInfSpec);
}
复制代码

上面方法用来做文件复制,也就是将我们编写的所有class,复制在classes文件夹下,并把所有第三方jar包,复制到lib目录下,这里的源(指的是我们的class和jar)路径就是从java这个插件提供的API中获得,可以从上面configureBootJarTask方法下看到,将获得的classpath输出时候,将会是一堆jar文件,还有我们class存放的父路径。

其中参数CopySpec是Gradle提供用来做文件复制的一个API。

在复制过程中,会把所有第三方jar的压缩级别设置为STORED,而这两种级别具体不太了解,只知道SpringBoot在启动时候,会检测第三方jar的压缩级别如果不是ZipCompression.STORED ,那就会抛出异常,导致无法启动。

protected ZipCompression resolveZipCompression(FileCopyDetails details) {
   return isLibrary(details) ? ZipCompression.STORED : ZipCompression.DEFLATED;
}
复制代码

在源码中,有这样一段代码,如下,他是提供一个接口让我们可以在打包时候复制一些自定义文件的。

public CopySpec bootInf(Action<CopySpec> action) {
   CopySpec bootInf = getBootInf();
   action.execute(bootInf);
   return bootInf;
}
复制代码

下面是他的使用方式,作用是在打包时,把/home/xxx.jar这个文件复制到/lib下。

tasks.named("bootJar"){
    bootInf{
        from("/home/xxx.jar")
        into("/lib")
    }
}
复制代码

还可以设置classpath等。

猜你喜欢

转载自juejin.im/post/7105580730196951053
今日推荐