Reduce garbage created when loading fat jars

Refactor fat jar loader classes so that less `char[]` instances are
created. This is primarily achieved by adding a new `StringSequence`
class that can chop up Strings without needing to copy the underlying
array. Since Java 8, calls to `String.subString(...)` always copy the
underlying char array. For many of the operations that we need, this
is unnecessary.

Fixes gh-11405
This commit is contained in:
Phillip Webb
2017-12-18 21:39:11 -08:00
parent c024313141
commit aa66d5dfb8
12 changed files with 484 additions and 159 deletions

View File

@@ -30,6 +30,8 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
public class AsciiBytesTests {
private static final char NO_SUFFIX = 0;
@Rule
public ExpectedException thrown = ExpectedException.none();
@@ -106,22 +108,6 @@ public class AsciiBytesTests {
abcd.substring(3, 5);
}
@Test
public void appendString() {
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
AsciiBytes appended = bc.append("D");
assertThat(bc.toString()).isEqualTo("BC");
assertThat(appended.toString()).isEqualTo("BCD");
}
@Test
public void appendBytes() {
AsciiBytes bc = new AsciiBytes(new byte[] { 65, 66, 67, 68 }, 1, 2);
AsciiBytes appended = bc.append(new byte[] { 68 });
assertThat(bc.toString()).isEqualTo("BC");
assertThat(appended.toString()).isEqualTo("BCD");
}
@Test
public void hashCodeAndEquals() {
AsciiBytes abcd = new AsciiBytes(new byte[] { 65, 66, 67, 68 });
@@ -163,4 +149,42 @@ public class AsciiBytesTests {
assertThat(new AsciiBytes(input).hashCode()).isEqualTo(input.hashCode());
}
@Test
public void matchesSameAsString() {
matchesSameAsString("abcABC123xyz!");
}
@Test
public void matchesSameAsStringWithSpecial() {
matchesSameAsString("special/\u00EB.dat");
}
@Test
public void matchesSameAsStringWithCyrillicCharacters() {
matchesSameAsString("\u0432\u0435\u0441\u043D\u0430");
}
@Test
public void matchesDifferentLengths() {
assertThat(new AsciiBytes("abc").matches("ab", NO_SUFFIX)).isFalse();
assertThat(new AsciiBytes("abc").matches("abcd", NO_SUFFIX)).isFalse();
assertThat(new AsciiBytes("abc").matches("abc", NO_SUFFIX)).isTrue();
assertThat(new AsciiBytes("abc").matches("a", 'b')).isFalse();
assertThat(new AsciiBytes("abc").matches("abc", 'd')).isFalse();
assertThat(new AsciiBytes("abc").matches("ab", 'c')).isTrue();
}
@Test
public void matchesSuffix() {
assertThat(new AsciiBytes("ab").matches("a", 'b')).isTrue();
}
@Test
public void matchesSameAsStringWithEmoji() {
matchesSameAsString("\ud83d\udca9");
}
private void matchesSameAsString(String input) {
assertThat(new AsciiBytes(input).matches(input, NO_SUFFIX)).isTrue();
}
}

View File

@@ -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.jar;
import org.junit.Test;
import org.springframework.boot.loader.jar.JarURLConnection.JarEntryName;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JarEntryName}.
*
* @author Andy Wilkinson
*/
public class JarEntryNameTests {
@Test
public void basicName() {
assertThat(new JarEntryName("a/b/C.class").toString()).isEqualTo("a/b/C.class");
}
@Test
public void nameWithSingleByteEncodedCharacters() {
assertThat(new JarEntryName("%61/%62/%43.class").toString())
.isEqualTo("a/b/C.class");
}
@Test
public void nameWithDoubleByteEncodedCharacters() {
assertThat(new JarEntryName("%c3%a1/b/C.class").toString())
.isEqualTo("\u00e1/b/C.class");
}
@Test
public void nameWithMixtureOfEncodedAndUnencodedDoubleByteCharacters() {
assertThat(new JarEntryName("%c3%a1/b/\u00c7.class").toString())
.isEqualTo("\u00e1/b/\u00c7.class");
}
}

View File

@@ -26,6 +26,7 @@ import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.springframework.boot.loader.TestJarCreator;
import org.springframework.boot.loader.jar.JarURLConnection.JarEntryName;
import static org.assertj.core.api.Assertions.assertThat;
@@ -155,6 +156,31 @@ public class JarURLConnectionTests {
.isEqualTo(connection.getJarEntry().getTime());
}
@Test
public void jarEntryBasicName() {
assertThat(new JarEntryName(new StringSequence("a/b/C.class")).toString())
.isEqualTo("a/b/C.class");
}
@Test
public void jarEntryNameWithSingleByteEncodedCharacters() {
assertThat(new JarEntryName(new StringSequence("%61/%62/%43.class")).toString())
.isEqualTo("a/b/C.class");
}
@Test
public void jarEntryNameWithDoubleByteEncodedCharacters() {
assertThat(new JarEntryName(new StringSequence("%c3%a1/b/C.class")).toString())
.isEqualTo("\u00e1/b/C.class");
}
@Test
public void jarEntryNameWithMixtureOfEncodedAndUnencodedDoubleByteCharacters() {
assertThat(
new JarEntryName(new StringSequence("%c3%a1/b/\u00c7.class")).toString())
.isEqualTo("\u00e1/b/\u00c7.class");
}
private String getAbsolutePath() {
return this.rootJarFile.getAbsolutePath().replace('\\', '/');
}

View File

@@ -0,0 +1,170 @@
/*
* 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.jar;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link StringSequence}.
*
* @author Phillip Webb
*/
public class StringSequenceTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void createWhenSourceIsNullShouldThrowException() {
this.thrown.expect(NullPointerException.class);
this.thrown.expectMessage("Source must not be null");
new StringSequence(null);
}
@Test
public void createWithIndexWhenSourceIsNullShouldThrowException() {
this.thrown.expect(NullPointerException.class);
this.thrown.expectMessage("Source must not be null");
new StringSequence(null, 0, 0);
}
@Test
public void createWhenStartIsLessThanZeroShouldThrowException() {
this.thrown.expect(StringIndexOutOfBoundsException.class);
new StringSequence("x", -1, 0);
}
@Test
public void createWhenEndIsGreaterThanLengthShouldThrowException() {
this.thrown.expect(StringIndexOutOfBoundsException.class);
new StringSequence("x", 0, 2);
}
@Test
public void creatFromString() {
assertThat(new StringSequence("test").toString()).isEqualTo("test");
}
@Test
public void subSequenceWithJustStartShouldReturnSubSequence() {
assertThat(new StringSequence("smiles").subSequence(1).toString())
.isEqualTo("miles");
}
@Test
public void subSequenceShouldReturnSubSequence() {
assertThat(new StringSequence("hamburger").subSequence(4, 8).toString())
.isEqualTo("urge");
assertThat(new StringSequence("smiles").subSequence(1, 5).toString())
.isEqualTo("mile");
}
@Test
public void subSequenceWhenCalledMultipleTimesShouldReturnSubSequence() {
assertThat(new StringSequence("hamburger").subSequence(4, 8).subSequence(1, 3)
.toString()).isEqualTo("rg");
}
@Test
public void subSequenceWhenEndPastExistingEndShouldThrowException() {
StringSequence sequence = new StringSequence("abcde").subSequence(1, 4);
assertThat(sequence.toString()).isEqualTo("bcd");
assertThat(sequence.subSequence(2, 3).toString()).isEqualTo("d");
this.thrown.expect(IndexOutOfBoundsException.class);
sequence.subSequence(3, 4);
}
@Test
public void subSequenceWhenStartPastExistingEndShouldThrowException() {
StringSequence sequence = new StringSequence("abcde").subSequence(1, 4);
assertThat(sequence.toString()).isEqualTo("bcd");
assertThat(sequence.subSequence(2, 3).toString()).isEqualTo("d");
this.thrown.expect(IndexOutOfBoundsException.class);
sequence.subSequence(4, 3);
}
@Test
public void isEmptyWhenEmptyShouldReturnTrue() {
assertThat(new StringSequence("").isEmpty()).isTrue();
}
@Test
public void isEmptyWhenNotEmptyShouldReturnFalse() {
assertThat(new StringSequence("x").isEmpty()).isFalse();
}
@Test
public void lengthShouldReturnLength() {
StringSequence sequence = new StringSequence("hamburger");
assertThat(sequence.length()).isEqualTo(9);
assertThat(sequence.subSequence(4, 8).length()).isEqualTo(4);
}
@Test
public void charAtShouldReturnChar() {
StringSequence sequence = new StringSequence("hamburger");
assertThat(sequence.charAt(0)).isEqualTo('h');
assertThat(sequence.charAt(1)).isEqualTo('a');
assertThat(sequence.subSequence(4, 8).charAt(0)).isEqualTo('u');
assertThat(sequence.subSequence(4, 8).charAt(1)).isEqualTo('r');
}
@Test
public void indexOfCharShouldReturnIndexOf() {
StringSequence sequence = new StringSequence("aabbaacc");
assertThat(sequence.indexOf('a')).isEqualTo(0);
assertThat(sequence.indexOf('b')).isEqualTo(2);
assertThat(sequence.subSequence(2).indexOf('a')).isEqualTo(2);
}
@Test
public void indexOfStringShouldReturnIndexOf() {
StringSequence sequence = new StringSequence("aabbaacc");
assertThat(sequence.indexOf("a")).isEqualTo(0);
assertThat(sequence.indexOf("b")).isEqualTo(2);
assertThat(sequence.subSequence(2).indexOf("a")).isEqualTo(2);
}
@Test
public void indexOfStringFromIndexShouldReturnIndexOf() {
StringSequence sequence = new StringSequence("aabbaacc");
assertThat(sequence.indexOf("a", 2)).isEqualTo(4);
assertThat(sequence.indexOf("b", 3)).isEqualTo(3);
assertThat(sequence.subSequence(2).indexOf("a", 3)).isEqualTo(3);
}
@Test
public void hashCodeShouldBeSameAsString() {
assertThat(new StringSequence("hamburger").hashCode())
.isEqualTo("hamburger".hashCode());
assertThat(new StringSequence("hamburger").subSequence(4, 8).hashCode())
.isEqualTo("urge".hashCode());
}
@Test
public void equalsWhenSameContentShouldMatch() {
StringSequence a = new StringSequence("hamburger").subSequence(4, 8);
StringSequence b = new StringSequence("urge");
StringSequence c = new StringSequence("urgh");
assertThat(a).isEqualTo(b).isNotEqualTo(c);
}
}