转载

你的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 一些实用技巧实现了应用层面监控应用是否正常打包部署更新和启动成功的问题。

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

原文  http://www.throwable.club/2019/12/09/spring-boot-server-deploy-monitor/
正文到此结束
Loading...