线上排查问题时,常见的几个灵魂拷问:
- “这台机器上的 JAR 到底是不是最新的?”
- “现在跑的是哪个版本?”
- “这个版本对应哪次 Git 提交?”
如果我们能在应用启动时,就统一打印出“构建时间 + 版本号 + Git commit 信息”,排查问题会轻松很多。
本文基于 Spring Boot 的能力,给出一个比较优雅的实现方案:
- 利用
spring-boot-maven-plugin生成 build-info - 可选地利用
git-commit-id-plugin写入 Git 提交信息 - 启动时从类路径里读取
META-INF/build-info.properties - 将 UTC 构建时间转换为服务器本地时区时间 并格式化输出
一、用文件最后修改时间有什么问题?
最直观的做法是:运行时拿到当前 JAR 文件,用 File.lastModified() 或 Files.getLastModifiedTime 得到时间戳。但这种方式有明显缺陷:
- 这是文件在当前机器上的修改时间,不一定等于“构建时间”
- 部署系统可能解压、重新打包,时间戳会变化
- 跨机器拷贝、上传时,某些工具也会改写文件时间
因此,更可靠的方式是:在构建阶段就把元信息写入 JAR 内部,运行时只做读取和展示。
二、使用 Spring Boot 的 build-info 功能
Spring Boot 的 spring-boot-maven-plugin 内置了 build-info 目标,执行后会在 JAR 中生成一个:
META-INF/build-info.properties
示例内容:
build.artifact=demo-app build.group=com.example build.name=demo-app build.version=0.0.1-SNAPSHOT build.time=2025-12-12T04:28:13.013Z
其中:
build.version:构建版本号(来源于 pom.xml)build.time:JAR 构建时间(ISO-8601,UTC)
2.1 Maven 配置:开启 build-info 生成
在 pom.xml 中,为 spring-boot-maven-plugin 增加 build-info 执行:
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build-info</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
之后只要执行:
mvn clean package生成的 Spring Boot JAR 里就会自动包含 META-INF/build-info.properties 文件。
三、(可选)集成 Git commit 信息
版本号能帮我们区分大版本,但在频繁迭代和 CI 场景下,直接看到 Git commit hash 更直观。
可以使用广泛应用的 git-commit-id-plugin:
3.1 在 pom.xml 中配置插件
<build>
<plugins>
<!-- 省略其他插件 -->
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
<version>4.0.4</version>
<executions>
<execution>
<goals>
<goal>revision</goal>
</goals>
</execution>
</executions>
<configuration>
<!-- 只举例常用配置,可按需增减 -->
<includeOnlyProperties>
<includeOnlyProperty>git.commit.id.abbrev</includeOnlyProperty>
<includeOnlyProperty>git.branch</includeOnlyProperty>
</includeOnlyProperties>
<verbose>false</verbose>
</configuration>
</plugin>
</plugins>
</build>
构建后会生成一个类似:
target/classes/git.properties
内容示例:
git.branch=main
git.commit.id.abbrev=abc1234提示:
- 这个插件版本号可以根据你项目使用的 Maven / JDK 版本适当调整
- 如果你不想增加新的插件,也可以通过 CI 把 Git 信息写入一个自定义的
build-info或属性文件,思路类似。
四、启动时统一打印构建信息
接下来,我们在应用启动完成时读取这些信息,并以统一格式打到日志里。
4.1 启动监听器:读取 build-info + git.properties
示例代码(使用 ApplicationListener<ApplicationReadyEvent>):
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Properties;
@Component
@Slf4j
public class ApplicationStartupListener implements ApplicationListener<ApplicationReadyEvent> {
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
String buildTime = getBuildTimeLocal();
String buildVersion = getBuildVersion();
String gitCommit = getGitCommitIdAbbrev();
String gitBranch = getGitBranch();
String startTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
String info = "\n----------------------------------------------------------\n\t" +
"Application is running!\n\t" +
"Build Version: " + buildVersion + "\n\t" +
"Jar Build Time: " + buildTime + "\n\t" +
(gitCommit != null ? "Git Commit: " + gitCommit + "\n\t" : "") +
(gitBranch != null ? "Git Branch: " + gitBranch + "\n\t" : "") +
"Start Time: " + startTime + "\n" +
"----------------------------------------------------------";
log.warn(info);
}
/**
* 从 META-INF/build-info.properties 中读取 build.time,
* 并转换为服务器本地时区的 yyyy-MM-dd HH:mm:ss 格式。
*/
private String getBuildTimeLocal() {
ClassPathResource resource = new ClassPathResource("META-INF/build-info.properties");
if (!resource.exists()) {
return "N/A";
}
Properties properties = new Properties();
try (InputStream inputStream = resource.getInputStream()) {
properties.load(inputStream);
String buildTime = properties.getProperty("build.time");
if (buildTime == null || buildTime.isEmpty()) {
return "N/A";
}
try {
// build.time 是 ISO-8601 UTC,例如:2025-12-12T04:28:13.013Z
Instant instant = Instant.parse(buildTime);
ZonedDateTime zonedDateTime = instant.atZone(ZoneId.systemDefault());
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
return formatter.format(zonedDateTime);
} catch (Exception e) {
// 解析失败时降级为原始字符串
return buildTime;
}
} catch (IOException e) {
log.warn("Failed to read build-info.properties for build time", e);
return "N/A";
}
}
/**
* 从 build-info.properties 中读取 build.version。
*/
private String getBuildVersion() {
ClassPathResource resource = new ClassPathResource("META-INF/build-info.properties");
if (!resource.exists()) {
return "N/A";
}
Properties properties = new Properties();
try (InputStream inputStream = resource.getInputStream()) {
properties.load(inputStream);
String version = properties.getProperty("build.version");
return (version == null || version.isEmpty()) ? "N/A" : version;
} catch (IOException e) {
log.warn("Failed to read build-info.properties for build version", e);
return "N/A";
}
}
/**
* 从 git.properties 中读取简短 commit id(可选)。
*/
private String getGitCommitIdAbbrev() {
ClassPathResource resource = new ClassPathResource("git.properties");
if (!resource.exists()) {
return null;
}
Properties properties = new Properties();
try (InputStream inputStream = resource.getInputStream()) {
properties.load(inputStream);
String commitId = properties.getProperty("git.commit.id.abbrev");
return (commitId == null || commitId.isEmpty()) ? null : commitId;
} catch (IOException e) {
log.warn("Failed to read git.properties for commit id", e);
return null;
}
}
/**
* 从 git.properties 中读取当前分支名(可选)。
*/
private String getGitBranch() {
ClassPathResource resource = new ClassPathResource("git.properties");
if (!resource.exists()) {
return null;
}
Properties properties = new Properties();
try (InputStream inputStream = resource.getInputStream()) {
properties.load(inputStream);
String branch = properties.getProperty("git.branch");
return (branch == null || branch.isEmpty()) ? null : branch;
} catch (IOException e) {
log.warn("Failed to read git.properties for branch", e);
return null;
}
}
}
几点说明:
- 构建时间:先从
build.time解析为Instant,再使用ZoneId.systemDefault()转为服务器本地时区,最后格式化成yyyy-MM-dd HH:mm:ss。 - 版本号:直接从
build.version读取,通常对应 pom.xml 中的<version>。 - Git 信息:完全可选,如果没有集成
git-commit-id-plugin,gitCommit/gitBranch会是null,不会打印对应行。
五、实际效果示例
部署到服务器并通过 JAR 启动后,日志中会看到类似信息:
----------------------------------------------------------
Application is running!
Build Version: 1.2.3
Jar Build Time: 2025-12-12 12:28:13
Git Commit: abc1234
Git Branch: main
Start Time: 2025-12-12 12:30:45
----------------------------------------------------------
解释一下:
Build Version:当前运行包的版本号Jar Build Time:JAR 构建完成时间(已转换成服务器本地时区)Git Commit:对应 Git 仓库的某次提交(简短 hash)Git Branch:构建时所在分支Start Time:当前这次应用启动的时间
对比多次部署 / 多台机器时,只需看这一段日志,就能迅速判断:
- 是否跑在同一个构建产物上?
- 具体是哪次提交?
- 构建时间是否符合预期?
文章评论