spring boot打包原理,maven及gradle下的构建解析

本文深入探讨SpringBoot项目使用Maven与Gradle构建工具的打包原理,详细解析Gradle配置与自定义打包流程,揭示spring-boot-loader的作用机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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打包的所有细节,展示出了打包的所有过程。至于打的包如何被执行,执行的过程是怎样的,本人会单开一篇博文去谈谈自己的看法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值