SpringBoot打包部署最佳實踐

spring boot介紹

Spring Boot目前流行的java web應用開發框架,相比傳統的spring開發,spring boot極大簡化了配置,而且遵照約定優於配置的原則即便0配置也能正常運行,這在spring中是不可思議的。spring boot應用程序能夠獨立運行,框架內嵌web容器,使得web應用程序能夠像本地程序同樣啓動和調試,十分的方便,這種設計方式也使得spring boot應用程序很是適合容器化進行大規模部署。生態方面,spring boot提供了很是豐富的組件,目前流行的java web框架基本都有spring boot版本,生態十分龐大,是目前java web開發最好的方案。java

spring boot部署問題

Springboot應用程序有兩種運行方式python

  • 以jar包方式運行
  • 以war包方式運行

兩種方式應用場景不同,各有優缺點git

jar包運行

經過maven插件spring-boot-maven-plugin,在進行打包時,會動態生成jar的啓動類org.springframework.boot.loader.JarLauncher,藉助該類對springboot應用程序進行啓動。web

優勢

  • 本地無需搭建web容器,方便開發和調試。
  • 由於自帶web容器,能夠避免因爲web容器的差別形成不一樣環境結果不一致問題。
  • 一個jar包就是所有,方便應用擴展。
  • 藉助容器化,能夠進行大規模的部署。

缺點

  • 應用過於獨立,難以統一管理。
  • 數據源沒法經過界面進行管理。
  • 應用體積過大。
  • 修改web容器相關配置較爲困難,須要藉助代碼實現。

war包運行

以war包方式運行,經過maven插件spring-boot-maven-plugin進行相關配置後,最終生成一個可運行在tomcat,weblogic等java web容器中的war包。spring

優勢

  • 能夠藉助web容器管理界面對應用進行管理。
  • 能夠管理JNDI數據源。
  • web容器配置較爲靈活,配置和程序分離。
  • 應用體積較小,甚至能夠藉助web容器的包管理功能(好比weblogic Library)進一步減少應用大小。

缺點

  • 本地須要搭建web容器,對本地環境要求更高點,學習成本也響應更高。
  • 調試較爲困難,須要藉助web容器。
  • 沒法兼容全部web容器(好比spring boot2.x沒法運行在weblogic 11g上)。
  • 部署較爲困難(好比和weblogic有較多的類衝突)

在實際的項目中,並無哪種方式是最好的,根據客戶不一樣的需求制定不一樣的部署方案,好比有些客戶比較看中管理功能,要求數據源和tomcat相關配置必須由管理員進行管理,那麼選擇war包方式,有些客戶但願藉助容器化進行大規模部署,那麼jar方式更適合。無論選擇哪一種方式,在部署時都會遇到下面的問題docker

  • 若是須要打war包,那麼不只是pom文件須要修改,應用程序也要作相應的改動,改動完後,應用程序就沒法本地運行,須要打完包後將配置信息修改回來,這樣不只麻煩,還容易出錯。
  • 不論是war包仍是jar包,如何管理不一樣環境的配置文件,保證不會出錯,雖然spring boot有提供spring.profiles.active配置設置不一樣的環境,但一方面須要人爲修改配置文件,只要是人爲的就有可能出錯,另外一方面,客戶有時出於安全考慮不會提供生產環境配置信息,那麼這時候就沒法指定prifiles.active
  • 如何將多個spring boot模塊打包在一塊兒。
  • jar包須要配合容器化才能發揮出最大的優點,若是沒有容器,spring boot jar包就是一個玩具,隨處運行的jar包,缺乏統一管理,是達不到生產的要求,那麼若是從jar包到容器也是一個問題。

早期碰到這些問題,都是人工解決,不只效率十分低下,部署一次都須要十幾分鍾,並且很容易出錯,一百次出錯一次算是機率低了,可是生產出錯一次都是重大事件,因此咱們也在思考如何經過自動化解決以上問題,如何將開發和部署分離,開發人員只關心開發,開發完提交代碼,打包和部署都是後臺透明的完成。如下就是咱們的解決方案。shell

打包

war包打包問題解決

spring boot打war包的步驟以下apache

  • pom.xml中將打包方式改成war。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    ...
    <packaging>war</packaging>
    ...
</project>
  • 設置spring-boot-starter-tomcat範圍爲provided
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>
  • 修改spring boot的啓動類,繼承SpringBootServletInitializer
public class DemoApplication extends SpringBootServletInitializer{
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(DemoApplication.class);
    }
}

每打包一次都要修改pom.xml和啓動類,打包完再修改回來,十分的繁瑣,由於,咱們提出如下整改方案tomcat

  • pom.xml複製一個pom-war.xml文件,將pom-war.xml修改成war包配置
  • 在根目錄下(除了src目錄外均可以)複製一份啓動類的代碼,修改成war包的配置方式。
  • 編寫shell腳本進行打包。

shell腳本打包過程爲安全

  1. 備份當前啓動類的java代碼。
  2. 將war包啓動類的代碼替換掉當前啓動類的代碼。
  3. maven指定pom-war.xml文件進行打包。
  4. 打包結束後恢復啓動類文件。

如下就是參考腳本

app-war.sh

#!/usr/bin/env bash

v1=src/main/java/com/definesys/demo/DemoApplication.java
v2=war/DemoApplication.java
v3=war/DemoApplication-bak.java

cp -rf $v2 $v1

mvn clean package -Dmaven.test.skip=true -f war-pom.xml

#recovery
cp -rf $v3 $v1

經過預先配置好pom文件和啓動類文件,開發人員只要運行app-war.sh腳本無需修改任何文件便可生成war包。

更優的方案

以上方案pom文件和啓動類文件都須要預先準備好,未實現徹底的自動化,經過優化方案作到徹底自動化。

  • 腳本能夠經過find命令搜索以*Application.java結尾的文件,做爲啓動類文件,讀取文件名獲取類名,經過字符串替換方式動態生成war包啓動類文件。
  • 在pom.xml中用註釋設置好錨點,腳本經過替換錨點動態生成pom.xml文件。
  • 若是不但願經過錨點實現,能夠藉助更高級的腳本語言,好比python對xml進行解析,再動態生成xml。

多模塊打包

這裏的多模塊指的是maven中的多模塊,項目工程中的代碼多模塊,一個項目按功能劃分模塊後,在建立工程時通常也按照功能層面上的模塊進行建立,這樣避免一個模塊代碼過於龐大,也利於任務的分工,但打包卻更麻煩了。

  • 每一個模塊都是獨立的spring boot程序,整合到一個包的時候會出現多個啓動類,多個配置文件衝突的問題。
  • 每一個模塊有引用相同的依賴,依賴包版本升級後,須要每一個pom文件都作修改。

經過優化項目結構解決以上問題

  • 父項目的pom指定spring boot的依賴和公共的依賴。
  • 建立一個spring boot的子項目,做爲啓動項目,咱們稱爲start項目。
  • 其他子項目爲普通的java maven項目,parent設置爲第一步建立的spring boot父項目。
  • start項目的pom引用其餘子項目的依賴。
  • 本地調試能夠直接運行start的啓動類,ide會自動編譯其餘模塊並引用。
  • 打包能夠在父項目上進行install後再進入start項目進行打包,腳本參考以下
mvn clean install
cd start
mvn clean package

目錄結構以下

.
├── pom.xml
├── role
│   ├── pom.xml
│   └── src
│       ├── main
│       │   ├── java
│       │   │   └── com
│       │   │       └── definesys
│       │   │           └── demo
│       │   │               └── controller
│       │   │                   └── RoleController.java
│       │   └── resources
├── start
│   ├── pom.xml
│   ├── src
│   │   ├── main
│   │   │   ├── java
│   │   │   │   └── com
│   │   │   │       └── definesys
│   │   │   │           └── demo
│   │   │   │               └── DemoApplication.java
│   │   │   └── resources
│   │   │       └── application.properties
└── user
    ├── pom.xml
    └── src
        └── main
            ├── java
            │   └── com
            │       └── definesys
            │           └── demo
            │               └── controller
            │                   └── UserController.java
            └── resources
  • start項目包含包含啓動類和配置文件,pom文件引用其他子項目。

start pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>blog0915</artifactId>
        <groupId>com.definesys.demo</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>start</artifactId>
    <dependencies>
        <dependency>
            <groupId>com.definesys.demo</groupId>
            <artifactId>user</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.definesys.demo</groupId>
            <artifactId>role</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  • 父項目parent爲spring boot,引用spring boot相關依賴和各個子項目公共的依賴

父項目 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <packaging>pom</packaging>
    <modules>
        <module>user</module>
        <module>role</module>
        <module>start</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.2.RELEASE</version>
        <relativePath/>
    </parent>

    <groupId>com.definesys.demo</groupId>
    <artifactId>blog0915</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

</project>
  • 全部非start的子項目須要指定版本號而且父項目都設爲根目錄項目。

子項目 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>blog0915</artifactId>
        <groupId>com.definesys.demo</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>role</artifactId>
    <version>1.0.0</version>
</project>
  • 全部子項目的包路徑前綴必須同樣,而且以start項目做爲基本路徑。

配置文件問題

spring boot提供spring.profiles.active指定配置文件,但生產環境有時候客戶出於安全考慮不提供配置信息給開發人員,而是預先將配置文件上傳到服務器指定路徑,程序須要在運行時去引用該配置文件,若是運行環境是kubernetes,則會提供一個config map做爲配置文件,這時候就要求spring boot程序讀取外部配置文件。

這裏討論的是線上環境配置文件方案,本地調試參考子模塊打包相關內容,能夠將配置文件統一寫在start項目中。

jar包外部配置文件讀取

jar運行能夠經過指定參數spring.config.location引用外部文件,命令參考以下:

java -jar start-1.0-SNAPSHOT.jar --spring.config.location=/Users/asan/workspace/config

config目錄存放properties配置文件

能夠經過配合spring.profiles.active參數能夠指定目錄下配置文件,如:

java -jar start-1.0-SNAPSHOT.jar --spring.profiles.active=prod --spring.config.location=/Users/asan/workspace/config

則會讀取/Users/asan/workspace/config/appliction-prod.properties文件做爲配置文件。

war包外部配置文件讀取

tomcat爲例,須要在tomcat啓動時指定-Dspring.config.location參數,能夠設置服務器環境變量CATALINA_OPTS達到目的。能夠編輯用戶 prifile文件

export CATALINA_OPTS=/Users/asan/workspace/config

一樣,也能夠經過-Dspring.profiles.active指定配置文件名稱。

容器化

spring boot藉助容器化,能夠如虎添翼,發揮出更大的威力,也只有經過容器化,才能體會到spring boot開發的高效。經過以上的介紹,你能夠很順利的打好一個jar包或者war包,那麼能夠經過編寫dockerfile文件進行鏡像的構建。spring boot在構建鏡像時有兩個地方須要考慮

  • 時區問題,基礎鏡像的時區默認是UTC,比北京時間早8小時,須要指定鏡像時區。
  • 配置文件問題,須要指定外部配置文件(根據項目具體狀況選擇)。

app-jar-dockerfile.Dockerfile

FROM openjdk:8-jdk-alpine

MAINTAINER definesys.com

VOLUME /tmp
ADD start-1.0-SNAPSHOT.jar app.jar
RUN echo "Asia/Shanghai" > /etc/timezone
EXPOSE 8080
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","--spring.config.location=/Users/asan/workspace/config","/app.jar"]

app-war.dockerfile.Dockerfile

FROM tomcat

MAINTAINER definesys.com

ENV CATALINA_OPTS -Dspring.config.location=file:/middleware/config/
ADD start-1.0-SNAPSHOT.war /usr/local/tomcat/webapps/app.jar
RUN echo "Asia/Shanghai" > /etc/timezone
EXPOSE 8080
EOF

部署

早期咱們採用的是如下部署過程

  • 首先構建測試環境的鏡像,上傳到鏡像倉庫,應用從新部署。
  • 接着構建UAT環境的鏡像,上傳到鏡像倉庫,應用從新部署。
  • 最後構建生產環境的鏡像,上傳到鏡像倉庫,應用從新部署。

每一次發佈都是一個新的鏡像,但這種方式有個問題就是如何保證前一個環境驗證沒問題,後一個環境就必定沒問題,由於兩個鏡像是不同的,雖然可能兩次構建都是基於同一版本代碼,但由於是從新構建,中間可能由於各類緣由,如maven包版本更新等,沒法保證兩次構建就是徹底同樣的鏡像。所以咱們優化了構建的流程,以下:

全部的環境都是用同一個鏡像,環境之間只有配置文件不一樣,文件經過configmap或者外部配置文件方式進行掛載,這樣保證了配置文件沒問題的前提下,每一個環境的程序必定是同樣的。

jenkins自動打包部署

打包和部署在本地進行也是有問題的,本地jdk版本取決於我的電腦,甚至有黑客污染jdk致使編譯的class文件自帶後門,我的電腦環境也是隨着用戶不一樣操做可能改變,構建出來的包不能保證是穩定的包。所以須要一個遠程服務器用於打包和部署,可以實現從源碼到鏡像過程。jenkins是一個基於java開發的持續集成工具,經過配置插件和編寫腳本實現程序從代碼到製品再到線上運行的過程。jenkins在spring boot開發中主要完成了如下工做。

  • 經過gitlab插件實現源代碼的獲取。
  • 基於以上介紹的腳本,實現從源碼到製品的過程。
  • 經過docker工具實現從製品到鏡像的過程。
  • 經過kubectl工具,實現從鏡像到上雲的過程。

jenkins在構建鏡像時須要藉助docker工具,但jenkins自己也是有docker版本的,因此就面臨着docker in docker的問題,這裏選擇的方案是用二進制文件安裝jenkin而非鏡像方式,雖然喪失了docker的便利性,但能夠簡化docker方案,下降集成的複雜度。

相關文章
相關標籤/搜索