Reduce the amount of garbage produced by JarFile
Previously, working with a JarFile created a large amount of garbage that was allocated on the thread local allocation buffer (TLAB). The TLAB allocations made a significant contribution to GC pressure and slowed down startup. This commit reduces the amount of garbage by making a number of changes. Reading from a RandomAccessDataFile has been reworked to avoid creating new RandomAccessFile instances. A single RandomAccessFile is now created for an entire jar file and it is used to read data from anywhere in that jar file, including entries in nested jar files. To ensure that reads remain thread-safe, a lock is taken on the RandomAccessFile that is shared by all RandomAccessDataFile instances that are provided access to (portions of) the same jar file. Reading all of the bytes from a RandomAccessData has been reworked to avoid the use of an InputStream that was created, used to read the data, and then thrown away. In place of the InputStream-based mechanism a method has been introduced that returns all of the RandomAccessData as a byte[]. Building on this change, a method has also been introduced to read a portion of a RandomAccessData as a byte[]. This avoids the need to create a new RandomAccessData subsection where the subsection was only used to read its entire contents and then thrown away. Decoding of an MS-DOS datetime has been reworked to use LocalDataTime rather than GregorianCalendar. The former produces less garbage than the latter. Closes gh-12226
This commit is contained in:
parent
569bad16da
commit
60ac2e5c09
|
@ -32,7 +32,6 @@ import java.util.UUID;
|
|||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||
import org.springframework.boot.loader.jar.JarFile;
|
||||
|
||||
/**
|
||||
|
@ -145,8 +144,7 @@ public class JarFileArchive implements Archive {
|
|||
}
|
||||
|
||||
private void unpack(JarEntry entry, File file) throws IOException {
|
||||
try (InputStream inputStream = this.jarFile.getInputStream(entry,
|
||||
ResourceAccess.ONCE);
|
||||
try (InputStream inputStream = this.jarFile.getInputStream(entry);
|
||||
OutputStream outputStream = new FileOutputStream(file)) {
|
||||
byte[] buffer = new byte[BUFFER_SIZE];
|
||||
int bytesRead;
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.loader.data;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* {@link RandomAccessData} implementation backed by a byte array.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class ByteArrayRandomAccessData implements RandomAccessData {
|
||||
|
||||
private final byte[] bytes;
|
||||
|
||||
private final long offset;
|
||||
|
||||
private final long length;
|
||||
|
||||
public ByteArrayRandomAccessData(byte[] bytes) {
|
||||
this(bytes, 0, (bytes == null ? 0 : bytes.length));
|
||||
}
|
||||
|
||||
public ByteArrayRandomAccessData(byte[] bytes, long offset, long length) {
|
||||
this.bytes = (bytes == null ? new byte[0] : bytes);
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream(ResourceAccess access) {
|
||||
return new ByteArrayInputStream(this.bytes, (int) this.offset, (int) this.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RandomAccessData getSubsection(long offset, long length) {
|
||||
return new ByteArrayRandomAccessData(this.bytes, this.offset + offset, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getSize() {
|
||||
return this.length;
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
||||
|
@ -30,11 +30,10 @@ public interface RandomAccessData {
|
|||
/**
|
||||
* Returns an {@link InputStream} that can be used to read the underlying data. The
|
||||
* caller is responsible close the underlying stream.
|
||||
* @param access hint indicating how the underlying data should be accessed
|
||||
* @return a new input stream that can be used to read the underlying data.
|
||||
* @throws IOException if the stream cannot be opened
|
||||
*/
|
||||
InputStream getInputStream(ResourceAccess access) throws IOException;
|
||||
InputStream getInputStream() throws IOException;
|
||||
|
||||
/**
|
||||
* Returns a new {@link RandomAccessData} for a specific subsection of this data.
|
||||
|
@ -44,28 +43,26 @@ public interface RandomAccessData {
|
|||
*/
|
||||
RandomAccessData getSubsection(long offset, long length);
|
||||
|
||||
/**
|
||||
* Reads all the data and returns it as a byte array.
|
||||
* @return the data
|
||||
* @throws IOException if the data cannot be read
|
||||
*/
|
||||
byte[] read() throws IOException;
|
||||
|
||||
/**
|
||||
* Reads the {@code length} bytes of data starting at the given {@code offset}.
|
||||
* @param offset the offset from which data should be read
|
||||
* @param length the number of bytes to be read
|
||||
* @return the data
|
||||
* @throws IOException if the data cannot be read
|
||||
*/
|
||||
byte[] read(long offset, long length) throws IOException;
|
||||
|
||||
/**
|
||||
* Returns the size of the data.
|
||||
* @return the size
|
||||
*/
|
||||
long getSize();
|
||||
|
||||
/**
|
||||
* Lock modes for accessing the underlying resource.
|
||||
*/
|
||||
enum ResourceAccess {
|
||||
|
||||
/**
|
||||
* Obtain access to the underlying resource once and keep it until the stream is
|
||||
* closed.
|
||||
*/
|
||||
ONCE,
|
||||
|
||||
/**
|
||||
* Obtain access to the underlying resource on each read, releasing it when done.
|
||||
*/
|
||||
PER_READ
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -17,25 +17,22 @@
|
|||
package org.springframework.boot.loader.data;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.Semaphore;
|
||||
|
||||
/**
|
||||
* {@link RandomAccessData} implementation backed by a {@link RandomAccessFile}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class RandomAccessDataFile implements RandomAccessData {
|
||||
|
||||
private static final int DEFAULT_CONCURRENT_READS = 4;
|
||||
|
||||
private final File file;
|
||||
|
||||
private final FilePool filePool;
|
||||
private final RandomAccessFile randomAccessFile;
|
||||
|
||||
private final long offset;
|
||||
|
||||
|
@ -45,30 +42,19 @@ public class RandomAccessDataFile implements RandomAccessData {
|
|||
* Create a new {@link RandomAccessDataFile} backed by the specified file.
|
||||
* @param file the underlying file
|
||||
* @throws IllegalArgumentException if the file is null or does not exist
|
||||
* @see #RandomAccessDataFile(File, int)
|
||||
*/
|
||||
public RandomAccessDataFile(File file) {
|
||||
this(file, DEFAULT_CONCURRENT_READS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link RandomAccessDataFile} backed by the specified file.
|
||||
* @param file the underlying file
|
||||
* @param concurrentReads the maximum number of concurrent reads allowed on the
|
||||
* underlying file before blocking
|
||||
* @throws IllegalArgumentException if the file is null or does not exist
|
||||
* @see #RandomAccessDataFile(File)
|
||||
*/
|
||||
public RandomAccessDataFile(File file, int concurrentReads) {
|
||||
if (file == null) {
|
||||
throw new IllegalArgumentException("File must not be null");
|
||||
}
|
||||
if (!file.exists()) {
|
||||
try {
|
||||
this.randomAccessFile = new RandomAccessFile(file, "r");
|
||||
}
|
||||
catch (FileNotFoundException ex) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("File %s must exist", file.getAbsolutePath()));
|
||||
}
|
||||
this.file = file;
|
||||
this.filePool = new FilePool(file, concurrentReads);
|
||||
this.offset = 0L;
|
||||
this.length = file.length();
|
||||
}
|
||||
|
@ -76,15 +62,16 @@ public class RandomAccessDataFile implements RandomAccessData {
|
|||
/**
|
||||
* Private constructor used to create a {@link #getSubsection(long, long) subsection}.
|
||||
* @param file the underlying file
|
||||
* @param pool the underlying pool
|
||||
* @param randomAccessFile the random access file from which data is read
|
||||
* @param offset the offset of the section
|
||||
* @param length the length of the section
|
||||
*/
|
||||
private RandomAccessDataFile(File file, FilePool pool, long offset, long length) {
|
||||
private RandomAccessDataFile(File file, RandomAccessFile randomAccessFile,
|
||||
long offset, long length) {
|
||||
this.file = file;
|
||||
this.filePool = pool;
|
||||
this.offset = offset;
|
||||
this.length = length;
|
||||
this.randomAccessFile = randomAccessFile;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -96,8 +83,8 @@ public class RandomAccessDataFile implements RandomAccessData {
|
|||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream(ResourceAccess access) throws IOException {
|
||||
return new DataInputStream(access);
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return new DataInputStream(this.randomAccessFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -105,8 +92,23 @@ public class RandomAccessDataFile implements RandomAccessData {
|
|||
if (offset < 0 || length < 0 || offset + length > this.length) {
|
||||
throw new IndexOutOfBoundsException();
|
||||
}
|
||||
return new RandomAccessDataFile(this.file, this.filePool, this.offset + offset,
|
||||
length);
|
||||
return new RandomAccessDataFile(this.file, this.randomAccessFile,
|
||||
this.offset + offset, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] read() throws IOException {
|
||||
return read(0, this.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] read(long offset, long length) throws IOException {
|
||||
byte[] bytes = new byte[(int) length];
|
||||
synchronized (this.randomAccessFile) {
|
||||
this.randomAccessFile.seek(this.offset + offset);
|
||||
this.randomAccessFile.read(bytes, 0, (int) length);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -115,7 +117,7 @@ public class RandomAccessDataFile implements RandomAccessData {
|
|||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
this.filePool.close();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -128,11 +130,8 @@ public class RandomAccessDataFile implements RandomAccessData {
|
|||
|
||||
private int position;
|
||||
|
||||
DataInputStream(ResourceAccess access) throws IOException {
|
||||
if (access == ResourceAccess.ONCE) {
|
||||
this.file = new RandomAccessFile(RandomAccessDataFile.this.file, "r");
|
||||
this.file.seek(RandomAccessDataFile.this.offset);
|
||||
}
|
||||
DataInputStream(RandomAccessFile file) throws IOException {
|
||||
this.file = file;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -170,24 +169,15 @@ public class RandomAccessDataFile implements RandomAccessData {
|
|||
if (cappedLen <= 0) {
|
||||
return -1;
|
||||
}
|
||||
RandomAccessFile file = this.file;
|
||||
try {
|
||||
if (file == null) {
|
||||
file = RandomAccessDataFile.this.filePool.acquire();
|
||||
file.seek(RandomAccessDataFile.this.offset + this.position);
|
||||
}
|
||||
synchronized (this.file) {
|
||||
this.file.seek(RandomAccessDataFile.this.offset + this.position);
|
||||
if (b == null) {
|
||||
int rtn = file.read();
|
||||
int rtn = this.file.read();
|
||||
moveOn(rtn == -1 ? 0 : 1);
|
||||
return rtn;
|
||||
}
|
||||
else {
|
||||
return (int) moveOn(file.read(b, off, cappedLen));
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (this.file == null && file != null) {
|
||||
RandomAccessDataFile.this.filePool.release(file);
|
||||
return (int) moveOn(this.file.read(b, off, cappedLen));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -197,13 +187,6 @@ public class RandomAccessDataFile implements RandomAccessData {
|
|||
return (n <= 0 ? 0 : moveOn(cap(n)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (this.file != null) {
|
||||
this.file.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cap the specified value such that it cannot exceed the number of bytes
|
||||
* remaining.
|
||||
|
@ -226,55 +209,4 @@ public class RandomAccessDataFile implements RandomAccessData {
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Manage a pool that can be used to perform concurrent reads on the underlying
|
||||
* {@link RandomAccessFile}.
|
||||
*/
|
||||
static class FilePool {
|
||||
|
||||
private final File file;
|
||||
|
||||
private final int size;
|
||||
|
||||
private final Semaphore available;
|
||||
|
||||
private final Queue<RandomAccessFile> files;
|
||||
|
||||
FilePool(File file, int size) {
|
||||
this.file = file;
|
||||
this.size = size;
|
||||
this.available = new Semaphore(size);
|
||||
this.files = new ConcurrentLinkedQueue<>();
|
||||
}
|
||||
|
||||
public RandomAccessFile acquire() throws IOException {
|
||||
this.available.acquireUninterruptibly();
|
||||
RandomAccessFile file = this.files.poll();
|
||||
if (file != null) {
|
||||
return file;
|
||||
}
|
||||
return new RandomAccessFile(this.file, "r");
|
||||
}
|
||||
|
||||
public void release(RandomAccessFile file) {
|
||||
this.files.add(file);
|
||||
this.available.release();
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
this.available.acquireUninterruptibly(this.size);
|
||||
try {
|
||||
RandomAccessFile pooledFile = this.files.poll();
|
||||
while (pooledFile != null) {
|
||||
pooledFile.close();
|
||||
pooledFile = this.files.poll();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.available.release(this.size);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
@ -27,6 +27,8 @@ import java.nio.charset.StandardCharsets;
|
|||
*/
|
||||
final class AsciiBytes {
|
||||
|
||||
private static final String EMPTY_STRING = "";
|
||||
|
||||
private static final int[] EXCESS = { 0x0, 0x1080, 0x96, 0x1c82080 };
|
||||
|
||||
private final byte[] bytes;
|
||||
|
@ -123,8 +125,13 @@ final class AsciiBytes {
|
|||
@Override
|
||||
public String toString() {
|
||||
if (this.string == null) {
|
||||
this.string = new String(this.bytes, this.offset, this.length,
|
||||
StandardCharsets.UTF_8);
|
||||
if (this.length == 0) {
|
||||
this.string = EMPTY_STRING;
|
||||
}
|
||||
else {
|
||||
this.string = new String(this.bytes, this.offset, this.length,
|
||||
StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
return this.string;
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
@ -16,12 +16,6 @@
|
|||
|
||||
package org.springframework.boot.loader.jar;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.springframework.boot.loader.data.RandomAccessData;
|
||||
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||
|
||||
/**
|
||||
* Utilities for dealing with bytes from ZIP files.
|
||||
*
|
||||
|
@ -29,45 +23,9 @@ import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
|||
*/
|
||||
final class Bytes {
|
||||
|
||||
private static final byte[] EMPTY_BYTES = new byte[] {};
|
||||
|
||||
private Bytes() {
|
||||
}
|
||||
|
||||
public static byte[] get(RandomAccessData data) throws IOException {
|
||||
try (InputStream inputStream = data.getInputStream(ResourceAccess.ONCE)) {
|
||||
return get(inputStream, data.getSize());
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[] get(InputStream inputStream, long length) throws IOException {
|
||||
if (length == 0) {
|
||||
return EMPTY_BYTES;
|
||||
}
|
||||
byte[] bytes = new byte[(int) length];
|
||||
if (!fill(inputStream, bytes)) {
|
||||
throw new IOException("Unable to read bytes");
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
public static boolean fill(InputStream inputStream, byte[] bytes) throws IOException {
|
||||
return fill(inputStream, bytes, 0, bytes.length);
|
||||
}
|
||||
|
||||
private static boolean fill(InputStream inputStream, byte[] bytes, int offset,
|
||||
int length) throws IOException {
|
||||
while (length > 0) {
|
||||
int read = inputStream.read(bytes, offset, length);
|
||||
if (read == -1) {
|
||||
return false;
|
||||
}
|
||||
offset += read;
|
||||
length = -read;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static long littleEndianValue(byte[] bytes, int offset, int length) {
|
||||
long value = 0;
|
||||
for (int i = length - 1; i >= 0; i--) {
|
||||
|
|
|
@ -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.
|
||||
|
@ -74,7 +74,7 @@ class CentralDirectoryEndRecord {
|
|||
private byte[] createBlockFromEndOfData(RandomAccessData data, int size)
|
||||
throws IOException {
|
||||
int length = (int) Math.min(data.getSize(), size);
|
||||
return Bytes.get(data.getSubsection(data.getSize() - length, length));
|
||||
return data.read(data.getSize() - length, length);
|
||||
}
|
||||
|
||||
private boolean isValid() {
|
||||
|
|
|
@ -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.
|
||||
|
@ -17,8 +17,8 @@
|
|||
package org.springframework.boot.loader.jar;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
|
||||
import org.springframework.boot.loader.data.RandomAccessData;
|
||||
|
||||
|
@ -75,8 +75,8 @@ final class CentralDirectoryFileHeader implements FileHeader {
|
|||
// Load variable part
|
||||
dataOffset += 46;
|
||||
if (variableData != null) {
|
||||
data = Bytes.get(variableData.getSubsection(variableOffset + 46,
|
||||
nameLength + extraLength + commentLength));
|
||||
data = variableData.read(variableOffset + 46,
|
||||
nameLength + extraLength + commentLength);
|
||||
dataOffset = 0;
|
||||
}
|
||||
this.name = new AsciiBytes(data, dataOffset, (int) nameLength);
|
||||
|
@ -115,27 +115,24 @@ final class CentralDirectoryFileHeader implements FileHeader {
|
|||
}
|
||||
|
||||
public long getTime() {
|
||||
long date = Bytes.littleEndianValue(this.header, this.headerOffset + 14, 2);
|
||||
long time = Bytes.littleEndianValue(this.header, this.headerOffset + 12, 2);
|
||||
return decodeMsDosFormatDateTime(date, time).getTimeInMillis();
|
||||
long datetime = Bytes.littleEndianValue(this.header, this.headerOffset + 12, 4);
|
||||
return decodeMsDosFormatDateTime(datetime);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode MS-DOS Date Time details. See
|
||||
* <a href="http://mindprod.com/jgloss/zip.html">mindprod.com/jgloss/zip.html</a> for
|
||||
* more details of the format.
|
||||
* @param date the date part
|
||||
* @param time the time part
|
||||
* @return a {@link Calendar} containing the decoded date.
|
||||
* @param datetime the date and time
|
||||
* @return the date and time as milliseconds since the epoch
|
||||
*/
|
||||
private Calendar decodeMsDosFormatDateTime(long date, long time) {
|
||||
int year = (int) ((date >> 9) & 0x7F) + 1980;
|
||||
int month = (int) ((date >> 5) & 0xF) - 1;
|
||||
int day = (int) (date & 0x1F);
|
||||
int hours = (int) ((time >> 11) & 0x1F);
|
||||
int minutes = (int) ((time >> 5) & 0x3F);
|
||||
int seconds = (int) ((time << 1) & 0x3E);
|
||||
return new GregorianCalendar(year, month, day, hours, minutes, seconds);
|
||||
private long decodeMsDosFormatDateTime(long datetime) {
|
||||
LocalDateTime localDateTime = LocalDateTime.of(
|
||||
(int) (((datetime >> 25) & 0x7f) + 1980), (int) ((datetime >> 21) & 0x0f),
|
||||
(int) ((datetime >> 16) & 0x1f), (int) ((datetime >> 11) & 0x1f),
|
||||
(int) ((datetime >> 5) & 0x3f), (int) ((datetime << 1) & 0x3e));
|
||||
return localDateTime.toEpochSecond(
|
||||
ZoneId.systemDefault().getRules().getOffset(localDateTime)) * 1000;
|
||||
}
|
||||
|
||||
public long getCrc() {
|
||||
|
@ -176,7 +173,7 @@ final class CentralDirectoryFileHeader implements FileHeader {
|
|||
public static CentralDirectoryFileHeader fromRandomAccessData(RandomAccessData data,
|
||||
int offset, JarEntryFilter filter) throws IOException {
|
||||
CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader();
|
||||
byte[] bytes = Bytes.get(data.getSubsection(offset, 46));
|
||||
byte[] bytes = data.read(offset, 46);
|
||||
fileHeader.load(bytes, 0, data, offset, filter);
|
||||
return fileHeader;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.springframework.boot.loader.data.RandomAccessData;
|
|||
* Parses the central directory from a JAR file.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
* @see CentralDirectoryVisitor
|
||||
*/
|
||||
class CentralDirectoryParser {
|
||||
|
@ -61,7 +62,7 @@ class CentralDirectoryParser {
|
|||
|
||||
private void parseEntries(CentralDirectoryEndRecord endRecord,
|
||||
RandomAccessData centralDirectoryData) throws IOException {
|
||||
byte[] bytes = Bytes.get(centralDirectoryData);
|
||||
byte[] bytes = centralDirectoryData.read(0, centralDirectoryData.getSize());
|
||||
CentralDirectoryFileHeader fileHeader = new CentralDirectoryFileHeader();
|
||||
int dataOffset = 0;
|
||||
for (int i = 0; i < endRecord.getNumberOfRecords(); i++) {
|
||||
|
|
|
@ -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.
|
||||
|
@ -28,6 +28,7 @@ import java.util.jar.Manifest;
|
|||
* Extended variant of {@link java.util.jar.JarEntry} returned by {@link JarFile}s.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class JarEntry extends java.util.jar.JarEntry implements FileHeader {
|
||||
|
||||
|
@ -49,11 +50,10 @@ class JarEntry extends java.util.jar.JarEntry implements FileHeader {
|
|||
setCompressedSize(header.getCompressedSize());
|
||||
setMethod(header.getMethod());
|
||||
setCrc(header.getCrc());
|
||||
setSize(header.getSize());
|
||||
setExtra(header.getExtra());
|
||||
setComment(header.getComment().toString());
|
||||
setSize(header.getSize());
|
||||
setTime(header.getTime());
|
||||
setExtra(header.getExtra());
|
||||
}
|
||||
|
||||
AsciiBytes getAsciiBytesName() {
|
||||
|
|
|
@ -31,7 +31,6 @@ import java.util.jar.Manifest;
|
|||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.springframework.boot.loader.data.RandomAccessData;
|
||||
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||
import org.springframework.boot.loader.data.RandomAccessDataFile;
|
||||
|
||||
/**
|
||||
|
@ -45,6 +44,7 @@ import org.springframework.boot.loader.data.RandomAccessDataFile;
|
|||
* </ul>
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class JarFile extends java.util.jar.JarFile {
|
||||
|
||||
|
@ -162,8 +162,7 @@ public class JarFile extends java.util.jar.JarFile {
|
|||
}
|
||||
}
|
||||
else {
|
||||
try (InputStream inputStream = getInputStream(MANIFEST_NAME,
|
||||
ResourceAccess.ONCE)) {
|
||||
try (InputStream inputStream = getInputStream(MANIFEST_NAME)) {
|
||||
if (inputStream == null) {
|
||||
return null;
|
||||
}
|
||||
|
@ -213,19 +212,14 @@ public class JarFile extends java.util.jar.JarFile {
|
|||
|
||||
@Override
|
||||
public synchronized InputStream getInputStream(ZipEntry ze) throws IOException {
|
||||
return getInputStream(ze, ResourceAccess.PER_READ);
|
||||
}
|
||||
|
||||
public InputStream getInputStream(ZipEntry ze, ResourceAccess access)
|
||||
throws IOException {
|
||||
if (ze instanceof JarEntry) {
|
||||
return this.entries.getInputStream((JarEntry) ze, access);
|
||||
return this.entries.getInputStream((JarEntry) ze);
|
||||
}
|
||||
return getInputStream(ze == null ? null : ze.getName(), access);
|
||||
return getInputStream(ze == null ? null : ze.getName());
|
||||
}
|
||||
|
||||
InputStream getInputStream(String name, ResourceAccess access) throws IOException {
|
||||
return this.entries.getInputStream(name, access);
|
||||
InputStream getInputStream(String name) throws IOException {
|
||||
return this.entries.getInputStream(name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -333,7 +327,7 @@ public class JarFile extends java.util.jar.JarFile {
|
|||
// happening that often.
|
||||
try {
|
||||
try (JarInputStream inputStream = new JarInputStream(
|
||||
getData().getInputStream(ResourceAccess.ONCE))) {
|
||||
getData().getInputStream())) {
|
||||
java.util.jar.JarEntry certEntry = inputStream.getNextJarEntry();
|
||||
while (certEntry != null) {
|
||||
inputStream.closeEntry();
|
||||
|
|
|
@ -27,7 +27,6 @@ import java.util.NoSuchElementException;
|
|||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.springframework.boot.loader.data.RandomAccessData;
|
||||
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||
|
||||
/**
|
||||
* Provides access to entries from a {@link JarFile}. In order to reduce memory
|
||||
|
@ -41,6 +40,7 @@ import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
|||
* which should consume about 122K.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
|
||||
|
||||
|
@ -177,18 +177,16 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
|
|||
return getEntry(name, JarEntry.class, true);
|
||||
}
|
||||
|
||||
public InputStream getInputStream(String name, ResourceAccess access)
|
||||
throws IOException {
|
||||
public InputStream getInputStream(String name) throws IOException {
|
||||
FileHeader entry = getEntry(name, FileHeader.class, false);
|
||||
return getInputStream(entry, access);
|
||||
return getInputStream(entry);
|
||||
}
|
||||
|
||||
public InputStream getInputStream(FileHeader entry, ResourceAccess access)
|
||||
throws IOException {
|
||||
public InputStream getInputStream(FileHeader entry) throws IOException {
|
||||
if (entry == null) {
|
||||
return null;
|
||||
}
|
||||
InputStream inputStream = getEntryData(entry).getInputStream(access);
|
||||
InputStream inputStream = getEntryData(entry).getInputStream();
|
||||
if (entry.getMethod() == ZipEntry.DEFLATED) {
|
||||
inputStream = new ZipInflaterInputStream(inputStream, (int) entry.getSize());
|
||||
}
|
||||
|
@ -208,8 +206,8 @@ class JarFileEntries implements CentralDirectoryVisitor, Iterable<JarEntry> {
|
|||
// local directory to the central directory. We need to re-read
|
||||
// here to skip them
|
||||
RandomAccessData data = this.jarFile.getData();
|
||||
byte[] localHeader = Bytes.get(
|
||||
data.getSubsection(entry.getLocalHeaderOffset(), LOCAL_FILE_HEADER_SIZE));
|
||||
byte[] localHeader = data.read(entry.getLocalHeaderOffset(),
|
||||
LOCAL_FILE_HEADER_SIZE);
|
||||
long nameLength = Bytes.littleEndianValue(localHeader, 26, 2);
|
||||
long extraLength = Bytes.littleEndianValue(localHeader, 28, 2);
|
||||
return data.getSubsection(entry.getLocalHeaderOffset() + LOCAL_FILE_HEADER_SIZE
|
||||
|
|
|
@ -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.
|
||||
|
@ -29,8 +29,6 @@ import java.net.URLEncoder;
|
|||
import java.net.URLStreamHandler;
|
||||
import java.security.Permission;
|
||||
|
||||
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||
|
||||
/**
|
||||
* {@link java.net.JarURLConnection} used to support {@link JarFile#getUrl()}.
|
||||
*
|
||||
|
@ -170,7 +168,7 @@ final class JarURLConnection extends java.net.JarURLConnection {
|
|||
}
|
||||
connect();
|
||||
InputStream inputStream = (this.jarEntryName.isEmpty()
|
||||
? this.jarFile.getData().getInputStream(ResourceAccess.ONCE)
|
||||
? this.jarFile.getData().getInputStream()
|
||||
: this.jarFile.getInputStream(this.jarEntry));
|
||||
if (inputStream == null) {
|
||||
throwFileNotFound(this.jarEntryName, this.jarFile);
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright 2012-2017 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.loader.data;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ByteArrayRandomAccessData}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class ByteArrayRandomAccessDataTests {
|
||||
|
||||
@Test
|
||||
public void testGetInputStream() throws Exception {
|
||||
byte[] bytes = new byte[] { 0, 1, 2, 3, 4, 5 };
|
||||
RandomAccessData data = new ByteArrayRandomAccessData(bytes);
|
||||
InputStream inputStream = data.getInputStream(ResourceAccess.PER_READ);
|
||||
assertThat(FileCopyUtils.copyToByteArray(inputStream)).isEqualTo(bytes);
|
||||
assertThat(data.getSize()).isEqualTo(bytes.length);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetSubsection() throws Exception {
|
||||
byte[] bytes = new byte[] { 0, 1, 2, 3, 4, 5 };
|
||||
RandomAccessData data = new ByteArrayRandomAccessData(bytes);
|
||||
data = data.getSubsection(1, 4).getSubsection(1, 2);
|
||||
InputStream inputStream = data.getInputStream(ResourceAccess.PER_READ);
|
||||
assertThat(FileCopyUtils.copyToByteArray(inputStream))
|
||||
.isEqualTo(new byte[] { 2, 3 });
|
||||
assertThat(data.getSize()).isEqualTo(2L);
|
||||
}
|
||||
|
||||
}
|
|
@ -18,14 +18,10 @@ package org.springframework.boot.loader.data;
|
|||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
|
@ -36,23 +32,14 @@ import org.junit.Rule;
|
|||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
import org.mockito.Mockito;
|
||||
|
||||
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
|
||||
import org.springframework.boot.loader.data.RandomAccessDataFile.FilePool;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.BDDMockito.willAnswer;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.spy;
|
||||
|
||||
/**
|
||||
* Tests for {@link RandomAccessDataFile}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class RandomAccessDataFileTests {
|
||||
|
||||
|
@ -84,7 +71,7 @@ public class RandomAccessDataFileTests {
|
|||
outputStream.write(BYTES);
|
||||
outputStream.close();
|
||||
this.file = new RandomAccessDataFile(this.tempFile);
|
||||
this.inputStream = this.file.getInputStream(ResourceAccess.PER_READ);
|
||||
this.inputStream = this.file.getInputStream();
|
||||
}
|
||||
|
||||
@After
|
||||
|
@ -109,22 +96,6 @@ public class RandomAccessDataFileTests {
|
|||
new RandomAccessDataFile(file);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileNotNullWithConcurrentReads() {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("File must not be null");
|
||||
new RandomAccessDataFile(null, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fileExistsWithConcurrentReads() {
|
||||
File file = new File("/does/not/exist");
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage(
|
||||
String.format("File %s must exist", file.getAbsolutePath()));
|
||||
new RandomAccessDataFile(file, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputStreamRead() throws Exception {
|
||||
for (int i = 0; i <= 255; i++) {
|
||||
|
@ -224,8 +195,7 @@ public class RandomAccessDataFileTests {
|
|||
@Test
|
||||
public void subsectionZeroLength() throws Exception {
|
||||
RandomAccessData subsection = this.file.getSubsection(0, 0);
|
||||
assertThat(subsection.getInputStream(ResourceAccess.PER_READ).read())
|
||||
.isEqualTo(-1);
|
||||
assertThat(subsection.getInputStream().read()).isEqualTo(-1);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -245,14 +215,13 @@ public class RandomAccessDataFileTests {
|
|||
@Test
|
||||
public void subsection() throws Exception {
|
||||
RandomAccessData subsection = this.file.getSubsection(1, 1);
|
||||
assertThat(subsection.getInputStream(ResourceAccess.PER_READ).read())
|
||||
.isEqualTo(1);
|
||||
assertThat(subsection.getInputStream().read()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inputStreamReadPastSubsection() throws Exception {
|
||||
RandomAccessData subsection = this.file.getSubsection(1, 2);
|
||||
InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
|
||||
InputStream inputStream = subsection.getInputStream();
|
||||
assertThat(inputStream.read()).isEqualTo(1);
|
||||
assertThat(inputStream.read()).isEqualTo(2);
|
||||
assertThat(inputStream.read()).isEqualTo(-1);
|
||||
|
@ -261,7 +230,7 @@ public class RandomAccessDataFileTests {
|
|||
@Test
|
||||
public void inputStreamReadBytesPastSubsection() throws Exception {
|
||||
RandomAccessData subsection = this.file.getSubsection(1, 2);
|
||||
InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
|
||||
InputStream inputStream = subsection.getInputStream();
|
||||
byte[] b = new byte[3];
|
||||
int amountRead = inputStream.read(b);
|
||||
assertThat(b).isEqualTo(new byte[] { 1, 2, 0 });
|
||||
|
@ -271,7 +240,7 @@ public class RandomAccessDataFileTests {
|
|||
@Test
|
||||
public void inputStreamSkipPastSubsection() throws Exception {
|
||||
RandomAccessData subsection = this.file.getSubsection(1, 2);
|
||||
InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
|
||||
InputStream inputStream = subsection.getInputStream();
|
||||
assertThat(inputStream.skip(3)).isEqualTo(2L);
|
||||
assertThat(inputStream.read()).isEqualTo(-1);
|
||||
}
|
||||
|
@ -293,7 +262,7 @@ public class RandomAccessDataFileTests {
|
|||
for (int i = 0; i < 100; i++) {
|
||||
results.add(executorService.submit(() -> {
|
||||
InputStream subsectionInputStream = RandomAccessDataFileTests.this.file
|
||||
.getSubsection(0, 256).getInputStream(ResourceAccess.PER_READ);
|
||||
.getSubsection(0, 256).getInputStream();
|
||||
byte[] b = new byte[256];
|
||||
subsectionInputStream.read(b);
|
||||
return Arrays.equals(b, BYTES);
|
||||
|
@ -304,45 +273,4 @@ public class RandomAccessDataFileTests {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void close() throws Exception {
|
||||
this.file.getInputStream(ResourceAccess.PER_READ).read();
|
||||
this.file.close();
|
||||
Field filePoolField = RandomAccessDataFile.class.getDeclaredField("filePool");
|
||||
filePoolField.setAccessible(true);
|
||||
Object filePool = filePoolField.get(this.file);
|
||||
Field filesField = filePool.getClass().getDeclaredField("files");
|
||||
filesField.setAccessible(true);
|
||||
Queue<?> queue = (Queue<?>) filesField.get(filePool);
|
||||
assertThat(queue).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void seekFailuresDoNotPreventSubsequentReads() throws Exception {
|
||||
FilePool filePool = (FilePool) ReflectionTestUtils.getField(this.file,
|
||||
"filePool");
|
||||
FilePool spiedPool = spy(filePool);
|
||||
ReflectionTestUtils.setField(this.file, "filePool", spiedPool);
|
||||
willAnswer((invocation) -> {
|
||||
RandomAccessFile originalFile = (RandomAccessFile) invocation
|
||||
.callRealMethod();
|
||||
if (Mockito.mockingDetails(originalFile).isSpy()) {
|
||||
return originalFile;
|
||||
}
|
||||
RandomAccessFile spiedFile = spy(originalFile);
|
||||
willThrow(new IOException("Seek failed")).given(spiedFile).seek(anyLong());
|
||||
return spiedFile;
|
||||
}).given(spiedPool).acquire();
|
||||
|
||||
for (int i = 0; i < 5; i++) {
|
||||
try {
|
||||
this.file.getInputStream(ResourceAccess.PER_READ).read();
|
||||
fail("Read should fail due to exception from seek");
|
||||
}
|
||||
catch (IOException ex) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -186,7 +186,7 @@ public class JarFileTests {
|
|||
@Test
|
||||
public void close() throws Exception {
|
||||
RandomAccessDataFile randomAccessDataFile = spy(
|
||||
new RandomAccessDataFile(this.rootJarFile, 1));
|
||||
new RandomAccessDataFile(this.rootJarFile));
|
||||
JarFile jarFile = new JarFile(randomAccessDataFile);
|
||||
jarFile.close();
|
||||
verify(randomAccessDataFile).close();
|
||||
|
|
Loading…
Reference in New Issue