Reduce ConfigurationPropertyName memory usage
Significantly rework `ConfigurationPropertyName` in an attempt to reduce the amount of memory and garbage produced. The name elements are now stored as CharSequences and whenever possible subsequences are used. This helps to reduce the memory footprint since the underlying char array can be shared between the source string, and the individual elements. For example: `ConfigurationProperty.of("foo.bar.baz")` will return a name that provides access to the elements `foo`, `bar` and `baz`. However, these three names all share the same char[], just using different offsets and lengths. See gh-9000pull/9145/head
parent
d969ebad07
commit
961d41f6f6
@ -1,181 +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.context.properties.source;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Element;
|
||||
import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
|
||||
/**
|
||||
* Builder class that can be used to create {@link ConfigurationPropertyName
|
||||
* ConfigurationPropertyNames}. This class is intended for use within custom
|
||||
* {@link ConfigurationPropertySource} implementations. When accessing
|
||||
* {@link ConfigurationProperty properties} from and existing
|
||||
* {@link ConfigurationPropertySource source} the
|
||||
* {@link ConfigurationPropertyName#of(String)} method should be used to obtain a
|
||||
* {@link ConfigurationPropertyName name}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Madhura Bhave
|
||||
* @see ConfigurationPropertyName
|
||||
*/
|
||||
class ConfigurationPropertyNameBuilder {
|
||||
|
||||
private static final Element INDEX_ZERO_ELEMENT = new Element("[0]");
|
||||
|
||||
private final ElementValueProcessor processor;
|
||||
|
||||
private final Map<String, ConfigurationPropertyName> nameCache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
ConfigurationPropertyNameBuilder() {
|
||||
this(ElementValueProcessor.identity());
|
||||
}
|
||||
|
||||
ConfigurationPropertyNameBuilder(ElementValueProcessor processor) {
|
||||
Assert.notNull(processor, "Processor must not be null");
|
||||
this.processor = processor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build using the specified name split up into elements using a known separator. For
|
||||
* example {@code from("foo.bar", '.')} will return a new builder containing the
|
||||
* elements "{@code foo}" and "{@code bar}". Any element in square brackets will be
|
||||
* considered "indexed" and will not be considered for splitting.
|
||||
* @param name the name build from
|
||||
* @param separator the separator
|
||||
* @return a builder with elements populated from the name
|
||||
*/
|
||||
public ConfigurationPropertyName from(String name, char separator) {
|
||||
Assert.notNull(name, "Name must not be null");
|
||||
ConfigurationPropertyName result = this.nameCache.get(name);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
List<Element> elements = new ArrayList<>();
|
||||
StringBuilder value = new StringBuilder(name.length());
|
||||
boolean indexed = false;
|
||||
for (int i = 0; i < name.length(); i++) {
|
||||
char ch = name.charAt(i);
|
||||
if (!indexed) {
|
||||
if (ch == '[') {
|
||||
addElement(elements, value);
|
||||
value.append(ch);
|
||||
indexed = true;
|
||||
}
|
||||
else if (ch == separator) {
|
||||
addElement(elements, value);
|
||||
}
|
||||
else {
|
||||
value.append(ch);
|
||||
}
|
||||
}
|
||||
else {
|
||||
value.append(ch);
|
||||
if (ch == ']') {
|
||||
addElement(elements, value);
|
||||
indexed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
addElement(elements, value);
|
||||
result = from(elements.stream().filter(Objects::nonNull)
|
||||
.filter((e) -> !e.getValue(Form.UNIFORM).isEmpty()).iterator());
|
||||
this.nameCache.put(name, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void addElement(List<Element> elements, StringBuilder value) {
|
||||
if (value.length() > 0) {
|
||||
elements.add(buildElement(value.toString()));
|
||||
value.setLength(0);
|
||||
}
|
||||
}
|
||||
|
||||
private ConfigurationPropertyName from(Iterator<Element> elements) {
|
||||
ConfigurationPropertyName name = null;
|
||||
while (elements.hasNext()) {
|
||||
name = new ConfigurationPropertyName(name, elements.next());
|
||||
}
|
||||
Assert.state(name != null, "At least one element must be defined");
|
||||
return name;
|
||||
}
|
||||
|
||||
public ConfigurationPropertyName from(ConfigurationPropertyName parent, int index) {
|
||||
if (index == 0) {
|
||||
return new ConfigurationPropertyName(parent, INDEX_ZERO_ELEMENT);
|
||||
}
|
||||
return from(parent, "[" + index + "]");
|
||||
|
||||
}
|
||||
|
||||
public ConfigurationPropertyName from(ConfigurationPropertyName parent,
|
||||
String elementValue) {
|
||||
return new ConfigurationPropertyName(parent, buildElement(elementValue));
|
||||
}
|
||||
|
||||
private Element buildElement(String value) {
|
||||
return new Element(this.processor.apply(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* An processor that will be applied to element values. Can be used to manipulate or
|
||||
* restrict the values that are used.
|
||||
*/
|
||||
public interface ElementValueProcessor {
|
||||
|
||||
/**
|
||||
* Apply the processor to the specified value.
|
||||
* @param value the value to process
|
||||
* @return the processed value
|
||||
* @throws RuntimeException if the value cannot be used
|
||||
*/
|
||||
String apply(String value) throws RuntimeException;
|
||||
|
||||
/**
|
||||
* Return an empty {@link ElementValueProcessor} that simply returns the original
|
||||
* value unchanged.
|
||||
* @return an empty {@link ElementValueProcessor}.
|
||||
*/
|
||||
static ElementValueProcessor identity() {
|
||||
return (value) -> value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend this processor with a to enforce standard element name rules.
|
||||
* @return an element processor that additionally enforces a valid name
|
||||
*/
|
||||
default ElementValueProcessor withValidName() {
|
||||
return (value) -> {
|
||||
value = apply(value);
|
||||
if (!Element.isValid(value)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Element value '" + value + "' is not valid");
|
||||
}
|
||||
return value;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,116 +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.context.properties.source;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Element;
|
||||
import org.springframework.boot.context.properties.source.ConfigurationPropertyNameBuilder.ElementValueProcessor;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ConfigurationPropertyNameBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Madhura Bhave
|
||||
*/
|
||||
public class ConfigurationPropertyNameBuilderTests {
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
private ConfigurationPropertyNameBuilder builder;
|
||||
|
||||
@Test
|
||||
public void createWhenElementProcessorIsNullShouldThrowException() throws Exception {
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("Processor must not be null");
|
||||
this.builder = new ConfigurationPropertyNameBuilder((ElementValueProcessor) null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildShouldCreateName() throws Exception {
|
||||
this.builder = new ConfigurationPropertyNameBuilder();
|
||||
ConfigurationPropertyName expected = ConfigurationPropertyName.of("foo.bar.baz");
|
||||
ConfigurationPropertyName name = this.builder.from("foo.bar.baz", '.');
|
||||
assertThat(name.toString()).isEqualTo(expected.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildShouldValidateProcessor() {
|
||||
this.builder = new ConfigurationPropertyNameBuilder(
|
||||
ElementValueProcessor.identity().withValidName());
|
||||
this.thrown.expect(IllegalArgumentException.class);
|
||||
this.thrown.expectMessage("Element value 'foo@!' is not valid");
|
||||
this.builder.from("foo@!.bar", '.');
|
||||
}
|
||||
|
||||
@Test
|
||||
public void buildShouldUseElementProcessor() throws Exception {
|
||||
this.builder = new ConfigurationPropertyNameBuilder(
|
||||
value -> value.replace("-", ""));
|
||||
ConfigurationPropertyName name = this.builder.from("FOO_THE-BAR", '_');
|
||||
assertThat(name.toString()).isEqualTo("foo.thebar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromNameShouldSetElements() throws Exception {
|
||||
this.builder = new ConfigurationPropertyNameBuilder();
|
||||
ConfigurationPropertyName name = this.builder.from("foo.bar", '.');
|
||||
assertThat(name.toString()).isEqualTo("foo.bar");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fromNameShouldSetIndexedElements() throws Exception {
|
||||
this.builder = new ConfigurationPropertyNameBuilder();
|
||||
assertThat(getElements("foo")).isEqualTo(elements("foo"));
|
||||
assertThat(getElements("[foo]")).isEqualTo(elements("[foo]"));
|
||||
assertThat(getElements("foo.bar")).isEqualTo(elements("foo", "bar"));
|
||||
assertThat(getElements("foo[foo.bar]")).isEqualTo(elements("foo", "[foo.bar]"));
|
||||
assertThat(getElements("foo.[bar].baz"))
|
||||
.isEqualTo(elements("foo", "[bar]", "baz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void appendShouldAppendElement() throws Exception {
|
||||
this.builder = new ConfigurationPropertyNameBuilder();
|
||||
ConfigurationPropertyName parent = this.builder.from("foo.bar", '.');
|
||||
ConfigurationPropertyName name = this.builder.from(parent, "baz");
|
||||
assertThat(name.toString()).isEqualTo("foo.bar.baz");
|
||||
}
|
||||
|
||||
private List<Element> elements(String... elements) {
|
||||
return Arrays.stream(elements).map(Element::new).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private List<Element> getElements(String name) {
|
||||
List<Element> elements = new ArrayList<>();
|
||||
for (Element element : this.builder.from(name, '.')) {
|
||||
elements.add(element);
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue