spring boot打包原理,maven及gradle下的构建解析
1. 概述
最近心血来潮,想研究下除了maven之外的部署及依赖管理框架,这就接触到了gradle。在研究gradle配置及部署的同时,发现gradle具备可定制的灵活配置方式以及可重写的编译发布模式,基于对spring boot启动机制及运行模式的了解,两相结合阐述一些较为本质的问题。
2. gradle简述
gradle是一种新兴的构建工具,与maven类似但又在概念上有着优势。gradle基于groovy实现了一套贯穿插件配置,依赖引入,compile,build,clean,打包,部署等一体的解决方案,将所有流程代码化,其内部实现细节(在安装的gradle目录能看到源码)总的来说很有研究价值,在这里不做详述。
gradle使用领域特定语言(dsl)完成全流程配置,对于开发人员来说显得更易于理解和配置,典型的就是依赖引入:
dependencies {
compile group: 'org.projectlombok', name: 'lombok', version: '1.18.12'
compile('org.springframework.boot:spring-boot-starter:2.2.6.RELEASE')
compile('org.springframework.boot:spring-boot-starter-web:2.2.6.RELEASE')
compile group: 'org.springframework.data', name: 'spring-data-commons', version: '2.2.6.RELEASE'
compile group: 'org.springframework.boot', name: 'spring-boot-loader', version: '2.2.6.RELEASE'
testCompile('org.springframework.boot:spring-boot-starter-test:2.2.6.RELEASE')
compile files('./build/classes/java/main/com')
}
上面给出了一些常用的依赖引入,通过不同的关键字来区分引入的包在整个构建流程中的角色,可以指定编译类型,也可以引入本地文件等,且引入的方式整洁而优雅。以task为单元进行定制化开发。
在gradle体系下,所有的构建操作都是可以自定义的,比如编译,clean,打包(package)等,在后文会给出一个具体的配置示例。
3. spring boot包运行模式
通常springboot包是可执行的jar文件,可以轻易得到其内部目录如下:
这里不具体以图片形式展示子目录,文字叙述,感兴趣的小伙伴可研究下
BOOT-INF:
包括该springboot项目的class文件(BOOT-INF/class),也就是你写的代码,属于项目内容
以及所依赖的三方库文件(BOOT-INF/lib),比如通常依赖的spring体系,Apache包等,都
存在里面。
META-INF:
标准jar包制式目录,核心文件META-INF/MANIFEST.MF,在我们执行java -jar的时候jvm就会去解析
jar文件中的该文件,得到Main-Class等等属性,根据该文件中的配置参数去找到项目的静态入口,然后
进行我们所熟知的jvm加载类的过程(当然,这一步会省去编译),我们也可以根据项目运行需要配置自定
义参数,当然springboot就这么干了,核心配置如下:
Manifest-Version: 1.0
Start-Class: com.family.spring.Main
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.3.0.M4
Main-Class: org.springframework.boot.loader.JarLauncher
其实由这个配置就可以看出,springboot在jar模式下启动的时候是配置的JarLauncher而非属于项目的
入口函数,它做了一些前置处理。当然,配置业务入口也是必要的。还指定了类文件与库文件目录,由此可
以看出,目录你随意定,但配置得匹配
org:
上面提供了执行参数配置,源文件及库依赖存储,显而易见,这里就是springboot的前置处理逻辑所在,也
是spring boot加载的核心所在,必不可少。也就是说,无论你用插件也好,或者硬打包写进来也罢,得把它
加载近来。简要说明下,spring-boot-loader是针对springboot定制化开发的加载器,可支持war,jar
等形式。拿jar方式来说,就是执行入口类main函数,然后根据配置信息找到class及lib将关联的jar引入
,通过反射获得Start-Class并执行main函数。我们知道jvm在解析class文件时,将其转换成jvm指令,当
在执行执行静态方法时会进行初始化,也就是在JarLauncher执行时,一切就已经顺理成章的运行了,到了
Start-Class执行时,就是springboot项目运行阶段,loader的任务就完成了。
到这里我们可能会想,既然运行参数取的是META-INF/MANIFEST.MF,岂不是我们可以随意篡改和自定义开发?答案是否定的,除非你自己重写loader,因为有些参数是写死在loader里的,虽然不知道spring团队为啥要这么做,扯远了。
打包:
上文告诉我们包的结构以及jar包模式下springboot的运行流程及阶段,那么springboot的可执行包是怎么生成的呢?这是作为一个构建工具需要具备的一个基本能力。接下来对常用的maven,gradle做简要说明:
maven:
<build>
<finalName>zds-guarantee-skynet</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
依赖于:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader-tools</artifactId>
</dependency>
/**
* 实际上就是spring团队基于maven规范开发了一套插件,这个插件关联了spring-boot-loader-tools
* 的依赖,这个tool有两个能力:
* 1. 具备打包成springboot制式jar包的能力;
* 2. 内聚了spring-boot-loader.jar,打包时将该包平行嵌入,使得最终的输出包具有可执行
* springboot应用的能力;
* 综合来说,这个插件打包的过程如下:
* 1. 执行mvn spring-boot:repackage命令,由maven触发spring-boot-maven-plugin的
* RepackageMojo类,执行execute方法,获取pom.xml里的配置与依赖,插件就只有插件的功能,不会
* 进行打包功能,仅仅是采集一些打包参数;
* 2. 插件将pom参数搜集完成,进入spring-boot-loader-tools工作阶段,主要是Repackager.class完
* 成,依次生成manifest.mf并写入jar包,抽取spring-boot-loader.jar平行写入jar包,最后再写入
* lib和classes(编译后的应用代码),这样核心的springboot可执行jar包就完成了。
**/
题外话:maven插件的运作原理及开发准则
maven插件,指的是基于maven的构建环境下,开发扩展应用,使得用户能够以命令行,如下格式:
mvn [plugin-name]:[goal-name] 的形式被用户执行,从而扩展maven的能力。对于此,我会单开一篇博文谈谈自己的看法。
gradle:
gradle在依赖处理上是兼容maven的,因为maven的长处是拥有一个成熟的依赖管理系统,对于gradle来说这是一个现成且主流的数据仓库,兼容它是必然的。这边也会单开一篇博文谈谈自己的看法,实际上生成过程是大同小异的,关键是不同构建体系的技术性差异。
4. 基于gradle自定义打包spring-boot
其实在gradle环境下的打包已经有了插件的支持,这看似一种多余的操作,本质上是对springboot打包的一种验证。贴配置build.gradle:
import java.util.jar.JarFile
buildscript {
repositories {
mavenLocal()
maven { url "http://maven.aliyun.com/nexus/content/groups/public/" }
}
dependencies {
classpath('org.springframework.boot:spring-boot-gradle-plugin:2.2.6.RELEASE')
}
}
tasks.withType(JavaCompile) {
options.encoding = "GBK"
}
apply plugin: 'idea'
apply plugin: 'java'
dependencies {
compileOnly group: 'org.projectlombok', name: 'lombok', version: '1.18.12'
compile('org.springframework.boot:spring-boot-starter:2.2.6.RELEASE')
compile('org.springframework.boot:spring-boot-starter-web:2.2.6.RELEASE')
compile group: 'org.springframework.boot', name: 'spring-boot-gradle-plugin', version: '2.3.0.RELEASE'//spring boot gradle插件
compile group: 'org.springframework.data', name: 'spring-data-commons', version: '2.2.6.RELEASE'
compile group: 'org.springframework.boot', name: 'spring-boot-loader', version: '2.2.6.RELEASE'
compile group: 'com.alibaba', name: 'fastjson', version: '1.2.68'
compile group: 'org.springframework.data', name: 'spring-data-jdbc', version: '1.1.6.RELEASE'
compile group: 'org.springframework.boot', name: 'spring-boot-maven-plugin', version: '2.2.6.RELEASE'
compile group: 'org.apache.maven.plugin-tools', name: 'maven-plugin-annotations', version: '3.6.0'
testCompile('org.springframework.boot:spring-boot-starter-test:2.2.6.RELEASE')
compile files('./build/classes/java/main/com')
}
//配置项目跟路径,源代码路径
String path = 'E:\\qinyu\\spring-cloud\\spring-family\\'
//执行构建是会自动执行编排的任务,在底层代码已固定,所以需要删除不必要的文件
task cleanBuild(type: Delete){
File file = new File(path+'build')
if(file.exists()){
delete file
}
File f2 = new File(path + 'org')
if(f2.exists()){
delete f2
}
}
//执行构建是会自动执行编排的任务,在底层代码已固定,所以需要删除不必要的文件
task clearLoader(type:Delete){
File f2 = new File(path + 'org')
if(f2.exists()){
delete f2
}
}
//spring boot 定制打包jar,依赖于编译和删除加载器task
task releaseJar(type: Jar,dependsOn: [classes,clearLoader]){
//添加spring boot依赖库文件
into('BOOT-INF/lib'){
from configurations.getByName('compile').filter {
it.name != 'org' && it.name != 'com'
}
}
//加载程序文件
into('BOOT-INF/classes/com'){
from configurations.getByName('compile').filter {
it.name == 'com'
}
}
//jar包索引文件,指出有哪些jar包,不影响程序启动
File file = new File("./classpath.idx")
String classpath = ''
configurations.getByName('compile').each {
if(it.getName()!='com' && it.getName()!= 'org'){
classpath = classpath + it.getName()
}
}
file.write(classpath)
into('BOOT-INF'){
from file.getAbsolutePath()
}
//manifest文件写入
manifest{
attributes 'Start-Class': 'com.family.spring.Main'
attributes 'Main-Class': 'org.springframework.boot.loader.JarLauncher'
attributes 'Spring-Boot-Classes': 'BOOT-INF/classes/'
attributes 'Spring-Boot-Lib': 'BOOT-INF/lib/'
attributes 'Spring-Boot-Version': '2.3.0.M4'
}
//打包时不能压缩jar包,否则启动报错,只能存储方式将jar包打进可执行程序中
setEntryCompression(ZipEntryCompression.STORED)
//导入spring boot loader
String loaderPath = new String(path)
loaderPath = folderPre(loaderPath)
springBootLoader(loaderPath)
File loader = new File(loaderPath+ 'org')
into('org'){
from loader.getAbsolutePath()
}
}
//相关文件夹前置处理
static String folderPre(String loaderPath){
loaderPath = loaderPath + 'build\\'
File f1 = new File(loaderPath)
if(!f1.exists()){
f1.mkdir()
}
loaderPath = loaderPath + 'loader\\'
File f2 = new File(loaderPath)
if(!f2.exists()){
f2.mkdir()
}
return loaderPath
}
//springboot loader加载进来
void springBootLoader(String loaderPath){
configurations.getByName('compile').each{
if(it.getName().contains('spring-boot-loader')){
JarFile jar = new JarFile(it.getAbsolutePath())
jar.entries().each{
if(it.getName().endsWith('.class')){
InputStream inputStream = jar.getInputStream(it)
File file1 = new File(loaderPath+it.name)
file1.createNewFile()
OutputStream os = new FileOutputStream(file1)
int index;
byte[] bytes = new byte[1024];
while ((index = inputStream.read(bytes)) != -1) {
os.write(bytes,0,index)
os.flush()
}
inputStream.close()
os.close()
}else if(it.getName().startsWith('org')){
File file1 = new File(loaderPath+it.name)
file1.mkdir()
}
}
}
}
}
上述代码重现了spring boot打包的所有细节,展示出了打包的所有过程。至于打的包如何被执行,执行的过程是怎样的,本人会单开一篇博文去谈谈自己的看法。