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
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
public DefaultPropertyNamePatternsMatcher(Set<String> names) {
|
||||
protected DefaultPropertyNamePatternsMatcher(char[] delimiters, boolean ignoreCase,
|
||||
String... names) {
|
||||
this(delimiters, ignoreCase, new HashSet<String>(Arrays.asList(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();
|
||||
|
||||
Reference in New Issue
Block a user