我们经常看到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的方式:
- 不支持未打成jar包的结构,例如业务工程直接在idea中执行,但是没有打成jar,此时即使工程的
META-INF
目录有MANIFEST.MF
文件也无法读取,需要对非jar做处理。 - 支持在SpringBoot打的jar包:虽然SpringBoot的jar结构是自定义的,但是spring-boot-loader,重写了
JarURLConnection
、org.springframework.boot.loader.jar.JarFile
等等,所以在SpringBoot jar中运行同样支持。
使用spring-boot-loader来读取.
下面是SpringBoot打包之后的jar包目录结构
需要依赖spring-boot-loader这个包,该包中的代码实际上就是上面截图中的内容,只不过SpringBoot打包插件把它的源码达到了jar中.
不建议使用这种方式
- 需要依赖额外的jar包.
- 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 order
if (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
该方式优点是:
- 不需要依赖spring-boot-loader
- 在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
-
Implementation-Title
:默认为pom.xml中${project.name}
,如果不存在则使用${project.artifactId}
- jar包名称默认为:
${project.artifactId}
-${project.version}
.jar -
<manifestFile>
指定的MANIFEST.MF中内容会合并到或者覆盖默认的生成的MANIFEST.MF中 - 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>