Update fat jar loader to support multi-release jar files
Closes gh-12523
This commit is contained in:
parent
60b35df215
commit
56eebc9385
|
@ -42,9 +42,9 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader {
|
|||
|
||||
private long localHeaderOffset;
|
||||
|
||||
JarEntry(JarFile jarFile, CentralDirectoryFileHeader header) {
|
||||
super(header.getName().toString());
|
||||
this.name = header.getName();
|
||||
JarEntry(JarFile jarFile, CentralDirectoryFileHeader header, AsciiBytes nameAlias) {
|
||||
super((nameAlias != null) ? nameAlias.toString() : header.getName().toString());
|
||||
this.name = (nameAlias != null) ? nameAlias : header.getName();
|
||||
this.jarFile = jarFile;
|
||||
this.localHeaderOffset = header.getLocalHeaderOffset();
|
||||
setCompressedSize(header.getCompressedSize());
|
||||
|
|
|
@ -24,6 +24,9 @@ import java.util.Iterator;
|
|||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.Attributes.Name;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.springframework.boot.loader.data.RandomAccessData;
|
||||
|
@ -44,6 +47,27 @@ import org.springframework.boot.loader.data.RandomAccessData;
|
|||
*/
|
||||
class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
|
||||
|
||||
private static final String META_INF_PREFIX = "META-INF/";
|
||||
|
||||
private static final Name MULTI_RELEASE = new Name("Multi-Release");
|
||||
|
||||
private static final int BASE_VERSION = 8;
|
||||
|
||||
private static final int RUNTIME_VERSION;
|
||||
|
||||
static {
|
||||
int version;
|
||||
try {
|
||||
Object runtimeVersion = Runtime.class.getMethod("version").invoke(null);
|
||||
version = (int) runtimeVersion.getClass().getMethod("major")
|
||||
.invoke(runtimeVersion);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
version = 8;
|
||||
}
|
||||
RUNTIME_VERSION = version;
|
||||
}
|
||||
|
||||
private static final long LOCAL_FILE_HEADER_SIZE = 30;
|
||||
|
||||
private static final char SLASH = '/';
|
||||
|
@ -66,6 +90,8 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
|
|||
|
||||
private int[] positions;
|
||||
|
||||
private Boolean multiReleaseJar;
|
||||
|
||||
private final Map<Integer, FileHeader> entriesCache = Collections
|
||||
.synchronizedMap(new LinkedHashMap<Integer, FileHeader>(16, 0.75f, true) {
|
||||
|
||||
|
@ -83,6 +109,9 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
|
|||
JarFileEntries(JarFile jarFile, JarEntryFilter filter) {
|
||||
this.jarFile = jarFile;
|
||||
this.filter = filter;
|
||||
if (RUNTIME_VERSION == BASE_VERSION) {
|
||||
this.multiReleaseJar = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -216,21 +245,68 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
|
|||
|
||||
private <T extends FileHeader> T getEntry(CharSequence name, Class<T> type,
|
||||
boolean cacheEntry) {
|
||||
T entry = doGetEntry(name, type, cacheEntry, null);
|
||||
if (isMultiReleaseJar() && !isMetaInfEntry(name)) {
|
||||
int version = RUNTIME_VERSION;
|
||||
AsciiBytes nameAlias = (entry instanceof JarEntry)
|
||||
? ((JarEntry) entry).getAsciiBytesName()
|
||||
: new AsciiBytes(name.toString());
|
||||
while (version > BASE_VERSION) {
|
||||
T versionedEntry = doGetEntry("META-INF/versions/" + version + "/" + name,
|
||||
type, cacheEntry, nameAlias);
|
||||
if (versionedEntry != null) {
|
||||
return versionedEntry;
|
||||
}
|
||||
version--;
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
private boolean isMetaInfEntry(CharSequence name) {
|
||||
return name.toString().startsWith(META_INF_PREFIX);
|
||||
}
|
||||
|
||||
private boolean isMultiReleaseJar() {
|
||||
Boolean multiRelease = this.multiReleaseJar;
|
||||
if (multiRelease != null) {
|
||||
return multiRelease;
|
||||
}
|
||||
try {
|
||||
Manifest manifest = this.jarFile.getManifest();
|
||||
if (manifest == null) {
|
||||
multiRelease = false;
|
||||
}
|
||||
else {
|
||||
Attributes attributes = manifest.getMainAttributes();
|
||||
multiRelease = attributes.containsKey(MULTI_RELEASE);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
multiRelease = false;
|
||||
}
|
||||
this.multiReleaseJar = multiRelease;
|
||||
return multiRelease;
|
||||
}
|
||||
|
||||
private <T extends FileHeader> T doGetEntry(CharSequence name, Class<T> type,
|
||||
boolean cacheEntry, AsciiBytes nameAlias) {
|
||||
int hashCode = AsciiBytes.hashCode(name);
|
||||
T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry);
|
||||
T entry = getEntry(hashCode, name, NO_SUFFIX, type, cacheEntry, nameAlias);
|
||||
if (entry == null) {
|
||||
hashCode = AsciiBytes.hashCode(hashCode, SLASH);
|
||||
entry = getEntry(hashCode, name, SLASH, type, cacheEntry);
|
||||
entry = getEntry(hashCode, name, SLASH, type, cacheEntry, nameAlias);
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
private <T extends FileHeader> T getEntry(int hashCode, CharSequence name,
|
||||
char suffix, Class<T> type, boolean cacheEntry) {
|
||||
char suffix, Class<T> type, boolean cacheEntry, AsciiBytes nameAlias) {
|
||||
int index = getFirstIndex(hashCode);
|
||||
while (index >= 0 && index < this.size && this.hashCodes[index] == hashCode) {
|
||||
T entry = getEntry(index, type, cacheEntry);
|
||||
if (entry.hasName(name, suffix)) {
|
||||
T entry = getEntry(index, type, cacheEntry, nameAlias);
|
||||
if (entry.hasName((nameAlias != null) ? nameAlias.toString() : name,
|
||||
suffix)) {
|
||||
return entry;
|
||||
}
|
||||
index++;
|
||||
|
@ -240,7 +316,7 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
|
|||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T extends FileHeader> T getEntry(int index, Class<T> type,
|
||||
boolean cacheEntry) {
|
||||
boolean cacheEntry, AsciiBytes nameAlias) {
|
||||
try {
|
||||
FileHeader cached = this.entriesCache.get(index);
|
||||
FileHeader entry = (cached != null) ? cached
|
||||
|
@ -249,7 +325,8 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
|
|||
this.centralDirectoryOffsets[index], this.filter);
|
||||
if (CentralDirectoryFileHeader.class.equals(entry.getClass())
|
||||
&& type.equals(JarEntry.class)) {
|
||||
entry = new JarEntry(this.jarFile, (CentralDirectoryFileHeader) entry);
|
||||
entry = new JarEntry(this.jarFile, (CentralDirectoryFileHeader) entry,
|
||||
nameAlias);
|
||||
}
|
||||
if (cacheEntry && cached != entry) {
|
||||
this.entriesCache.put(index, entry);
|
||||
|
@ -299,7 +376,7 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
|
|||
}
|
||||
int entryIndex = JarFileEntries.this.positions[this.index];
|
||||
this.index++;
|
||||
return getEntry(entryIndex, JarEntry.class, false);
|
||||
return getEntry(entryIndex, JarEntry.class, false, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
* Copyright 2012-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -52,13 +52,25 @@ public abstract class TestJarCreator {
|
|||
writeNestedEntry("nested.jar", unpackNested, jarOutputStream);
|
||||
writeNestedEntry("another-nested.jar", unpackNested, jarOutputStream);
|
||||
writeNestedEntry("space nested.jar", unpackNested, jarOutputStream);
|
||||
writeNestedMultiReleaseEntry("multi-release.jar", unpackNested,
|
||||
jarOutputStream);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeNestedEntry(String name, boolean unpackNested,
|
||||
JarOutputStream jarOutputStream) throws Exception {
|
||||
writeNestedEntry(name, unpackNested, jarOutputStream, false);
|
||||
}
|
||||
|
||||
private static void writeNestedMultiReleaseEntry(String name, boolean unpackNested,
|
||||
JarOutputStream jarOutputStream) throws Exception {
|
||||
writeNestedEntry(name, unpackNested, jarOutputStream, true);
|
||||
}
|
||||
|
||||
private static void writeNestedEntry(String name, boolean unpackNested,
|
||||
JarOutputStream jarOutputStream, boolean multiRelease) throws Exception {
|
||||
JarEntry nestedEntry = new JarEntry(name);
|
||||
byte[] nestedJarData = getNestedJarData();
|
||||
byte[] nestedJarData = getNestedJarData(multiRelease);
|
||||
nestedEntry.setSize(nestedJarData.length);
|
||||
nestedEntry.setCompressedSize(nestedJarData.length);
|
||||
if (unpackNested) {
|
||||
|
@ -74,23 +86,40 @@ public abstract class TestJarCreator {
|
|||
jarOutputStream.closeEntry();
|
||||
}
|
||||
|
||||
private static byte[] getNestedJarData() throws Exception {
|
||||
private static byte[] getNestedJarData(boolean multiRelease) throws Exception {
|
||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
||||
JarOutputStream jarOutputStream = new JarOutputStream(byteArrayOutputStream);
|
||||
writeManifest(jarOutputStream, "j2");
|
||||
writeEntry(jarOutputStream, "3.dat", 3);
|
||||
writeEntry(jarOutputStream, "4.dat", 4);
|
||||
writeEntry(jarOutputStream, "\u00E4.dat", '\u00E4');
|
||||
writeManifest(jarOutputStream, "j2", multiRelease);
|
||||
if (multiRelease) {
|
||||
writeEntry(jarOutputStream, "multi-release.dat", 8);
|
||||
writeEntry(jarOutputStream, "META-INF/versions/9/multi-release.dat", 9);
|
||||
writeEntry(jarOutputStream, "META-INF/versions/10/multi-release.dat", 10);
|
||||
writeEntry(jarOutputStream, "META-INF/versions/11/multi-release.dat", 11);
|
||||
}
|
||||
else {
|
||||
writeEntry(jarOutputStream, "3.dat", 3);
|
||||
writeEntry(jarOutputStream, "4.dat", 4);
|
||||
writeEntry(jarOutputStream, "\u00E4.dat", '\u00E4');
|
||||
}
|
||||
jarOutputStream.close();
|
||||
return byteArrayOutputStream.toByteArray();
|
||||
}
|
||||
|
||||
private static void writeManifest(JarOutputStream jarOutputStream, String name)
|
||||
throws Exception {
|
||||
writeManifest(jarOutputStream, name, false);
|
||||
}
|
||||
|
||||
private static void writeManifest(JarOutputStream jarOutputStream, String name,
|
||||
boolean multiRelease) throws Exception {
|
||||
writeDirEntry(jarOutputStream, "META-INF/");
|
||||
Manifest manifest = new Manifest();
|
||||
manifest.getMainAttributes().putValue("Built-By", name);
|
||||
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
|
||||
if (multiRelease) {
|
||||
manifest.getMainAttributes().putValue("Multi-Release",
|
||||
Boolean.toString(true));
|
||||
}
|
||||
jarOutputStream.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
|
||||
manifest.write(jarOutputStream);
|
||||
jarOutputStream.closeEntry();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
* Copyright 2012-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -108,7 +108,7 @@ public class ExplodedArchiveTests {
|
|||
@Test
|
||||
public void getEntries() {
|
||||
Map<String, Archive.Entry> entries = getEntriesMap(this.archive);
|
||||
assertThat(entries.size()).isEqualTo(11);
|
||||
assertThat(entries.size()).isEqualTo(12);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -78,7 +78,7 @@ public class JarFileArchiveTests {
|
|||
@Test
|
||||
public void getEntries() {
|
||||
Map<String, Archive.Entry> entries = getEntriesMap(this.archive);
|
||||
assertThat(entries.size()).isEqualTo(11);
|
||||
assertThat(entries.size()).isEqualTo(12);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2017 the original author or authors.
|
||||
* Copyright 2012-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -82,6 +82,7 @@ public class CentralDirectoryParserTests {
|
|||
assertThat(headers.next().getName().toString()).isEqualTo("nested.jar");
|
||||
assertThat(headers.next().getName().toString()).isEqualTo("another-nested.jar");
|
||||
assertThat(headers.next().getName().toString()).isEqualTo("space nested.jar");
|
||||
assertThat(headers.next().getName().toString()).isEqualTo("multi-release.jar");
|
||||
assertThat(headers.hasNext()).isFalse();
|
||||
}
|
||||
|
||||
|
|
|
@ -91,6 +91,7 @@ public class JarFileTests {
|
|||
assertThat(entries.nextElement().getName()).isEqualTo("nested.jar");
|
||||
assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar");
|
||||
assertThat(entries.nextElement().getName()).isEqualTo("space nested.jar");
|
||||
assertThat(entries.nextElement().getName()).isEqualTo("multi-release.jar");
|
||||
assertThat(entries.hasMoreElements()).isFalse();
|
||||
URL jarUrl = new URL("jar:" + this.rootJarFile.toURI() + "!/");
|
||||
URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { jarUrl });
|
||||
|
@ -134,6 +135,7 @@ public class JarFileTests {
|
|||
assertThat(entries.nextElement().getName()).isEqualTo("nested.jar");
|
||||
assertThat(entries.nextElement().getName()).isEqualTo("another-nested.jar");
|
||||
assertThat(entries.nextElement().getName()).isEqualTo("space nested.jar");
|
||||
assertThat(entries.nextElement().getName()).isEqualTo("multi-release.jar");
|
||||
assertThat(entries.hasMoreElements()).isFalse();
|
||||
}
|
||||
|
||||
|
@ -499,4 +501,26 @@ public class JarFileTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multiReleaseEntry() throws Exception {
|
||||
JarFile multiRelease = this.jarFile
|
||||
.getNestedJarFile(this.jarFile.getEntry("multi-release.jar"));
|
||||
ZipEntry entry = multiRelease.getEntry("multi-release.dat");
|
||||
assertThat(entry.getName()).isEqualTo("multi-release.dat");
|
||||
InputStream inputStream = multiRelease.getInputStream(entry);
|
||||
assertThat(inputStream.available()).isEqualTo(1);
|
||||
assertThat(inputStream.read()).isEqualTo(getJavaVersion());
|
||||
}
|
||||
|
||||
private int getJavaVersion() {
|
||||
try {
|
||||
Object runtimeVersion = Runtime.class.getMethod("version").invoke(null);
|
||||
return (int) runtimeVersion.getClass().getMethod("major")
|
||||
.invoke(runtimeVersion);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue