Skip to content

Commit 8d3e8ca

Browse files
knittlbclozel
authored andcommitted
Optimize allocation in StringUtils#cleanPath
This commit applies several optimizations to StringUtils#cleanPath and related methods: * pre-size pathElements deque in StringUtils#cleanPath with pathElements.length elements, since this this is the maximum size and the most likely case. * optimize StringUtils#collectionToDelimitedString to calculate the size of the resulting String and avoid array auto-resizing in the StringBuilder. * If the path did not contain any components that required cleaning, return the (normalized) path as-is. No need to concatenate the prefix and the trailing path. See gh-26316
1 parent ae56f2a commit 8d3e8ca

File tree

2 files changed

+42
-4
lines changed

2 files changed

+42
-4
lines changed

spring-core/src/main/java/org/springframework/util/StringUtils.java

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,9 @@ public static String cleanPath(String path) {
667667
if (!hasLength(path)) {
668668
return path;
669669
}
670-
String pathToUse = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR);
670+
671+
String normalizedPath = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR);
672+
String pathToUse = normalizedPath;
671673

672674
// Shortcut if there is no work to do
673675
if (pathToUse.indexOf('.') == -1) {
@@ -695,7 +697,8 @@ public static String cleanPath(String path) {
695697
}
696698

697699
String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR);
698-
Deque<String> pathElements = new ArrayDeque<>();
700+
// we never require more elements than pathArray and in the common case the same number
701+
Deque<String> pathElements = new ArrayDeque<>(pathArray.length);
699702
int tops = 0;
700703

701704
for (int i = pathArray.length - 1; i >= 0; i--) {
@@ -721,7 +724,7 @@ else if (TOP_PATH.equals(element)) {
721724

722725
// All path elements stayed the same - shortcut
723726
if (pathArray.length == pathElements.size()) {
724-
return prefix + pathToUse;
727+
return normalizedPath;
725728
}
726729
// Remaining top paths need to be retained.
727730
for (int i = 0; i < tops; i++) {
@@ -732,7 +735,40 @@ else if (TOP_PATH.equals(element)) {
732735
pathElements.addFirst(CURRENT_PATH);
733736
}
734737

735-
return prefix + collectionToDelimitedString(pathElements, FOLDER_SEPARATOR);
738+
final String joined = joinStrings(pathElements, FOLDER_SEPARATOR);
739+
// avoid string concatenation with empty prefix
740+
return prefix.isEmpty() ? joined : prefix + joined;
741+
}
742+
743+
/**
744+
* Convert a {@link Collection Collection&lt;String&gt;} to a delimited {@code String} (e.g. CSV).
745+
* <p>This is an optimized variant of {@link #collectionToDelimitedString(Collection, String)}, which does not
746+
* require dynamic resizing of the StringBuilder's backing array.
747+
* @param coll the {@code Collection Collection&lt;String&gt;} to convert (potentially {@code null} or empty)
748+
* @param delim the delimiter to use (typically a ",")
749+
* @return the delimited {@code String}
750+
*/
751+
private static String joinStrings(@Nullable Collection<String> coll, String delim) {
752+
753+
if (CollectionUtils.isEmpty(coll)) {
754+
return "";
755+
}
756+
757+
// precompute total length of resulting string
758+
int totalLength = (coll.size() - 1) * delim.length();
759+
for (String str : coll) {
760+
totalLength += str.length();
761+
}
762+
763+
StringBuilder sb = new StringBuilder(totalLength);
764+
Iterator<?> it = coll.iterator();
765+
while (it.hasNext()) {
766+
sb.append(it.next());
767+
if (it.hasNext()) {
768+
sb.append(delim);
769+
}
770+
}
771+
return sb.toString();
736772
}
737773

738774
/**

spring-core/src/test/java/org/springframework/util/StringUtilsTests.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,8 @@ void cleanPath() {
402402
assertThat(StringUtils.cleanPath("file:.././")).isEqualTo("file:../");
403403
assertThat(StringUtils.cleanPath("file:/mypath/spring.factories")).isEqualTo("file:/mypath/spring.factories");
404404
assertThat(StringUtils.cleanPath("file:///c:/some/../path/the%20file.txt")).isEqualTo("file:///c:/path/the%20file.txt");
405+
assertThat(StringUtils.cleanPath("jar:file:///c:\\some\\..\\path\\.\\the%20file.txt")).isEqualTo("jar:file:///c:/path/the%20file.txt");
406+
assertThat(StringUtils.cleanPath("jar:file:///c:/some/../path/./the%20file.txt")).isEqualTo("jar:file:///c:/path/the%20file.txt");
405407
}
406408

407409
@Test

0 commit comments

Comments
 (0)