当我们在生产环境部署了 SpringBoot
应用的时候,虽然可以通过 Jenkins
的构建状态和 Linux
的 ps
命令去感知应用是否在新的一次发布中部署和启动成功,但是这种监控手段是运维层面的。那么,可以提供一种手段能够在应用层面感知服务在新的一次发布中的构建部署和启动是否成功吗?这个问题笔者花了一点时间想通了这个问题,通过这篇文章提供一个简单的实现思路。
其实基本思路很简单,一般 SpringBoot
应用会使用 Maven
插件打包(笔者不熟悉 Gradle
,所以暂时不对 Gradle
做分析),所以可以这样考虑:
Maven
插件打包的时候,把 构建时间
和 pom
文件中的版本号都写到 jar
包的描述文件中,正确来说就是 MANIFEST.MF
文件中。 spring-boot-starter-actuator
,通过 /actuator/info
端点去暴露应用的信息(最好控制网络访问权限为只允许内网访问)。 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)