diff --git a/spring-core/src/jmh/java/org/springframework/util/StringUtilsBenchmark.java b/spring-core/src/jmh/java/org/springframework/util/StringUtilsBenchmark.java new file mode 100644 index 0000000000..1fc7753b22 --- /dev/null +++ b/spring-core/src/jmh/java/org/springframework/util/StringUtilsBenchmark.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2021 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 + * + * https://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.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Random; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.infra.Blackhole; + +/** + * Benchmarks for {@link StringUtils}. + * + * @author Brian Clozel + */ +@BenchmarkMode(Mode.Throughput) +public class StringUtilsBenchmark { + + @Benchmark + public void collectionToDelimitedString(DelimitedStringState state, Blackhole bh) { + bh.consume(StringUtils.collectionToCommaDelimitedString(state.elements)); + } + + @State(Scope.Benchmark) + public static class DelimitedStringState { + + @Param("10") + int elementMinSize; + + @Param("20") + int elementMaxSize; + + @Param("10") + int elementCount; + + Collection elements; + + @Setup(Level.Iteration) + public void setup() { + Random random = new Random(); + this.elements = new ArrayList<>(this.elementCount); + int bound = this.elementMaxSize - this.elementMinSize; + for (int i = 0; i < this.elementCount; i++) { + this.elements.add(String.format("%0" + (random.nextInt(bound) + this.elementMinSize) + "d", 1)); + } + } + } + + @Benchmark + public void cleanPath(CleanPathState state, Blackhole bh) { + for (String path : state.paths) { + bh.consume(StringUtils.cleanPath(path)); + } + } + + @State(Scope.Benchmark) + public static class CleanPathState { + + private static final List SEGMENTS = Arrays.asList("some", "path", ".", "..", "springspring"); + + @Param("10") + int segmentCount; + + @Param("20") + int pathsCount; + + Collection paths; + + @Setup(Level.Iteration) + public void setup() { + this.paths = new ArrayList<>(this.pathsCount); + Random random = new Random(); + for (int i = 0; i < this.pathsCount; i++) { + this.paths.add(createSamplePath(random)); + } + } + + private String createSamplePath(Random random) { + String separator = random.nextBoolean() ? "/" : "\\"; + StringBuilder sb = new StringBuilder(); + sb.append("jar:file:///c:"); + for (int i = 0; i < this.segmentCount; i++) { + sb.append(separator); + sb.append(SEGMENTS.get(random.nextInt(SEGMENTS.size()))); + } + sb.append(separator); + sb.append("the%20file.txt"); + return sb.toString(); + } + + } +} diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java index e320e9bd4c..80a996d481 100644 --- a/spring-core/src/main/java/org/springframework/util/StringUtils.java +++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java @@ -735,42 +735,11 @@ public abstract class StringUtils { pathElements.addFirst(CURRENT_PATH); } - final String joined = joinStrings(pathElements, FOLDER_SEPARATOR); + final String joined = collectionToDelimitedString(pathElements, FOLDER_SEPARATOR); // avoid string concatenation with empty prefix return prefix.isEmpty() ? joined : prefix + joined; } - /** - * Convert a {@link Collection Collection<String>} to a delimited {@code String} (e.g. CSV). - *

This is an optimized variant of {@link #collectionToDelimitedString(Collection, String)}, which does not - * require dynamic resizing of the StringBuilder's backing array. - * @param coll the {@code Collection Collection<String>} to convert (potentially {@code null} or empty) - * @param delim the delimiter to use (typically a ",") - * @return the delimited {@code String} - */ - private static String joinStrings(@Nullable Collection coll, String delim) { - - if (CollectionUtils.isEmpty(coll)) { - return ""; - } - - // precompute total length of resulting string - int totalLength = (coll.size() - 1) * delim.length(); - for (String str : coll) { - totalLength += str.length(); - } - - StringBuilder sb = new StringBuilder(totalLength); - Iterator it = coll.iterator(); - while (it.hasNext()) { - sb.append(it.next()); - if (it.hasNext()) { - sb.append(delim); - } - } - return sb.toString(); - } - /** * Compare two paths after normalization of them. * @param path1 first path for comparison @@ -1330,7 +1299,12 @@ public abstract class StringUtils { return ""; } - StringBuilder sb = new StringBuilder(); + int totalLength = coll.size() * (prefix.length() + suffix.length()) + (coll.size() - 1) * delim.length(); + for (Object element : coll) { + totalLength += element.toString().length(); + } + + StringBuilder sb = new StringBuilder(totalLength); Iterator it = coll.iterator(); while (it.hasNext()) { sb.append(prefix).append(it.next()).append(suffix);