Spring Boot Web应用如何读取项目特定META/MANIFEST.MF文件
问题根因
原有代码无法正常读取MANIFEST.MF、始终返回null,核心原因是逻辑只适配了Spring Boot可执行jar包直接通过java -jar启动的场景,完全不兼容war包部署到外部Tomcat的运行模式:
- 代码中通过类的
ProtectionDomain拼接MANIFEST文件路径的逻辑,在Tomcat中拿到的是解压后WEB-INF/classes/目录的路径,而war包的MANIFEST.MF存放在war包根目录的META-INF/路径下,拼接出的目标路径本身就是错的 - 用字符串完全相等的方式匹配MANIFEST文件URL的逻辑过于死板,Tomcat返回的资源URL会带有
war:file:、!/等特殊前缀和分隔符,永远无法匹配到你拼接的路径字符串 - 所有IO异常和读取异常都被静默吞掉,没有任何错误日志,完全无法定位具体失败点
- 遍历类路径下所有MANIFEST.MF的逻辑会扫描到所有依赖jar包的清单文件,效率低还容易匹配错误目标
解决方案
你可以二选一,优先选Spring Boot官方提供的构建信息读取方案,兼容性最好,不需要自己处理文件读取逻辑。
方案1:使用Spring Boot官方BuildProperties(推荐)
这个方案不依赖手动读取MANIFEST.MF,Spring Boot会自动生成构建信息属性文件,jar、war部署场景都能兼容。
- 首先在Gradle配置中开启buildInfo任务,自动生成构建信息:
// build.gradle中添加配置 plugins { id 'org.springframework.boot' version '你当前使用的Spring Boot版本' // 其他原有插件 } springBoot { buildInfo { // 自定义你需要的属性,比如构建号 properties { buildNumber = project.findProperty("buildNum") ?: "dev" version = project.version } } }
- 直接在Controller中注入
BuildProperties即可使用,不需要自己定义Manifest的Bean:
import org.springframework.boot.info.BuildProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class AppController { @Autowired private Environment env; @Autowired private BuildProperties buildProperties; @GetMapping("/get-app-details") public String getAppDetails() { String buildNum = buildProperties.getBuildNumber(); // 兼容原有构建号截取逻辑 if (buildNum != null && buildNum.contains("_")) { buildNum = buildNum.substring(buildNum.lastIndexOf('_') + 1); } String appVersion = env.getProperty("App.Version", "unknown") + "." + buildNum; return "Build Number - " + buildNum + ", AppVersion - " + appVersion; } }
方案2:修正原有MANIFEST.MF读取逻辑,适配war部署
如果你必须读取MANIFEST.MF文件,不要用类路径拼接+遍历匹配的逻辑,直接通过ServletContext读取web应用根目录下的清单文件,兼容Tomcat等外部容器部署场景:
- 先确认Gradle的war任务配置正确,确保你需要的属性已经写入MANIFEST.MF:
war { manifest { attributes( 'Build-Number': project.findProperty("buildNum") ?: "dev", 'App-Version': project.version ) } }
- 修正AppConfig中的Manifest Bean定义:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.context.ServletContextAware; import javax.servlet.ServletContext; import java.io.InputStream; import java.util.jar.Manifest; @Configuration public class AppConfig implements ServletContextAware { private ServletContext servletContext; @Override public void setServletContext(ServletContext servletContext) { this.servletContext = servletContext; } @Bean("manifest") public Manifest getManifest() { // War部署场景:通过ServletContext直接读取web应用根下的MANIFEST try (InputStream is = servletContext.getResourceAsStream("/META-INF/MANIFEST.MF")) { if (is != null) { return new Manifest(is); } } catch (Exception e) { // 本地开发运行场景:降级从类路径读取 try (InputStream localIs = getClass().getClassLoader().getResourceAsStream("META-INF/MANIFEST.MF")) { if (localIs != null) { return new Manifest(localIs); } } catch (Exception ex) { throw new RuntimeException("读取MANIFEST.MF失败", ex); } } throw new RuntimeException("未找到应用MANIFEST.MF文件,请检查构建配置"); } }
- 给Controller中的属性读取加非空判断,避免空指针:
@GetMapping("/get-app-details") public String getAppDetails() { Attributes mainAttributes = manifest.getMainAttributes(); String buildNum = mainAttributes.getValue("Build-Number"); if (buildNum == null) { buildNum = "unknown"; } else if (buildNum.contains("_")) { buildNum = buildNum.substring(buildNum.lastIndexOf('_') + 1); } String appVersion = env.getProperty("App.Version", "unknown") + "." + buildNum; return "Build Number - " + buildNum + ", AppVersion - " + appVersion; }
注意事项
不要在代码中静默吞掉所有异常,至少要打印错误日志,否则出现问题时完全没有排查线索。
内容的提问来源于stack exchange,提问作者Ravi
相关产品推荐
相关产品推荐

