Commit 12b876fb authored by Phillip Webb's avatar Phillip Webb

Reduce PropertySource access when binding

Update `PropertiesConfigurationFactory` so that when possible fewer
calls are made to the underlying `PropertySource`. The
`PropertySourcesPropertyValues` class now accepts a matcher which is
used to limit the properties that should be adapted. The factory will
create a matcher based on the standard relaxed binding rules.

Fixes gh-3402
See gh-3515
parent 1d31d23e
......@@ -21,22 +21,34 @@ import java.util.HashSet;
import java.util.Set;
/**
* Default {@link PropertyNamePatternsMatcher} that matches when a property name exactly
* matches one of the given names, or starts with one of the given names followed by '.'
* or '_'. This implementation is optimized for frequent calls.
* {@link PropertyNamePatternsMatcher} that matches when a property name exactly matches
* one of the given names, or starts with one of the given names followed by a delimiter.
* This implementation is optimized for frequent calls.
*
* @author Phillip Webb
* @since 1.2.0
*/
class DefaultPropertyNamePatternsMatcher implements PropertyNamePatternsMatcher {
private final char[] delimiters;
private final boolean ignoreCase;
private final String[] names;
public DefaultPropertyNamePatternsMatcher(String... names) {
this(new HashSet<String>(Arrays.asList(names)));
protected DefaultPropertyNamePatternsMatcher(char[] delimiters, String... names) {
this(delimiters, false, names);
}
protected DefaultPropertyNamePatternsMatcher(char[] delimiters, boolean ignoreCase,
String... names) {
this(delimiters, ignoreCase, new HashSet<String>(Arrays.asList(names)));
}
public DefaultPropertyNamePatternsMatcher(Set<String> names) {
public DefaultPropertyNamePatternsMatcher(char[] delimiters, boolean ignoreCase,
Set<String> names) {
this.delimiters = delimiters;
this.ignoreCase = ignoreCase;
this.names = names.toArray(new String[names.size()]);
}
......@@ -55,18 +67,19 @@ class DefaultPropertyNamePatternsMatcher implements PropertyNamePatternsMatcher
return false;
}
for (int charIndex = 0; charIndex < propertyNameChars.length; charIndex++) {
noneMatched = true;
for (int nameIndex = 0; nameIndex < this.names.length; nameIndex++) {
if (match[nameIndex]) {
match[nameIndex] = false;
if (charIndex < this.names[nameIndex].length()) {
if (this.names[nameIndex].charAt(charIndex) == propertyNameChars[charIndex]) {
if (isCharMatch(this.names[nameIndex].charAt(charIndex),
propertyNameChars[charIndex])) {
match[nameIndex] = true;
noneMatched = false;
}
}
else {
char charAfter = propertyNameChars[this.names[nameIndex].length()];
if (charAfter == '.' || charAfter == '_') {
if (isDelimeter(charAfter)) {
match[nameIndex] = true;
noneMatched = false;
}
......@@ -85,4 +98,20 @@ class DefaultPropertyNamePatternsMatcher implements PropertyNamePatternsMatcher
return false;
}
private boolean isCharMatch(char c1, char c2) {
if (this.ignoreCase) {
return Character.toLowerCase(c1) == Character.toLowerCase(c2);
}
return c1 == c2;
}
private boolean isDelimeter(char c) {
for (char delimiter : this.delimiters) {
if (c == delimiter) {
return true;
}
}
return false;
}
}
......@@ -27,11 +27,11 @@ import org.springframework.util.PatternMatchUtils;
* @author Phillip Webb
* @since 1.2.0
*/
class SimplePropertyNamePatternsMatcher implements PropertyNamePatternsMatcher {
class PatternPropertyNamePatternsMatcher implements PropertyNamePatternsMatcher {
private final String[] patterns;
public SimplePropertyNamePatternsMatcher(Collection<String> patterns) {
public PatternPropertyNamePatternsMatcher(Collection<String> patterns) {
this.patterns = (patterns == null ? new String[] {} : patterns
.toArray(new String[patterns.size()]));
}
......
......@@ -17,8 +17,9 @@
package org.springframework.boot.bind;
import java.beans.PropertyDescriptor;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
......@@ -34,6 +35,7 @@ import org.springframework.context.MessageSourceAware;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.env.PropertySources;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.DataBinder;
......@@ -51,6 +53,10 @@ import org.springframework.validation.Validator;
public class PropertiesConfigurationFactory<T> implements FactoryBean<T>,
MessageSourceAware, InitializingBean {
private static final char[] EXACT_DELIMETERS = { '_', '.', '[' };
private static final char[] TARGET_NAME_DELIMETERS = { '_', '.' };
private final Log logger = LogFactory.getLog(getClass());
private boolean ignoreUnknownFields = true;
......@@ -257,16 +263,28 @@ public class PropertiesConfigurationFactory<T> implements FactoryBean<T>,
}
private Set<String> getNames() {
Set<String> names = new HashSet<String>();
Set<String> names = new LinkedHashSet<String>();
if (this.target != null) {
Iterable<String> prefixes = (StringUtils.hasLength(this.targetName) ? new RelaxedNames(
this.targetName) : null);
PropertyDescriptor[] descriptors = BeanUtils
.getPropertyDescriptors(this.target.getClass());
String prefix = (this.targetName != null ? this.targetName + "." : "");
for (PropertyDescriptor descriptor : descriptors) {
String name = descriptor.getName();
if (!name.equals("class")) {
for (String relaxedName : new RelaxedNames(prefix + name)) {
names.add(relaxedName);
RelaxedNames relaxedNames = RelaxedNames.forCamelCase(name);
if (prefixes == null) {
for (String relaxedName : relaxedNames) {
names.add(relaxedName);
}
}
else {
for (String prefix : prefixes) {
for (String relaxedName : relaxedNames) {
names.add(prefix + "." + relaxedName);
names.add(prefix + "_" + relaxedName);
}
}
}
}
}
......@@ -278,8 +296,33 @@ public class PropertiesConfigurationFactory<T> implements FactoryBean<T>,
if (this.properties != null) {
return new MutablePropertyValues(this.properties);
}
return new PropertySourcesPropertyValues(this.propertySources,
new DefaultPropertyNamePatternsMatcher(names), names);
return getPropertySourcesPropertyValues(names);
}
private PropertyValues getPropertySourcesPropertyValues(Set<String> names) {
PropertyNamePatternsMatcher includes = getPropertyNamePatternsMatcher(names);
return new PropertySourcesPropertyValues(this.propertySources, names, includes);
}
private PropertyNamePatternsMatcher getPropertyNamePatternsMatcher(Set<String> names) {
if (this.ignoreUnknownFields && !isMapTarget()) {
// Since unknown fields are ignored we can filter them out early to save
// unnecessary calls to the PropertySource.
return new DefaultPropertyNamePatternsMatcher(EXACT_DELIMETERS, true, names);
}
if (this.targetName != null) {
// We can filter properties to those starting with the target name, but
// we can't do a complete filter since we need to trigger the
// unknown fields check
return new DefaultPropertyNamePatternsMatcher(TARGET_NAME_DELIMETERS,
this.targetName);
}
// Not ideal, we basically can't filter anything
return PropertyNamePatternsMatcher.ALL;
}
private boolean isMapTarget() {
return this.target != null && Map.class.isAssignableFrom(this.target.getClass());
}
private void validate(RelaxedDataBinder dataBinder) throws BindException {
......
......@@ -24,6 +24,15 @@ package org.springframework.boot.bind;
*/
interface PropertyNamePatternsMatcher {
PropertyNamePatternsMatcher ALL = new PropertyNamePatternsMatcher() {
@Override
public boolean matches(String propertyName) {
return true;
}
};
PropertyNamePatternsMatcher NONE = new PropertyNamePatternsMatcher() {
@Override
......
......@@ -16,7 +16,6 @@
package org.springframework.boot.bind;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
......@@ -30,7 +29,7 @@ import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.PropertySources;
import org.springframework.core.env.PropertySourcesPropertyResolver;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.util.Assert;
import org.springframework.validation.DataBinder;
/**
......@@ -46,17 +45,16 @@ public class PropertySourcesPropertyValues implements PropertyValues {
private final PropertySources propertySources;
private static final Collection<String> PATTERN_MATCHED_PROPERTY_SOURCES = Arrays
.asList(StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
StandardEnvironment.SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME);
private final Collection<String> propertyNames;
private final PropertyNamePatternsMatcher includes;
/**
* Create a new PropertyValues from the given PropertySources
* @param propertySources a PropertySources instance
*/
public PropertySourcesPropertyValues(PropertySources propertySources) {
this(propertySources, (PropertyNamePatternsMatcher) null,
(Collection<String>) null);
this(propertySources, (Collection<String>) null, PropertyNamePatternsMatcher.ALL);
}
/**
......@@ -64,52 +62,55 @@ public class PropertySourcesPropertyValues implements PropertyValues {
* @param propertySources a PropertySources instance
* @param includePatterns property name patterns to include from system properties and
* environment variables
* @param names exact property names to include
* @param propertyNames the property names to used in lieu of an
* {@link EnumerablePropertySource}.
*/
public PropertySourcesPropertyValues(PropertySources propertySources,
Collection<String> includePatterns, Collection<String> names) {
this(propertySources, new SimplePropertyNamePatternsMatcher(includePatterns),
names);
Collection<String> includePatterns, Collection<String> propertyNames) {
this(propertySources, propertyNames, new PatternPropertyNamePatternsMatcher(
includePatterns));
}
/**
* Create a new PropertyValues from the given PropertySources
* @param propertySources a PropertySources instance
* @param includes property name patterns to include from system properties and
* environment variables
* @param names exact property names to include
* @param propertyNames the property names to used in lieu of an
* {@link EnumerablePropertySource}.
* @param includes the property name patterns to include
*/
PropertySourcesPropertyValues(PropertySources propertySources,
PropertyNamePatternsMatcher includes, Collection<String> names) {
Collection<String> propertyNames, PropertyNamePatternsMatcher includes) {
Assert.notNull(propertySources, "PropertySources must not be null");
Assert.notNull(includes, "Includes must not be null");
this.propertySources = propertySources;
if (includes == null) {
includes = PropertyNamePatternsMatcher.NONE;
}
if (names == null) {
names = Collections.emptySet();
}
this.propertyNames = (propertyNames == null ? Collections.<String> emptySet()
: propertyNames);
this.includes = includes;
PropertySourcesPropertyResolver resolver = new PropertySourcesPropertyResolver(
propertySources);
for (PropertySource<?> source : propertySources) {
processPropertySource(source, resolver, includes, names);
processPropertySource(source, resolver);
}
}
private void processPropertySource(PropertySource<?> source,
PropertySourcesPropertyResolver resolver,
PropertyNamePatternsMatcher includes, Collection<String> exacts) {
PropertySourcesPropertyResolver resolver) {
if (source instanceof CompositePropertySource) {
processCompositePropertySource((CompositePropertySource) source, resolver,
includes, exacts);
processCompositePropertySource((CompositePropertySource) source, resolver);
}
else if (source instanceof EnumerablePropertySource) {
processEnumerablePropertySource((EnumerablePropertySource<?>) source,
resolver, includes);
resolver, this.includes);
}
else {
// We can only do exact matches for non-enumerable property names, but
// that's better than nothing...
processDefaultPropertySource(source, resolver, includes, exacts);
processNonEnumerablePropertySource(source, resolver);
}
}
private void processCompositePropertySource(CompositePropertySource source,
PropertySourcesPropertyResolver resolver) {
for (PropertySource<?> nested : source.getPropertySources()) {
processPropertySource(nested, resolver);
}
}
......@@ -117,14 +118,9 @@ public class PropertySourcesPropertyValues implements PropertyValues {
PropertySourcesPropertyResolver resolver, PropertyNamePatternsMatcher includes) {
if (source.getPropertyNames().length > 0) {
for (String propertyName : source.getPropertyNames()) {
if (PropertySourcesPropertyValues.PATTERN_MATCHED_PROPERTY_SOURCES
.contains(source.getName()) && !includes.matches(propertyName)) {
continue;
}
Object value = getEnumerableProperty(source, resolver, propertyName);
if (!this.propertyValues.containsKey(propertyName)) {
this.propertyValues.put(propertyName, new PropertyValue(propertyName,
value));
if (includes.matches(propertyName)) {
Object value = getEnumerableProperty(source, resolver, propertyName);
putIfAbsent(propertyName, value);
}
}
}
......@@ -141,18 +137,11 @@ public class PropertySourcesPropertyValues implements PropertyValues {
}
}
private void processCompositePropertySource(CompositePropertySource source,
PropertySourcesPropertyResolver resolver,
PropertyNamePatternsMatcher includes, Collection<String> exacts) {
for (PropertySource<?> nested : source.getPropertySources()) {
processPropertySource(nested, resolver, includes, exacts);
}
}
private void processDefaultPropertySource(PropertySource<?> source,
PropertySourcesPropertyResolver resolver,
PropertyNamePatternsMatcher includes, Collection<String> exacts) {
for (String propertyName : exacts) {
private void processNonEnumerablePropertySource(PropertySource<?> source,
PropertySourcesPropertyResolver resolver) {
// We can only do exact matches for non-enumerable property names, but
// that's better than nothing...
for (String propertyName : this.propertyNames) {
if (!source.containsProperty(propertyName)) {
continue;
}
......@@ -166,11 +155,13 @@ public class PropertySourcesPropertyValues implements PropertyValues {
if (value == null) {
value = source.getProperty(propertyName.toUpperCase());
}
if (value != null && !this.propertyValues.containsKey(propertyName)) {
this.propertyValues.put(propertyName, new PropertyValue(propertyName,
value));
continue;
}
putIfAbsent(propertyName, value);
}
}
private void putIfAbsent(String propertyName, Object value) {
if (value != null && !this.propertyValues.containsKey(propertyName)) {
this.propertyValues.put(propertyName, new PropertyValue(propertyName, value));
}
}
......
......@@ -188,4 +188,13 @@ public final class RelaxedNames implements Iterable<String> {
}
}
/**
* Return a {@link RelaxedNames} for the given source camelCase source name
* @param name the source name in camelCase
* @return the relaxed names
*/
public static RelaxedNames forCamelCase(String name) {
return new RelaxedNames(Manipulation.CAMELCASE_TO_HYPHEN.apply(name));
}
}
......@@ -28,42 +28,53 @@ import static org.junit.Assert.assertTrue;
*/
public class DefaultPropertyNamePatternsMatcherTests {
private static final char[] DELIMITERS = { '.', '_' };
@Test
public void namesShorter() {
assertFalse(new DefaultPropertyNamePatternsMatcher("aaaa", "bbbb")
assertFalse(new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaaa", "bbbb")
.matches("zzzzz"));
}
@Test
public void namesExactMatch() {
assertTrue(new DefaultPropertyNamePatternsMatcher("aaaa", "bbbb", "cccc")
.matches("bbbb"));
assertTrue(new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaaa", "bbbb",
"cccc").matches("bbbb"));
}
@Test
public void namesLonger() {
assertFalse(new DefaultPropertyNamePatternsMatcher("aaaaa", "bbbbb", "ccccc")
.matches("bbbb"));
assertFalse(new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaaaa", "bbbbb",
"ccccc").matches("bbbb"));
}
@Test
public void nameWithDot() throws Exception {
assertTrue(new DefaultPropertyNamePatternsMatcher("aaaa", "bbbb", "cccc")
.matches("bbbb.anything"));
assertTrue(new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaaa", "bbbb",
"cccc").matches("bbbb.anything"));
}
@Test
public void nameWithUnderscore() throws Exception {
assertTrue(new DefaultPropertyNamePatternsMatcher("aaaa", "bbbb", "cccc")
.matches("bbbb_anything"));
assertTrue(new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaaa", "bbbb",
"cccc").matches("bbbb_anything"));
}
@Test
public void namesMatchWithDifferentLengths() throws Exception {
assertTrue(new DefaultPropertyNamePatternsMatcher("aaa", "bbbb", "ccccc")
.matches("bbbb"));
assertTrue(new DefaultPropertyNamePatternsMatcher(DELIMITERS, "aaa", "bbbb",
"ccccc").matches("bbbb"));
}
@Test
public void withSquareBrackets() throws Exception {
char[] delimeters = "._[".toCharArray();
PropertyNamePatternsMatcher matcher = new DefaultPropertyNamePatternsMatcher(
delimeters, "aaa", "bbbb", "ccccc");
assertTrue(matcher.matches("bbbb"));
assertTrue(matcher.matches("bbbb[4]"));
assertFalse(matcher.matches("bbb[4]"));
}
}
......@@ -102,6 +102,20 @@ public class RelaxedDataBinderTests {
assertEquals("bar", target.getFoo());
}
@Test
public void testBindToCamelCaseFromEnvironmentStyleWithPrefix() throws Exception {
VanillaTarget target = new VanillaTarget();
bind(target, "TEST_FOO_BAZ: bar", "test");
assertEquals("bar", target.getFooBaz());
}
@Test
public void testBindToCamelCaseFromEnvironmentStyle() throws Exception {
VanillaTarget target = new VanillaTarget();
bind(target, "test.FOO_BAZ: bar", "test");
assertEquals("bar", target.getFooBaz());
}
@Test
public void testBindFromEnvironmentStyleWithNestedPrefix() throws Exception {
VanillaTarget target = new VanillaTarget();
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment