我们经常看到java的一些jar包META-INF目录下包含一个MANIFEST.MF文件,里面包含一些版本信息,标题,实现组织,很多第三方的jar包还会自定义一个属性。

本文讲解如何读取jar包中MANIFEST.MF中的内容

概述

JDK中实际上提供了java.util.jar.Manifest用于封装MANIFEST.MF中的属性值。应用程序启动时会通过类加载器加载jar包中的类。而在加载类之前首先需要读取jar包。

首先将jar包路径封装在sun.misc.URLClassPath.Loader中,该类是一个抽象类,有两个子类,sun.misc.URLClassPath.JarLoader:用于加载jar包中的资源
sun.misc.URLClassPath.FileLoader:用于加载目录中的资源
URLClassPath.Loader中有个URLClassPath.Loader#getResource(java.lang.String, boolean)用于返回sun.misc.Resource对象,Resource中的方法Resource#getManifest则可以获取Manifest对象,便可以读取MANIFEST.MF其中的属性值.

Resource有两个匿名内部类的实现:
一个是在URLClassPath.FileLoader#getResource方法中创建,但该方法并没有实现读取MANIFEST.MF
另一个是在JarLoader#getResource(java.lang.String, boolean)中创建,该内部类实现了读取MANIFEST.MF

URLClassPath中的public Resource #getResource(java.lang.String classFilePath, boolean)则是遍历所有的URLClassPath.Loader实现来判断当前要加载的类是否包含在对应的Loader中,如果包含则通过该Loader获取Resource,然后加载Class

读取MANIFEST.MF中属性值

通过ClassLoader来加载对应的资源

java.lang.ClassLoader提供了#getResource方法,用于获取类路径上的资源返回URL,默认为sun.misc.Launcher.AppClassLoader,其继承了URLClassLoader

当需要获取一个类所在jar包的Manifest时,可以将类名.转为/,例如如下格式做为资源名
org/springframework/boot/SpringApplication.class

如果资源存在jar包中,url.openConnection()返回的是JarURLConnection,通过java.net.JarURLConnection#getJarFile可以返回java.util.jar.JarFile对象,调用java.util.jar.JarFile#getManifest便可以获取MANIFEST.MF中的信息.

当然如果获取到类所在jar的路径,可以调用构造方法java.util.jar.JarFile#JarFile(java.lang.String)直接创建JarFile对象。

可参考JDK中的代码如下:

/*** 只有在查找具体某个类所在jar包的Manifest信息时, 才会加载对应Manifest* 优化: 可以在找到后做缓存*/
public static Manifest getJarManifest(Class<?> clazz) {ArrayList<URL> resourceUrls = new ArrayList<>();ClassLoader classLoader = clazz.getClassLoader();String sourceName = clazz.getName().replace('.', '/').concat(".class");try {Enumeration<URL> resources = classLoader.getResources(sourceName);while (resources.hasMoreElements()) {resourceUrls.add(resources.nextElement());}if (resourceUrls.size() > 1) {log.warn("class:{}在多个不同的包中:{}", clazz.getName(), resourceUrls);}if (resourceUrls.size() > 0) {URL url = resourceUrls.get(0);URLConnection urlConnection = url.openConnection();if (urlConnection instanceof JarURLConnection) {JarURLConnection jarURL = (JarURLConnection) urlConnection;JarFile jarFile = jarURL.getJarFile();Manifest manifest = jarFile.getManifest();jarFile.close();return manifest;} else {//TODO 需要对非jar做处理}}} catch (IOException e) {e.printStackTrace();}return null;
}

上面的读取MANIFEST.MF的方式:

  1. 不支持未打成jar包的结构,例如业务工程直接在idea中执行,但是没有打成jar,此时即使工程的META-INF目录有MANIFEST.MF文件也无法读取,需要对非jar做处理。
  2. 支持在SpringBoot打的jar包:虽然SpringBoot的jar结构是自定义的,但是spring-boot-loader,重写了JarURLConnectionorg.springframework.boot.loader.jar.JarFile等等,所以在SpringBoot jar中运行同样支持。

使用spring-boot-loader来读取.

下面是SpringBoot打包之后的jar包目录结构

需要依赖spring-boot-loader这个包,该包中的代码实际上就是上面截图中的内容,只不过SpringBoot打包插件把它的源码达到了jar中.

不建议使用这种方式

  1. 需要依赖额外的jar包.
  2. spring-boot-loader中很多方法不是公开的,需要自己实现的代码比较多.
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-loader</artifactId><scope>provided</scope>
</dependency>
import org.springframework.boot.loader.archive.Archive;
import org.springframework.boot.loader.archive.ExplodedArchive;import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URL;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.Manifest;/*** Created by bruce on 2022/1/21 14:23*/
public class SpringBootLibJarLoader {static Archive rootArchive;static ClassPathIndexFile classPathIndex;static volatile Map<URL, Archive> urlArchiveMap;public static Manifest getManifest(Class<?> clazz) {Archive entries = create(clazz);try {return entries != null ? entries.getManifest() : null;} catch (IOException e) {e.printStackTrace();}return null;}public static Archive create(Class<?> clazz) {ProtectionDomain protectionDomain = clazz.getProtectionDomain();CodeSource codeSource = protectionDomain.getCodeSource();URL location = null;try {location = (codeSource != null) ? codeSource.getLocation().toURI().toURL() : null;} catch (Exception e) {e.printStackTrace();}if (urlArchiveMap == null) {synchronized (SpringBootLibJarLoader.class) {if (urlArchiveMap == null) {urlArchiveMap = load(clazz);}}}return urlArchiveMap.get(location);}private static Map<URL, Archive> load(Class<?> clazz) {HashMap<URL, Archive> jarArchiveMap = new HashMap<>();ClassLoader classLoader = clazz.getClassLoader();if (classLoader instanceof LaunchedURLClassLoader) {try {Field rootArchiveField = LaunchedURLClassLoader.class.getDeclaredField("rootArchive");rootArchiveField.setAccessible(true);rootArchive = (Archive) rootArchiveField.get(classLoader);classPathIndex = getClassPathIndex(rootArchive);Iterator<Archive> classPathArchivesIterator = getClassPathArchivesIterator();while (classPathArchivesIterator.hasNext()) {Archive archive = classPathArchivesIterator.next();jarArchiveMap.put(archive.getUrl(), archive);}} catch (Exception e) {e.printStackTrace();}}return jarArchiveMap;}private static final String DEFAULT_CLASSPATH_INDEX_LOCATION = "BOOT-INF/classpath.idx";protected static final String BOOT_CLASSPATH_INDEX_ATTRIBUTE = "Spring-Boot-Classpath-Index";private static String getClassPathIndexFileLocation(Archive archive) throws IOException {Manifest manifest = archive.getManifest();Attributes attributes = (manifest != null) ? manifest.getMainAttributes() : null;String location = (attributes != null) ? attributes.getValue(BOOT_CLASSPATH_INDEX_ATTRIBUTE) : null;return (location != null) ? location : DEFAULT_CLASSPATH_INDEX_LOCATION;}protected static ClassPathIndexFile getClassPathIndex(Archive archive) throws IOException {// Only needed for exploded archives, regular ones already have a defined orderif (archive instanceof ExplodedArchive) {String location = getClassPathIndexFileLocation(archive);return ClassPathIndexFile.loadIfPossible(archive.getUrl(), location);}return null;}static final Archive.EntryFilter NESTED_ARCHIVE_ENTRY_FILTER = (entry) -> {if (entry.isDirectory()) {return entry.getName().equals("BOOT-INF/classes/");}return entry.getName().startsWith("BOOT-INF/lib/");};protected static boolean isNestedArchive(Archive.Entry entry) {return NESTED_ARCHIVE_ENTRY_FILTER.matches(entry);}protected static boolean isSearchCandidate(Archive.Entry entry) {return entry.getName().startsWith("BOOT-INF/");}private static boolean isEntryIndexed(Archive.Entry entry) {if (classPathIndex != null) {return classPathIndex.containsEntry(entry.getName());}return false;}protected static Iterator<Archive> getClassPathArchivesIterator() throws Exception {Archive.EntryFilter searchFilter = SpringBootLibJarLoader::isSearchCandidate;Iterator<Archive> archives = rootArchive.getNestedArchives(searchFilter,(entry) -> isNestedArchive(entry) && !isEntryIndexed(entry));// if (isPostProcessingClassPathArchives()) {//     archives = applyClassPathArchivePostProcessing(archives);// }return archives;}// protected static void postProcessClassPathArchives(List<Archive> archives) throws Exception {// }// private static Iterator<Archive> applyClassPathArchivePostProcessing(Iterator<Archive> archives) throws Exception {//     List<Archive> list = new ArrayList<>();//     while (archives.hasNext()) {//         list.add(archives.next());//     }//     postProcessClassPathArchives(list);//     return list.iterator();// }//////// protected static boolean isPostProcessingClassPathArchives() {//     return false;// }
}

通过读取classpath文件的方式获取Manifest

该方式优点是:

  1. 不需要依赖spring-boot-loader
  2. 在springboot的jar中同样有效
/*** 通过读取读取classpath文件的方式获取Manifest*/
public static Manifest getManifestFromClasspath(Class<?> clazz) {ProtectionDomain protectionDomain = clazz.getProtectionDomain();CodeSource codeSource = protectionDomain.getCodeSource();URI codeJarUri = null;try {codeJarUri = (codeSource != null) ? codeSource.getLocation().toURI() : null;} catch (URISyntaxException e) {e.printStackTrace();}if (codeJarUri == null) {return null;}if (codeJarUri.getScheme().equals("jar")) {String newPath = codeJarUri.getSchemeSpecificPart();String suffix = "!/BOOT-INF/classes!/";if (newPath.endsWith(suffix)) {newPath = newPath.substring(0, newPath.length() - suffix.length());}if (newPath.endsWith("!/")) {newPath = newPath.substring(0, newPath.length() - 2);}try {codeJarUri = new URI(newPath);} catch (URISyntaxException e) {e.printStackTrace();}}if (uriManifestMap == null) {synchronized (ManifestUtil.class) {if (uriManifestMap == null) {try {uriManifestMap = readClasspathAllManifest();} catch (Exception e) {e.printStackTrace();}}}}return uriManifestMap.get(codeJarUri);
}private static HashMap<URI, Manifest> readClasspathAllManifest() throws Exception {HashMap<URI, Manifest> manifestMap = new HashMap<>();PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();org.springframework.core.io.Resource[] resources =resolver.getResources(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "META-INF/MANIFEST.MF");for (org.springframework.core.io.Resource resource : resources) {URL manifestUrl = resource.getURL();int lastIndex = 0;String manifestPath = null;if (manifestUrl.getProtocol().equals("file")) {manifestPath = manifestUrl.toString();lastIndex = manifestPath.indexOf("META-INF/MANIFEST.MF");} else if (manifestUrl.getProtocol().equals("jar")) {manifestPath = manifestUrl.getPath();lastIndex = manifestPath.indexOf("!/META-INF/MANIFEST.MF");} else {System.err.println("jar位置的格式不支持");continue;}URI jarUri = new URI(manifestPath.substring(0, lastIndex));InputStream inputStream = null;try {inputStream = resource.getInputStream();Manifest manifest = new Manifest(inputStream);manifestMap.put(jarUri, manifest);} finally {if (inputStream != null) {inputStream.close();}}}return manifestMap;
}

自定义MANIFEST.MF中属性值

SpringBoot打jar包后的默认MANIFEST.MF

普通应用 maven打包生成的jar 默认MANIFEST.MF

maven的打包插件支持在打包时自定义MANIFEST.MF中属性值.
例如向MANIFEST.MF中,写入appId, build.time

  1. Implementation-Title:默认为pom.xml中 ${project.name} ,如果不存在则使用${project.artifactId}
  2. jar包名称默认为: ${project.artifactId}-${project.version}.jar
  3. <manifestFile> 指定的MANIFEST.MF中内容会合并到或者覆盖默认的生成的MANIFEST.MF中
  4. MANIFEST.MF中的内容支持分组
<plugin><groupId>org.codehaus.mojo</groupId><artifactId>build-helper-maven-plugin</artifactId><version>3.2.0</version><executions><execution><id>timestamp-property</id><goals><goal>timestamp-property</goal></goals><configuration><name>build.time</name><pattern>yyyy-MM-dd HH:mm</pattern><timeZone>GMT+8</timeZone></configuration></execution></executions>
</plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.2.0</version><configuration><archive><manifestEntries><appId>${project.name}</appId><buildTime>${build.time}</buildTime></manifestEntries><manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile></archive></configuration>
</plugin>

java原生和SpringBoot读取jar包中MANIFEST.MF的方式相关推荐

  1. 解决springboot读取jar包中文件的问题

    解决springboot读取jar包中文件的问题 参考文章: (1)解决springboot读取jar包中文件的问题 (2)https://www.cnblogs.com/songxiaotong/p ...

  2. Springboot读取jar包中的MANIFEST.MF文件内容

    Springboot打包成可执行jar包后,包结构如下所示: 工程编译后的文件都在BOOT-INF/classes目录下,如果需要读取并显示META-INF/MANIFEST.MF文件内容,那么可以用 ...

  3. java jar包中MANIFEST.MF中有版本信息

    有时候查看人家的源代码,但有可能该软件有很多个版本,这个时候就可以通过 MANIFEST.MF来查看当前项目引用的是哪个版本.当然了如果本身jar包名已经是用版本号来命名的.则不需要这样查看. 可以参 ...

  4. 打包部署后无法读取jar包里的文件(实测可行,Java中读取jar包中的文件)

    打包部署后无法读取jar包里的文件 Java中读取jar包中的文件 linux中无法读取jar包中的内容(windows可以的!),如何解决 一.背景 项目中免不了需要读取文件,如果文件用绝对路径读取 ...

  5. java 读取jar包中的文件

    文章目录 项目resource中文件路径和jar包中文件路径的区别 正常读取 jar包读取 完整代码: 项目resource中文件路径和jar包中文件路径的区别 打成jar包后,是一个整体的文件. 正 ...

  6. java class修改_【原创】Java基础之简单修改jar包中的class

    有时需要修改很多jar(假设这些jar都位于lib目录)中其中一个jar中的某一个类,而且又没有原始代码或ide,这时最简单的方式是: 1 进入lib目录 #cd lib#ls test.jar de ...

  7. jav中jar包的MANIFEST.MF

    打开Java的JAR文件我们经常可以看到文件中包含着一个METAR-INF目录,这个目录下会有一些文件,其中必有一个MANIFEST.MF,这个文件描述了该Jar文件的很多信息,下面将详细介绍MANI ...

  8. JAR 包及MANIFEST.MF 文件详解

    常常在网上看到有人询问:如何把 java 程序编译成 .exe 文件.通常回答只有两种,一种是制作一个可执行的 JAR 文件包,然后就可以像.chm 文档一样双击运行了:而另一种是使用 JET 来进行 ...

  9. 关于java中读取jar包中的文件

    背景 之前项目使用的都是war包用tomcat去部署的,其中用读取一些xml文件的地方,代码结构如下图: 之前读取tpl中的xml使用的是 new Flie(file://xxx/xx)方法.因为这个 ...

最新文章

  1. php析构函数的用法
  2. 如何使用CodeSmith批量生成代码
  3. Jenkins配置基于角色的项目权限管理--转
  4. c++和c语言的区别_C 语言和 C++ 有什么区别?老程序员居然这样理解,不怕你不懂...
  5. 钉钉提示请勿通过开发者调试模式_钉钉开放平台demo调试异常问题解决:hostname in certificate didn't match...
  6. 将WildFly绑定到其他IP地址或多宿主上的所有地址
  7. Arthas - Java 线上问题定位处理的终极利器
  8. nchw_to_nhwc=True
  9. [SceneKit] 不会 Unity3D 的另一种选择
  10. JavaWeb编程中如果jar包存在但显示无法输出,就在IDEA的项目发布中添加lib依赖!
  11. Spring→简介核心作用范围、框架、接口编程、IOC控制反转、单元测试、Bean容器、注入、作用域、生命周期、自动装配注入、自动扫描@注解
  12. 介绍几款高级DAC解码芯片(整编)
  13. 上位机通信标准-OPC
  14. 基于opencv的图像拼接
  15. android 手表解决方案,智能手表解决方案
  16. tp5利用redis缓存制作qq邮箱验证
  17. 私域运营中引流加爆微信好友的方法
  18. 数据中心中交换机的转发原理 ---尚文网络奎哥
  19. 最新版CATIA,让您快速创造完整高级机械项目
  20. 计算机技术对艺术设计的意义,解析数字艺术对艺术设计的影响论文

热门文章

  1. linux系统有哪些版本 linux系统哪个版本好用
  2. 什么是冲突域,什么是广播域?区别又是什么
  3. 医疗数字化:区块链或成最强辅助
  4. jQuery和CSS制作霓虹灯文字效果
  5. rest php,使用php创建一个Rest Api
  6. STLINk驱动安装
  7. CloudComparePCL 基于FPFH特征的SAC-IA算法
  8. java事务类型_Spring事务类型祥解
  9. 【机器人关节空间与笛卡尔空间示教】
  10. 准确率-召回率 - Precision-Recall