持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情
前言
本系列目录
- Task
- Project、Task常用API
- 文件操作
- 依赖管理
- 多模块构建
- 插件编写
- SpringBoot插件源码分析
- 过度到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源码下的一个小模块,位于下面这个路径。
我们首先打开他的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等。