前提

当我们在生产环境部署了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-ArchiverAvailable VariablesSpringBoot的配置文件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)