一個小技巧,Maven的打Jar包體積減少100倍

ehang發表於2023-11-19

大家好,我是一航!

為了大家能夠從從專案整合的苦力活中解放出來,最近這一週,整理了兩篇關於Jenkins構建、部署的文章,感興趣的朋友可以看看:

還在手動發包?手把手教你 Jenkins 自動化部署SpringBoot

Jenkins自動化部署SpringBoot多模組專案

這期間,和粉絲大佬交流到關於Maven構建的最佳化、壓縮的問題;一段簡單的配置,就可以將包Jar包的大小縮小近百倍,實際的開發部署過程中,非常的實用的一個小技巧,在這裡分享給各位;

SpringBoot專案的依賴,我們一般都會採用Maven管理,整個專案,一般都分為以下幾部分:

  • 三方依賴

    透過pom.xml檔案配置,新增到專案中來

    特點: 變化小,佔用空間大

  • 業務程式碼

    特點: 變化大,佔用空間小

  • 靜態資源

    特點:變化適中,佔用空間大;不過一般的靜態資源都另外管理,很少會直接放在專案裡面;

而整個專案通常會被構建成一個Jar,上傳到伺服器執行;整個Jar包中,三方依賴會被一併打包進去,佔用空間最大的,也就是這部分依賴包;

比如下面這個最基本的測試 SpringBoot 專案,就一個簡單的hello world介面,但是打包出來的jar就有 20多M

把Jar包解壓之後,發現三方依賴竟然比整個Jar包都大(可能壓縮的原因),自己的程式碼只有100多K;

這還只是一個最基礎的專案,如果業務複雜,依賴多,光是三方包可能就佔用幾十、幾百M之多;

由於依賴包 變化小,佔用空間大的特點,大部分情況是第一次新增之後,後面很少會去調整;但每次修改哪怕是一行程式碼,都需要重新把他們構建Jar中去,往伺服器上傳、釋出,白白消耗了大量的資源、頻寬以及時間;

那能否將三方依賴和自己的業務程式碼分開打包呢?答案是: 可以的

1 依賴拆分配置

只需要在專案 pom.xml檔案中新增下面的配置:



<
build>

     < plugins>
         < plugin>
             < groupId>org.apache.maven.plugins </ groupId>
             < artifactId>maven-compiler-plugin </ artifactId>
             < configuration>
                 < source>1.8 </ source>
                 < target>1.8 </ target>
             </ configuration>
         </ plugin>
         < plugin>
             < groupId>org.springframework.boot </ groupId>
             < artifactId>spring-boot-maven-plugin </ artifactId>
             < configuration>
                 < fork>true </ fork>
                 < finalName>${project.build.finalName} </ finalName>
                 <!--解決windows命令列視窗中文亂碼-->
                 < jvmArguments>-Dfile.encoding=UTF-8 </ jvmArguments>
                 < layout>ZIP </ layout>
                 < includes>
                     <!--這裡是填寫需要包含進去的jar,
                        必須專案中的某些模組,會經常變動,那麼就應該將其座標寫進來
                        如果沒有則non-exists ,表示不打包依賴
                    -->

                     < include>
                         < groupId>non-exists </ groupId>
                         < artifactId>non-exists </ artifactId>
                     </ include>
                 </ includes>
             </ configuration>
             < executions>
                 < execution>
                     < goals>
                         < goal>repackage </ goal>
                     </ goals>
                 </ execution>
             </ executions>
         </ plugin>
         <!--此外掛用於將依賴包抽出-->
         < plugin>
             < groupId>org.apache.maven.plugins </ groupId>
             < artifactId>maven-dependency-plugin </ artifactId>
             < executions>
                 < execution>
                     < id>copy-dependencies </ id>
                     < phase>package </ phase>
                     < goals>
                         < goal>copy-dependencies </ goal>
                     </ goals>
                     < configuration>
                         < outputDirectory>${project.build.directory}/lib </ outputDirectory>
                         < excludeTransitive>false </ excludeTransitive>
                         < stripVersion>false </ stripVersion>
                         < includeScope>runtime </ includeScope>
                     </ configuration>
                 </ execution>
             </ executions>
         </ plugin>
         < plugin>
             < artifactId>maven-surefire-plugin </ artifactId>
             < configuration>
                 < skip>true </ skip>
             </ configuration>
         </ plugin>
     </ plugins>
</ build>

再次構建


mvn clean package -DskipTests=true

發現 target目錄中多了個 lib資料夾,裡面儲存了所有的 依賴jar,自己業務相關的jar也只有小小的 157kb,相比之前21M, 足足小了100多倍

這種方式打的包,在專案啟動時,需要透過 -Dloader.path指定 lib的路徑:


java -Dloader.path=./lib -jar xxx.jar

雖然這樣打包,三方依賴的大小並沒有任何的改變,但有個很大的不同就是我們自己的業務包和依賴包分開了;在不改變依賴的情況下,也就只需要第一次上傳 lib目錄到伺服器,後續業務的調整、bug修復,在沒調整依賴的情況下,就只需要上傳更新小小的業務包即可;既省資源又省時間;就算是依賴變化了,也只需要更新調整的依賴,沒變的依賴包我們也就不管了。

有朋友可能會說: 你這業務包確實小了,但是無形中增加了對依賴包的管理,提高了管理成本

沒錯,這種方式,確實增加的了Jar包的管理成本,多人協調開發,構建的時候,還需要專門去關注是否有人更新依賴;

不過這並不是啥大事兒,前面學習的Jenkins自動化工具,就能自動幫我們維護這個lib目錄,減少人工核對,避免維護成本;

配置好之後,你會發現原有大Jar包的上傳/下載;現在變成只有原來的百分之一;整合速度將會有非常明顯的提升;

2 Jenkins 管理依賴拆分的Jar

這一部分的內容依然是對前兩篇關於Jenkins【單模組】、【多模組】打包的完善,透過最佳化指令碼,來實現 Jenkins 對依賴包、業務包的自動增量管理;

所以,這個並不是從0開始的教程,沒看過前兩篇文章的,可以先去掃一眼,然後再繼續往下看;

SSH的方式

SSH的方式相比於之前的方式,只是多了管理lib中jar的過程,未調整的依賴Jar包,不上傳到伺服器;所以相比之前的方案,多了一個檢測指令碼 jenkins_jar_and_lib_check.sh;他的作用就是在SSH上傳之前,檢測那些依賴更新了,然後只要留已更新的依賴上傳到伺服器;

Jenkins構建的過程

  • 拉取最新程式碼

  • Maven打包

  • Jenkins本地執行 jenkins_jar_and_lib_check.sh檢測依賴Jar和App jar是否更新

  • 上傳已經更新的Jar/指令碼

  • 遠端執行 jenkins_restart_mini.sh

    判斷是否更新並重啟,為了不影響之前的教程,這裡新加了一個指令碼 jenkins_restart_mini.sh,和前面幾篇文章中提到的 jenkins_restart.sh作用是一樣的

    • 判斷依賴jar/業務jar是否更新(任意一個更新都需要重啟)
    • 不需要更新的前提下,判斷程式是否存在
    • 重啟服務

jenkins_jar_and_lib_check.sh指令碼

Jenkins本地校驗踢出未更新的依賴和業務Jar的指令碼;需要在Maven構建完,ssh傳輸之前使用;

完整指令碼地址:

校驗步驟:

  • 模組下建立一個tmp的臨時目錄

  • 將業務jar和依賴jar複製的tmp臨時目錄下

  • 分別對業務jar和依賴jar進行MD5校驗

    • 更新的留下

    • 未更新的刪除掉

      只有Jenkins本地的校驗才刪除,減少不必要的傳輸;服務端的檢驗不要刪了,每一個都需要使用的;

指令碼部分細節說明:

  • Jar包MD5校驗方法: jar_check_md5()

    公共方法!直接透過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校驗 JAR的MD5檔案:"$JAR_MD5_FILE
      if [ -f $JAR_MD5_FILE ]; then
        cat $JAR_MD5_FILE
        md5sum $JAR_FILE
        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的方式沒有成功,就需要再透過解壓的方式校驗

    
    
    #
     將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校驗 JAR包名稱:"$UNZIP_JAR_FILE_NAME
      #  jar所在的路徑
      UNZIP_JAR_FILE_BASE_PATH=${UNZIP_JAR_FILE%/${UNZIP_JAR_FILE_NAME}*}
      echo "jenkins校驗 JAR包路徑:"$UNZIP_JAR_FILE_BASE_PATH
      #  解壓的臨時目錄
      JAR_FILE_UNZIP_PATH=${UNZIP_JAR_FILE_BASE_PATH}/jar_unzip_tmp
      echo "jenkins校驗 解壓路徑:"$JAR_FILE_UNZIP_PATH

      #
     用於快取解壓後檔案詳情的目錄
      UNZIP_JAR_FILE_LIST=${UNZIP_JAR_FILE_BASE_PATH}/${UNZIP_JAR_FILE_NAME}.files
      echo "jenkins校驗 jar檔案詳情路徑:"$UNZIP_JAR_FILE_LIST
      #  快取解壓後檔案詳情的MD5
      UNZIP_JAR_FILE_LIST_MD5=${UNZIP_JAR_FILE_BASE_PATH}/${UNZIP_JAR_FILE_NAME}.files.md5
      echo "jenkins校驗 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

      cat $UNZIP_JAR_FILE_LIST_MD5
      md5sum $UNZIP_JAR_FILE_LIST
      md5sum --status -c $UNZIP_JAR_FILE_LIST_MD5
      RE=$?
      md5sum $UNZIP_JAR_FILE_LIST > $UNZIP_JAR_FILE_LIST_MD5
      #  返回校驗結果
      return $RE
    }
  • 彙總判斷

    公共方法!這裡彙總了 jar_check_md5jar_unzip_check_md5兩個方法呼叫;

    注意:由於這是上傳前Jenkins呼叫的本地校驗,一旦校驗發現沒有改變,會執行 rm -f $JAR_FILE刪除命令;

    
    check_md5() {
    
      #  jar 包的路徑
      JAR_FILE=$1
      if [ -f $JAR_FILE ]; then
        # 直接透過jar校驗
        jar_check_md5 $JAR_FILE
        if [ $? = 0 ];then
          echo "jenkins校驗 透過Jar的MD5校驗成功"
          rm -f $JAR_FILE
          return 0
        else
          echo "jenkins校驗 透過Jar的MD5校驗失敗"
        fi

        # 透過解壓jar 校驗是否更新
        jar_unzip_check_md5 $JAR_FILE
        if [ $? = 0 ];then
          echo "jenkins校驗 透過解壓的MD5校驗成功"
          rm -f $JAR_FILE
          return 0
        else
          echo "jenkins校驗 透過解壓的MD5校驗失敗"
        fi
      fi

      return 1
    }
  • 判斷依賴包

    
    
    #
     lib目錄的路徑
    
    MODULE_LIB_PATH=${MODULE_PATH}/target/lib
    echo "jenkins校驗 lib目錄:"$MODULE_LIB_PATH
    if [ -d $MODULE_LIB_PATH ]; then
      #  將打包後的lib下的依賴全部複製到臨時的lib資料夾下
      \cp -r ${MODULE_LIB_PATH}/* ${MODULE_TMP_LIB_PATH}
      for LIB_JAR_FILE in ${MODULE_TMP_LIB_PATH}/*.jar
      do
        echo $LIB_JAR_FILE
        if [ -f $LIB_JAR_FILE ];then
          echo "jenkins校驗依賴Jar:"$LIB_JAR_FILE
          check_md5 $LIB_JAR_FILE
          if [ $? = 0 ];then
            echo "jenkins依賴lib校驗!成功,沒有發生變化"$LIB_JAR_FILE
          else
            echo "jenkins依賴lib校驗!失敗,已經更新"$LIB_JAR_FILE
          fi
        fi
      done
    fi
  • 判斷業務包

    
    MODULE_JAR=${MODULE_TMP_PATH}/${JAR_NAME}.jar
    
    echo "jenkins校驗專案Jar:"$MODULE_JAR
    check_md5 $MODULE_JAR
    if [ $? = 0 ];then
       echo "jenkins校驗成功,沒有發生變化"
    else
       echo "jenkins校驗失敗,已經更新"
    fi

SSH傳輸說明

前面的教程,SSH傳輸的都是各個專案target目錄下的jar,由於這裡,本地要做校驗,需要快取歷史的MD5值等訊息,就建立了臨時檔案 tmp,不再使用target(每次編譯都會被清空,無法快取);

因此這裡關於Jenkins的SSH傳輸配置就需要傳輸tmp目錄了,包括下面指令碼中使用的專案路徑,也是tmp目錄,不再使用target了

jenkins_restart_mini.sh

服務端檢測更新,重啟服務的指令碼;

地址:

校驗步驟:

  • 遍歷所有模組的tmp目錄
  • 校驗依賴Jar、業務Jar是否更新
  • 判斷程式是否存在
    • 已更新 / 程式不存在;重啟
    • 未更新跳過

構建測試:

Docker的方式

Docker方式和SSH的方式有比較大的差異,採用SSH的方式,一般是明確知道那些伺服器,然後直接上傳;但採用Docker,最終服務在那些機器上執行,就不一定了,比如使用了K8S;

那就意味著,服務所需的包、依賴,都必須打到Docker映象中,以方便容器啟動時使用;但這似乎又違背了本文的意圖,哪怕是隻有業務更新,也需要把所有的依賴新增到映象中去;

既然業務包也依賴包能拆分,業務(app)映象和依賴(lib)映象分開也就能解決這個問題了,如下圖:

  • 基礎依賴映象

    避免干擾,每個模組都又自己獨立的依賴映象;只有在依賴變更的情況下構建更新;是否需要推送的映象倉庫

  • 業務映象

    模組依賴更新/業務更新的時候,重新構建更新

示例專案:

目錄說明:

  • app

    儲存業務包以及構建映象用的Dockerfile

  • lib

    儲存業務所需的依賴包以及構建依賴映象的Dockerfile

  • docker-image-build.sh

    Jenkins 構建依賴映象、業務映象的指令碼

  • docker-image-pull.sh

    伺服器獲取最新映象的指令碼

  • docker-compose.yaml

    啟動容器的基礎配置檔案

Jenkins構建Docker映象並啟動的過程

  • 拉取最新程式碼

  • Maven打包

  • 執行 script/jenkins目錄下的 jenkins_docker_build.sh

    作用是遍歷所有模組中的 docker-image-build.sh並逐一執行

  • 執行模組下的 docker-image-build.sh

    • 校驗依賴jar是否更新

      a. 更新構建依賴基礎映象

      b. 未更新跳過

    • 檢驗業務並構建業務映象

    • 將映象推送到映象倉庫

  • script/jenkins指令碼以及模組下 docker目錄的指令碼上傳到伺服器

  • 執行 jenkins_restart_docker.sh

    • 執行 docker-image-pull.sh下載最新的映象
    • 執行 docker-compose.yaml啟動服務

依賴映象的Dockerfile

目錄在/docker/lib/Dockerfile



FROM openjdk:
8


# 同步時區
ENV TZ=Asia/Shanghai
RUN  ln -snf /usr/share/zoneinfo/ $TZ /etc/localtime &&  echo  $TZ > /etc/timezone

# 將lib目錄下的所有jar全部複製到映象裡面
ADD  ./*.jar /lib/

就是將所有的目錄包複製到映象中去;

構建映象:


docker build -t lib-jenjins-mini-build:latest ./docker/lib/.

最終會構建出一個名為 lib-jenjins-mini-build:latest的映象

業務映象的Dockerfile

目錄在/docker/app/Dockerfile



# 整合自依賴基礎映象

FROM lib-jenjins-mini-build:latest

# 將當前目錄下的jar複製到容器類
ADD  ./*.jar /app.jar

# 監聽埠
EXPOSE  18092

# 啟動
ENTRYPOINT  [ "java", "-Dloader.path=/lib", "-Djava.security.egd=file:/dev/./urandom" \
, "-XX:+UnlockExperimentalVMOptions", "-XX:+UseCGroupMemoryLimitForHeap" \
, "-jar""/app.jar" ]

  • FROM

    指明基礎映象為前面構建的依賴映象

  • ADD

    將業務jar包複製到容器中去

  • EXPOSE 設定監聽埠

  • ENTRYPOINT

    啟動服務,這裡務必要指定 -Dloader.path=/lib,其中 /lib就是前面基礎映象依賴儲存的地址,如果你有調整,這裡也需要跟著調整

docker-image-build.sh

用於構建基礎映象和業務映象的指令碼;

指令碼地址:https://github.com/vehang/ehang-spring-boot/blob/main/spring-boot-012-tools-jenkins-mini-build/docker/docker-image-build.sh

  • 公共方法

    以下的三個校驗方法屬於公共方法,和前面介紹的一樣

    jar_check_md5 透過jar的md5值直接檢測

    jar_unzip_check_md5 透過對jar包解壓 校驗檔案詳情的MD5

    check_md5 彙總上面兩個方法的校驗

  • 準備jar

    將業務jar包複製到 docker/app目錄

    將依賴Jar複製到 docker/lib目錄

    
    MODULE_LIB_PATH=${MODULE_BATH_PATH}/docker/lib
    
    MODULE_APP_PATH=${MODULE_BATH_PATH}/docker/app

    \cp -r ${MODULE_BATH_PATH}/target/*.jar ${MODULE_APP_PATH}
    \cp -r ${MODULE_BATH_PATH}/target/lib/*.jar ${MODULE_LIB_PATH}
  • 校驗並構建依賴映象

    檢驗依賴包是否更新,如果更新,構建基礎的依賴映象;

    
    LIB_UPDATE=false
    
    for LIB_JAR_FILE in ${MODULE_LIB_PATH}/*.jar
    do
      echo $LIB_JAR_FILE
      if [ -f $LIB_JAR_FILE ];then
        echo "Jenkins Docker映象構建校驗lib 依賴Jar:"$LIB_JAR_FILE
        check_md5 $LIB_JAR_FILE
        if [ $? = 0 ];then
          echo "Jenkins Docker映象構建校驗lib!成功,沒有發生變化"$LIB_JAR_FILE
        else
          LIB_UPDATE=true
          echo "Jenkins Docker映象構建校驗lib!失敗,已經更新"$LIB_JAR_FILE
        fi
      fi
    done
    #  一旦發現lib有變化,就構建新的lib映象
    if [ $LIB_UPDATE = true ]; then
      docker build -t ${MODULE_DOCKER_LIB_IMAGE_NAME}:latest ${MODULE_LIB_PATH}/.
    fi
  • 校驗業務jar並構建映象

    校驗業務包、依賴包是否更新 if [ $APP_UPDATE = true ] || [ $LIB_UPDATE = true ];更新就重新構建映象;

    
    APP_UPDATE=false
    
    for APP_JAR_FILE in ${MODULE_APP_PATH}/*.jar
    do
      echo $APP_JAR_FILE
      if [ -f $APP_JAR_FILE ];then
        echo "Jenkins Docker映象構建校驗APP 依賴Jar:"$APP_JAR_FILE
        check_md5 $APP_JAR_FILE
        if [ $? = 0 ];then
          echo "Jenkins Docker映象構建校驗APP!成功,沒有發生變化"$APP_JAR_FILE
        else
          APP_UPDATE=true
          echo "Jenkins Docker映象構建校驗APP!失敗,已經更新"$APP_JAR_FILE
        fi
      fi
    done
    #  一旦發現lib有變化,或者APP發生變化 都需要構建新的映象
    if [ $APP_UPDATE = true ] || [ $LIB_UPDATE = true ]; then
      #  構建映象
      docker build -t registry.cn-guangzhou.aliyuncs.com/ehang_jenkins/${MODULE_DOCKER_IMAGE_NAME}:latest ${MODULE_APP_PATH}/.
      #  將映象推送到阿里雲
      docker push registry.cn-guangzhou.aliyuncs.com/ehang_jenkins/${MODULE_DOCKER_IMAGE_NAME}:latest
    fi

構建測試:

再回看一下部署過程,發現曾經最耗時的部分,現在一下變的絲滑好多...

文中有任何的問題或者疑問,歡迎隨時微信(mbb2100)交流;感謝您的點贊、關注!


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70035356/viewspace-2996024/,如需轉載,請註明出處,否則將追究法律責任。

相關文章