1. 程式人生 > >你的SpringBoot應用真的部署更新成功了嗎

你的SpringBoot應用真的部署更新成功了嗎

前提

當我們在生產環境部署了SpringBoot應用的時候,雖然可以通過Jenkins的構建狀態和Linuxps命令去感知應用是否在新的一次釋出中部署和啟動成功,但是這種監控手段是運維層面的。那麼,可以提供一種手段能夠在應用層面感知服務在新的一次釋出中的構建部署和啟動是否成功嗎?這個問題筆者花了一點時間想通了這個問題,通過這篇文章提供一個簡單的實現思路。

基本思路

其實基本思路很簡單,一般SpringBoot應用會使用Maven外掛打包(筆者不熟悉Gradle,所以暫時不對Gradle做分析),所以可以這樣考慮:

  1. Maven外掛打包的時候,把構建時間和pom檔案中的版本號都寫到jar包的描述檔案中,正確來說就是MANIFEST.MF
    檔案中。
  2. 引入spring-boot-starter-actuator,通過/actuator/info端點去暴露應用的資訊(最好控制網路訪問許可權為只允許內網訪問)。
  3. 把第1步中打包到jar包中的MANIFEST.MF檔案的內容讀取並且載入到SpringBoot環境屬性中的info.*屬性中,以便可以通過/actuator/info訪問。

思路定好了,那麼下面開始實施編碼。

編碼實現

最近剛好在調研螞蟻金服的SofaStack體系,這裡引入SofaBoot編寫示例。pom檔案如下:

<?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>
    <groupId>club.throwable</groupId>
    <artifactId>sofa-boot-sample</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>sofa-boot-sample</name>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <sofa.boot.version>3.2.0</sofa.boot.version>
        <spring.boot.version>2.1.0.RELEASE</spring.boot.version>
        <maven.build.timestamp.format>yyyy-MM-dd HH:mm:ss.SSS</maven.build.timestamp.format>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>com.alipay.sofa</groupId>
                <artifactId>sofaboot-dependencies</artifactId>
                <version>${sofa.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.alipay.sofa</groupId>
            <artifactId>healthcheck-sofa-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

    <build>
        <finalName>sofa-boot-sample</finalName>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring.boot.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addBuildEnvironmentEntries>true</addBuildEnvironmentEntries>
                        </manifest>
                        <manifestEntries>
                            <Application-Name>${project.groupId}:${project.artifactId}:${project.version}</Application-Name>
                            <Build-Timestamp>${maven.build.timestamp}</Build-Timestamp>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

pom檔案中一些屬性和佔位符的設定,可以參考一下這兩個連結:Maven-Archiver和Available Variables。SpringBoot的配置檔案application.yaml如下:

server:
  port: 9091
management:
  server:
    port: 10091
  endpoints:
    enabled-by-default: false
    web:
      exposure:
        include: info
  endpoint:
    info:
      enabled: true
spring:
  application:
    name: sofa-boot-sample

這裡要注意一點:SpringBoot應用通過其Maven外掛打出來的jar包解壓後的目錄如下:

sofa-boot-sample.jar
  - META-INF
    - MANIFEST.MF
    - maven ...
  - org
    - springframework 
      - boot ...
  - BOOT-INF
    - classes ...
    - lib ...    

瞭解此解壓目錄是我們編寫MANIFEST.MF檔案的解析實現過程的前提。編寫MANIFEST.MF檔案的解析類:

@SuppressWarnings("ConstantConditions")
public enum ManiFestFileExtractUtils {


    /**
     * 單例
     */
    X;

    private static Map<String, String> RESULT = new HashMap<>(16);
    private static final Logger LOGGER = LoggerFactory.getLogger(ManiFestFileExtractUtils.class);

    static {
        String jarFilePath = ClassUtils.getDefaultClassLoader().getResource("").getPath().replace("!/BOOT-INF/classes!/", "");
        if (jarFilePath.startsWith("file")) {
            jarFilePath = jarFilePath.substring(5);
        }
        LOGGER.info("讀取的Jar路徑為:{}", jarFilePath);
        try (JarFile jarFile = new JarFile(jarFilePath)) {
            JarEntry entry = jarFile.getJarEntry("META-INF/MANIFEST.MF");
            if (null != entry) {
                BufferedReader reader = new BufferedReader(new InputStreamReader(jarFile.getInputStream(entry), StandardCharsets.UTF_8));
                String line;
                while (null != (line = reader.readLine())) {
                    LOGGER.info("讀取到行:{}", line);
                    int i = line.indexOf(":");
                    if (i > -1) {
                        String key = line.substring(0, i).trim();
                        String value = line.substring(i + 1).trim();
                        RESULT.put(key, value);
                    }
                }
            }

        } catch (Exception e) {
            LOGGER.warn("解析MANIFEST.MF檔案異常", e);
        }
    }

    public Map<String, String> extract() {
        return RESULT;
    }
}

可以通過一個CommandLineRunner的實現把MANIFEST.MF檔案的內容寫到Environment例項中:

@Component
public class SofaBootSampleRunner implements CommandLineRunner {

    @Autowired
    private ConfigurableEnvironment configurableEnvironment;

    @Override
    public void run(String... args) throws Exception {
        MutablePropertySources propertySources = configurableEnvironment.getPropertySources();
        Map<String, String> result = ManiFestFileExtractUtils.X.extract();
        Properties properties = new Properties();
        for (Map.Entry<String, String> entry : result.entrySet()) {
            String key = "info." + entry.getKey();
            properties.setProperty(key, entry.getValue());
        }
        if (!properties.isEmpty()) {
            propertySources.addFirst(new PropertiesPropertySource("infoProperties", properties));
        }
    }
}

啟動類如下:

@SpringBootApplication
public class SofaBootSampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(SofaBootSampleApplication.class, args);
    }
}

最終效果

在專案的根目錄使用命令mvn package,打出jar包後直接啟動:

cd Jar包的目錄
java -jar sofa-boot-sample.jar

呼叫http://localhost:10091/actuator/info介面輸出如下:

{
    "Spring-Boot-Version": "2.1.0.RELEASE",
    "Start-Class": "club.throwable.sofa.SofaBootSampleApplication",
    "Main-Class": "org.springframework.boot.loader.JarLauncher",
    "Manifest-Version": "1.0",
    "Build-Jdk-Spec": "1.8",
    "Spring-Boot-Classes": "BOOT-INF/classes/",
    "Created-By": "Maven Jar Plugin 3.2.0",
    "Build-Timestamp": "2019-12-08 17:41:21.844",
    "Spring-Boot-Lib": "BOOT-INF/lib/",
    "Application-Name": "club.throwable:sofa-boot-sample:1.0-SNAPSHOT"
}

改變pom檔案中的版本標籤<version>1.0.0,再次打包並且啟動成功後呼叫http://localhost:10091/actuator/info介面輸出如下:

{
    "Spring-Boot-Version": "2.1.0.RELEASE",
    "Start-Class": "club.throwable.sofa.SofaBootSampleApplication",
    "Main-Class": "org.springframework.boot.loader.JarLauncher",
    "Manifest-Version": "1.0",
    "Build-Jdk-Spec": "1.8",
    "Spring-Boot-Classes": "BOOT-INF/classes/",
    "Created-By": "Maven Jar Plugin 3.2.0",
    "Build-Timestamp": "2019-12-08 17:42:07.273",
    "Spring-Boot-Lib": "BOOT-INF/lib/",
    "Application-Name": "club.throwable:sofa-boot-sample:1.0.0"
}

可見構建時間戳Build-Timestamp和服務名Application-Name都發生了變化,達到了監控服務是否正常部署和啟動的目的。如果有多個服務節點,可以新增一個ip屬性加以區分。

小結

這篇文章通過SpringBoot一些實用技巧實現了應用層面監控應用是否正常打包部署更新和啟動成功的問題。

原文連結

  • Github Page:http://throwable.club/2019/12/09/spring-boot-server-deploy-monitor
  • Coding Page:http://throwable.coding.me/2019/12/09/spring-boot-server-deploy-monitor

(本文完 e-a-20191209:1:39 c-1-d