Jenkins自动化部署SpringBoot多模块项目
大家好,我是一航;
上周的时候,跟大家分享了通过Jenkins 一键部署SpringBoot项目:还在手动发包?手把手教你 Jenkins 自动化部署SpringBoot
这一周时间,也有不少朋友通过微信在和我交流Jenkins环境搭建的一些问题,期间就有一个朋友反馈到多模块部署的一个问题,说我写的 jenkins_restart.sh
脚本,在多模块部署的时候,没办法检测到未更新的模块;
什么意思呢?
举个例子,加入一个项目,分了10个小模块,类似于下图:
本次修改,只是模块①修复了1个Bug,其他9个都没有变动,那么编译打包整个项目之后,也只需要更新模块①即可,其他的9个模块完全可以不做任何操作,要做到这一需求,就需要在这10个模块中找出那些模块更新了,那些没有更新;上篇文章中采用的方案是:计算 jar 包的MD5,如果MD5值一样,说明没有更新;
但这是一个方案,是有问题,下面就一起来分析一下问题原因;
以及如何解决多模块的自动部署问题?
前文中我写的脚本,是经过仔细测试的,但这位朋友说到这个问题时,我还很仔细的说没有问题,能检测到;
经过反复沟通之后,让我有点不自信;按着这位朋友说的问题点,测试了一番;确实存在这个问题,就算代码没有做任何的改动,Maven打出来的Jar包MD5值都不一样,只是当时脚本测试的策略不对;我就只在第一次编译的时候打了10个模块的包,之后只是测试脚本,为了追求速度,就没有再去编译各个模块了,导致后面所有的脚本测试,都是用的第一次打出来的Jar,所以MD5值都一样;因此整个过程,丝毫没发现有啥问题。
问题复现
MD5 判断文件是否改变,思路似乎没有任何问题;代码既然没做任何改变,所有文件结构目录也相同,那按理说打出来的Jar包的MD5值应该是一样的,但为什么会有问题呢?为了验证这个问题,对项目连续打两次包,分别得到两个相同大小的 a.jar
和 b.jar
;然后做了MD5计算,发现确实不一样:
然后Beyound对两个包进行比较,发现除了修改时间不同,文件内容也都是一摸一样的;
这是为啥呢?
Zip测试及原因分析
Java打出来的Jar包格式是以zip文件格式作为基础,为了方便,我们用Zip包做一下测试;
准备了2个相同内容的测试文件 a.txt
和 b.txt
,里面保存相同的内容:123
;先对txt文件进行MD5值计算,然后将两个文件打包成zip之后,再计算MD5值;
可以看出,不压缩前,a.txt
和 b.txt
的MD5是一样的;压缩之后的zip包,MD5值就不同了;同时我们再看一下文件大小,不压缩前,文件只有4字节大小,可压缩之后反而变成更大的164字节;只能说明压缩的时候,还被添加了其他的信息;
经过查阅,在这篇文章中找到了原因:https://adoyle.me/blog/why-zip-file-checksum-changed.html
Zip在压缩的时候,会将将文件的access time写入到压缩包中,压缩包里面虽然保存的文件内容虽然是一致的,但由于时间不同,导致最终压缩包的MD5值也就不一致;因此,jar 包所面临的问题就属于类似的情况。
解决方案
既然知道包里面的文件都是一样的,只是由于压缩带来的问题,我们完全可以换个思路来解决,将Jar包解压之后,判断各个文件是否发生变化,同样也能够校验出来,过程如下:
-
只用
unzip
命令解压Jar包unzip app.jar -d /tmp/jar_unzip_tmp
-
通过
find
命令查找解压目录下的所有文件并计算MD5值find /tmp/jar_unzip_tmp -type f -print | xargs md5sum > ./jar_files # 上面的这条命令等价于下面这个for循环 #for file in `find /tmp/jar_unzip_tmp` #do # if [ -f $file ];then # echo $file # `md5sum $file >> ./jar_files` # fi #done
得到的
jar_files
;左侧表示文件的MD5值,右侧为文件的路径;如果文件内容发生变化,左侧MD5就会不同,如果是结构/目录发生变化,右侧的详细路径就会不一样; -
计算详情列表(jar_files)对应的MD5值
如果代码发生变化、目录结构发生变化,得到的文件详情列表就是产生差异,那根据详情列表得到的MD5值也就不同了
-
没有或者与前一次不一样
发生变化,需要更新重启
-
MD5校验一致
未发生变化,跳过
-
Jenkins 多模块自动构建
本文的主要目的是:优化多模块的自动化构建,能感知变化,只自动部署已经修改的模块;
通过上面的原因分析以及解决方案梳理,下面就来调整一下相关的脚本;
以下的内容是基于上一篇文章《还在手动发包?手把手教你 Jenkins 自动化部署SpringBoot》的改进,如果还没有看过前文,麻烦稍微花点时间阅读一下,再继续往下看;
SSH方式优化
SSH的方式,主要的修改是在 jenkins_restart.sh
脚本上,当Jar被传到运行服务,执行 jenkins_restart.sh
脚本启动各个模块的时候,解压检测,变化的就重启,没变的就跳过
-
脚本
脚本的每行都加了注释,没什么特别的地方,实现的也就是上面解决方案的四个步骤
#!/bin/sh # JDK的环境变量 export JAVA_HOME=/usr/local/jdk-11.0.14 export PATH=$JAVA_HOME/bin:$PATH # 基础路径,由参数传入 # 多模块的时候,需要在路径中使用*统配一下多模块 # 比如/opt/ehang-spring-boot是多模块,下面由module1和module2 # 那么执行shell的时候使用:sh restart.sh /opt/ehang-spring-boot/\* 注意这里的*需要转义一下 JAR_BATH=$1 echo "基础路径:"$JAR_BATH JAR_PATH=${JAR_BATH}/target/*.jar # 临时的解压目录 JAR_UNZIP_PATH=/tmp/jar_unzip_tmp # 获取所有的JAR 开始遍历 for JAR_FILE in $JAR_PATH do if [ -f $JAR_FILE ] then echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>" echo "JAR路径:"$JAR_FILE #JAR_FILE_MD5=${JAR_FILE}.md5 # 解压目录的文件列表详情及MD5 JAR_FILES_INFO=${JAR_FILE}_files # 详情列表文件的MD5详细 JAR_FILES_INFO_MD5=${JAR_FILES_INFO}.md5 # 删除解压后的临时文件夹 避免之前的缓存导致解压失败 rm -rf $JAR_UNZIP_PATH # 解压文件 unzip $JAR_FILE -d $JAR_UNZIP_PATH # 遍历解压目录,计算每个文件的MD5值及路径 输出到详情列表文件中 find $JAR_UNZIP_PATH -type f -print | xargs md5sum > $JAR_FILES_INFO # 上面的这条命令等价于下面这个for循环 #for file in `find $JAR_UNZIP_PATH` #do # if [ -f $file ];then # echo $file # `md5sum $file >> $JAR_FILES_INFO` # fi #done # 用于标记是否需要重启的标识 RESTART=false # 判断MD5文件是否存在,存在就校验MD5值 if [ -f $JAR_FILES_INFO_MD5 ]; then # 校验MD5 md5sum --status -c $JAR_FILES_INFO_MD5 # = 0表示校验成功 =1 表示校验失败 if [ $? = 1 ];then echo "MD5校验失败,安装包已经更新!" RESTART=true else echo "与前一次的MD5匹配成功,说明安装包没有更新!" fi else echo "没有MD5值,说明是第一次启动" RESTART=true fi # 获取进程号 判断当前服务是否启动;如果Jar没变,但是服务未启动,也需要执行启动脚本 PROCESS_ID=`ps -ef | grep $JAR_FILE | grep -v grep | awk '{print $2}'` # 如果不需要重启,但是进程号没有,说明当前jar没有启动,同样也需要启动一下 if [ $RESTART == false ] && [ ${#PROCESS_ID} == 0 ] ;then echo "没有发现进程,说明服务未启动,需要启动服务" RESTART=true fi # 如果是需要启动 if [ $RESTART == true ]; then # kill掉原有的进程 ps -ef | grep $JAR_FILE | grep -v grep | awk '{print $2}' | xargs kill -9 #如果出现Jenins Job执行完之后,进程被jenkins杀死,可尝试放开此配置项 #BUILD_ID=dontKillMe #启动Jar nohup java -jar $JAR_FILE > ${JAR_FILE}.log 2>&1 & # =0 启动成功 =1 启动失败 if [ $? == 0 ];then echo "restart success!!! process id:" `ps -ef | grep $JAR_FILE | grep -v grep | awk '{print $2}'` else echo "启动失败!" fi # 将最新的MD5值写入到缓存文件 md5sum $JAR_FILES_INFO > $JAR_FILES_INFO_MD5 fi echo "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" echo "" fi done
-
测试
Docker 方式优化
Docker 的镜像 和 ZIP压缩包有着类似的问题,就算是同一个jar、同一个Dockerfile,连续两次执行 docker build
构建出来的镜像,他的镜像ID也是不一样的;
Docker相比于SSH方式,在操作步骤上,就会存在一些差异,SSH方式是在启动Jar之前去做校验;但如果使用Docker的方式,在Jenkins镜像构建之前,就需要判断那些Jar发生了变化,然后只对有变化的Jar包去构建镜像,没有改变的,跳过镜像构建;因此,Docker方式主要调整的就是镜像构建的脚本 docker-image-build.sh
;其他脚本和前文的一样;
-
脚本调整
# 该脚本是用于单jar(业务和依赖绑定)的检测,打包,部署 # docker的名称 MODULE_DOCKER_IMAGE_NAME=ehang-sping-boot-hello-world # 项目目录 MODULE_BATH_PATH=./spring-boot-001-hello-world # docker的配置文件目录 MODULE_DOCKER_CONFIG_PATH=${MODULE_BATH_PATH}/docker # jar_check_md5 通过jar的md5值直接检测 # jar_unzip_check_md5 通过对jar包解压 校验文件详情的MD5 # check_md5 汇总上面两个方法的校验 # 直接通过jar校验 jar_check_md5() { # jar 包的路径 JAR_FILE=$1 if [ ! -f $JAR_FILE ]; then # 如果校验的jar不存在 返回失败 return 1 fi JAR_MD5_FILE=${JAR_FILE}.md5 echo "Jenkins Docker镜像构建校验 JAR的MD5文件:"$JAR_MD5_FILE if [ -f $JAR_MD5_FILE ]; then md5sum --status -c $JAR_MD5_FILE RE=$? md5sum $JAR_FILE > $JAR_MD5_FILE return $RE else md5sum $JAR_FILE > $JAR_MD5_FILE fi return 1 } # 将Jar解压之后校验 jar_unzip_check_md5() { # jar 包的路径 UNZIP_JAR_FILE=$1 if [ ! -f $UNZIP_JAR_FILE ]; then # 如果校验的jar不存在 返回失败 return 1 fi # jar的名称 UNZIP_JAR_FILE_NAME=`basename -s .jar $UNZIP_JAR_FILE` echo "Jenkins Docker镜像构建校验 JAR包名称:"$UNZIP_JAR_FILE_NAME # jar所在的路径 UNZIP_JAR_FILE_BASE_PATH=${UNZIP_JAR_FILE%/${UNZIP_JAR_FILE_NAME}*} echo "Jenkins Docker镜像构建校验 JAR包路径:"$UNZIP_JAR_FILE_BASE_PATH # 解压的临时目录 JAR_FILE_UNZIP_PATH=${UNZIP_JAR_FILE_BASE_PATH}/jar_unzip_tmp echo "Jenkins Docker镜像构建校验 解压路径:"$JAR_FILE_UNZIP_PATH # 用于缓存解压后文件详情的目录 UNZIP_JAR_FILE_LIST=${UNZIP_JAR_FILE_BASE_PATH}/${UNZIP_JAR_FILE_NAME}.files echo "Jenkins Docker镜像构建校验 jar文件详情路径:"$UNZIP_JAR_FILE_LIST # 缓存解压后文件详情的MD5 UNZIP_JAR_FILE_LIST_MD5=${UNZIP_JAR_FILE_BASE_PATH}/${UNZIP_JAR_FILE_NAME}.files.md5 echo "Jenkins Docker镜像构建校验 jar文件详情MD5校验路径:"$UNZIP_JAR_FILE_LIST rm -rf $JAR_FILE_UNZIP_PATH mkdir -p $JAR_FILE_UNZIP_PATH # 解压文件到临时目录 unzip $UNZIP_JAR_FILE -d $JAR_FILE_UNZIP_PATH # 遍历解压目录,计算每个文件的MD5值及路径 输出到详情列表文件中 find $JAR_FILE_UNZIP_PATH -type f -print | xargs md5sum > $UNZIP_JAR_FILE_LIST rm -rf $JAR_FILE_UNZIP_PATH if [ ! -f $UNZIP_JAR_FILE_LIST_MD5 ]; then # 如果校验文件不存在 直接返回校验失败 md5sum $UNZIP_JAR_FILE_LIST > $UNZIP_JAR_FILE_LIST_MD5 return 1 fi # 根据上一次生成的MD5校验 md5sum --status -c $UNZIP_JAR_FILE_LIST_MD5 RE=$? # 生成最新的文件列表的MD5 md5sum $UNZIP_JAR_FILE_LIST > $UNZIP_JAR_FILE_LIST_MD5 # 返回校验结果 return $RE } check_md5() { # jar 包的路径 JAR_FILE=$1 if [ -f $JAR_FILE ]; then # 直接通过jar校验 jar_check_md5 $JAR_FILE if [ $? = 0 ];then echo "Jenkins Docker镜像构建校验 通过Jar的MD5校验成功" return 0 else echo "Jenkins Docker镜像构建校验 通过Jar的MD5校验失败" fi # 通过解压jar 校验是否更新 jar_unzip_check_md5 $JAR_FILE if [ $? = 0 ];then echo "Jenkins Docker镜像构建校验 通过解压的MD5校验成功" return 0 else echo "Jenkins Docker镜像构建校验 通过解压的MD5校验失败" fi fi return 1 } \cp -r ${MODULE_BATH_PATH}/target/*.jar ${MODULE_DOCKER_CONFIG_PATH} APP_UPDATE=false for APP_JAR_FILE in ${MODULE_DOCKER_CONFIG_PATH}/*.jar do echo $APP_JAR_FILE if [ -f $APP_JAR_FILE ];then echo "Jenkins Docker镜像构建校验lib 依赖Jar:"$APP_JAR_FILE check_md5 $APP_JAR_FILE if [ $? = 0 ];then echo "Jenkins Docker镜像构建校验lib!成功,没有发生变化"$APP_JAR_FILE else APP_UPDATE=true echo "Jenkins Docker镜像构建校验lib!失败,已经更新"$APP_JAR_FILE fi fi done if [ $APP_UPDATE = true ]; then # 构建镜像 docker build -t registry.cn-guangzhou.aliyuncs.com/ehang_jenkins/${MODULE_DOCKER_IMAGE_NAME}:latest ${MODULE_DOCKER_CONFIG_PATH}/. # 将镜像推送到阿里云 docker push registry.cn-guangzhou.aliyuncs.com/ehang_jenkins/${MODULE_DOCKER_IMAGE_NAME}:latest fi
这是一段Maven构建完之后,用于检测Jar是否发生更新的脚本,稍微有点点长,我们来简单的分块解读一下
下面是几个公共方法:
-
直接通过Jar的MD5值检测
# 直接通过jar校验 jar_check_md5() { # jar 包的路径 JAR_FILE=$1 if [ ! -f $JAR_FILE ]; then # 如果校验的jar不存在 返回失败 return 1 fi JAR_MD5_FILE=${JAR_FILE}.md5 echo "Jenkins Docker镜像构建校验 JAR的MD5文件:"$JAR_MD5_FILE if [ -f $JAR_MD5_FILE ]; then md5sum --status -c $JAR_MD5_FILE RE=$? md5sum $JAR_FILE > $JAR_MD5_FILE return $RE else md5sum $JAR_FILE > $JAR_MD5_FILE fi return 1 }
-
解压Jar,然后根据文件详情的MD5值检验是否改变
# 将Jar解压之后校验 jar_unzip_check_md5() { # jar 包的路径 UNZIP_JAR_FILE=$1 if [ ! -f $UNZIP_JAR_FILE ]; then # 如果校验的jar不存在 返回失败 return 1 fi # jar的名称 UNZIP_JAR_FILE_NAME=`basename -s .jar $UNZIP_JAR_FILE` echo "Jenkins Docker镜像构建校验 JAR包名称:"$UNZIP_JAR_FILE_NAME # jar所在的路径 UNZIP_JAR_FILE_BASE_PATH=${UNZIP_JAR_FILE%/${UNZIP_JAR_FILE_NAME}*} echo "Jenkins Docker镜像构建校验 JAR包路径:"$UNZIP_JAR_FILE_BASE_PATH # 解压的临时目录 JAR_FILE_UNZIP_PATH=${UNZIP_JAR_FILE_BASE_PATH}/jar_unzip_tmp echo "Jenkins Docker镜像构建校验 解压路径:"$JAR_FILE_UNZIP_PATH # 用于缓存解压后文件详情的目录 UNZIP_JAR_FILE_LIST=${UNZIP_JAR_FILE_BASE_PATH}/${UNZIP_JAR_FILE_NAME}.files echo "Jenkins Docker镜像构建校验 jar文件详情路径:"$UNZIP_JAR_FILE_LIST # 缓存解压后文件详情的MD5 UNZIP_JAR_FILE_LIST_MD5=${UNZIP_JAR_FILE_BASE_PATH}/${UNZIP_JAR_FILE_NAME}.files.md5 echo "Jenkins Docker镜像构建校验 jar文件详情MD5校验路径:"$UNZIP_JAR_FILE_LIST rm -rf $JAR_FILE_UNZIP_PATH mkdir -p $JAR_FILE_UNZIP_PATH # 解压文件到临时目录 unzip $UNZIP_JAR_FILE -d $JAR_FILE_UNZIP_PATH # 遍历解压目录,计算每个文件的MD5值及路径 输出到详情列表文件中 find $JAR_FILE_UNZIP_PATH -type f -print | xargs md5sum > $UNZIP_JAR_FILE_LIST rm -rf $JAR_FILE_UNZIP_PATH if [ ! -f $UNZIP_JAR_FILE_LIST_MD5 ]; then # 如果校验文件不存在 直接返回校验失败 md5sum $UNZIP_JAR_FILE_LIST > $UNZIP_JAR_FILE_LIST_MD5 return 1 fi # 根据上一次生成的MD5校验 md5sum --status -c $UNZIP_JAR_FILE_LIST_MD5 RE=$? # 生成最新的文件列表的MD5 md5sum $UNZIP_JAR_FILE_LIST > $UNZIP_JAR_FILE_LIST_MD5 # 返回校验结果 return $RE }
-
check_md5
汇总了前面两种校验方式
check_md5() { # jar 包的路径 JAR_FILE=$1 if [ -f $JAR_FILE ]; then # 直接通过jar校验 jar_check_md5 $JAR_FILE if [ $? = 0 ];then echo "Jenkins Docker镜像构建校验 通过Jar的MD5校验成功" return 0 else echo "Jenkins Docker镜像构建校验 通过Jar的MD5校验失败" fi # 通过解压jar 校验是否更新 jar_unzip_check_md5 $JAR_FILE if [ $? = 0 ];then echo "Jenkins Docker镜像构建校验 通过解压的MD5校验成功" return 0 else echo "Jenkins Docker镜像构建校验 通过解压的MD5校验失败" fi fi return 1 }
-
判断APP的jar是否更新了
APP_UPDATE=false for APP_JAR_FILE in ${MODULE_DOCKER_CONFIG_PATH}/*.jar do echo $APP_JAR_FILE if [ -f $APP_JAR_FILE ];then echo "Jenkins Docker镜像构建校验lib 依赖Jar:"$APP_JAR_FILE check_md5 $APP_JAR_FILE if [ $? = 0 ];then echo "Jenkins Docker镜像构建校验lib!成功,没有发生变化"$APP_JAR_FILE else APP_UPDATE=true echo "Jenkins Docker镜像构建校验lib!失败,已经更新"$APP_JAR_FILE fi fi done
-
构建镜像
如果更新了,构建镜像
if [ $APP_UPDATE = true ]; then # 构建镜像 docker build -t registry.cn-guangzhou.aliyuncs.com/ehang_jenkins/${MODULE_DOCKER_IMAGE_NAME}:latest ${MODULE_DOCKER_CONFIG_PATH}/. # 将镜像推送到阿里云 docker push registry.cn-guangzhou.aliyuncs.com/ehang_jenkins/${MODULE_DOCKER_IMAGE_NAME}:latest fi
Jenkin是在构建之前,会在各个项目的目录下,创建了一个临时的
tmp
文件夹,用来临时汇总配置、脚本、jar、解压目录等-
app.jar
当前模块的最新Jar包
-
Dockerfile
构建当前模块镜像的Dockerfile
-
docker-image-build.sh
构建镜像并推送到远端镜像仓库的脚本,主要脚本之一
-
jar_files
缓存本次jar中文件列表信息(MD5、文件路径)
-
jar_files.md5
缓存上一个jar包的
jar_files
对应的MD5值信息,校验是否发生变化的重要文件 -
jar_unzip_tmp
app.jar 解压保存的临时文件,主要为了方便输出
jar_files
,用完就删掉了 -
lib
将会在下一篇文章讲解Maven构建压缩时用到;本文忽略
-
-
测试
-
第一次构建
-
未更新
-
已更新
-
标题:Jenkins自动化部署SpringBoot多模块项目
作者:码霸霸
地址:https://blog.lupf.cn/articles/2022/09/05/1662368452411.html