在 Spring Boot 中优雅地打印 JAR 构建时间、版本号和 Git 提交信息

2026-01-01 71点热度 0人点赞 0条评论

线上排查问题时,常见的几个灵魂拷问:

  • “这台机器上的 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-plugingitCommit / 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:当前这次应用启动的时间

对比多次部署 / 多台机器时,只需看这一段日志,就能迅速判断:

  • 是否跑在同一个构建产物上?
  • 具体是哪次提交?
  • 构建时间是否符合预期?

admin

这个人很懒,什么都没留下

文章评论

您需要 登录 之后才可以评论