java原生和SpringBoot读取jar包中MANIFEST.MF的方式_brucelwl的博客-CSDN博客_springboot manifest.mf

我们经常看到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 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

该方式优点是:

  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>

在这里插入图片描述


原网址: 访问
创建于: 2023-01-09 10:26:06
目录: default
标签: 无

请先后发表评论
  • 最新评论
  • 总共0条评论