Skip to content

Add @ConfigurationPropertyValue annotation #8385

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,9 @@ your application, as shown in the following example:

private InetAddress remoteAddress;

@ConfigurationPropertyValue(fallback="acmeCorp.companyName")
private String companyName;

private final Security security = new Security();

public boolean isEnabled() { ... }
Expand All @@ -914,6 +917,10 @@ your application, as shown in the following example:

public void setRemoteAddress(InetAddress remoteAddress) { ... }

public String getCompanyName() { ... }

public void setCompanyName(String companyName) { ... }

public Security getSecurity() { ... }

public static class Security {
Expand Down Expand Up @@ -944,6 +951,8 @@ The preceding POJO defines the following properties:

* `acme.enabled`, `false` by default.
* `acme.remote-address`, with a type that can be coerced from `String`.
* `acme.companyName`, with a `String` value that is read from the property `acmeCorp.companyName` if
`acme.companyName` has not been set
* `acme.security.username`, with a nested "security" object whose name is determined by
the name of the property. In particular, the return type is not used at all there and
could have been `SecurityProperties`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@

package org.springframework.boot.context.properties;

import java.util.Map;

import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver;
import org.springframework.boot.context.properties.bind.handler.FallbackBindHandler;
import org.springframework.boot.context.properties.bind.handler.IgnoreErrorsBindHandler;
import org.springframework.boot.context.properties.bind.handler.NoUnboundElementsBindHandler;
import org.springframework.boot.context.properties.bind.validation.ValidationBindHandler;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
import org.springframework.boot.context.properties.source.UnboundElementsSourceFilter;
Expand Down Expand Up @@ -70,10 +74,12 @@ void bind(Object target, ConfigurationProperties annotation) {
Binder binder = new Binder(this.configurationSources,
new PropertySourcesPlaceholdersResolver(this.propertySources),
this.conversionService);
Validator validator = determineValidator(target);
BindHandler handler = getBindHandler(annotation, validator);
Bindable<?> bindable = Bindable.ofInstance(target);
try {
Map<ConfigurationPropertyName, ConfigurationPropertyName> fallbacks = getConfigurationPropertyFallbacks(
target, annotation.prefix());
Validator validator = determineValidator(target);
BindHandler handler = getBindHandler(annotation, validator, fallbacks);
Bindable<?> bindable = Bindable.ofInstance(target);
binder.bind(annotation.prefix(), bindable, handler);
}
catch (Exception ex) {
Expand All @@ -97,7 +103,8 @@ private Validator determineValidator(Object bean) {
}

private BindHandler getBindHandler(ConfigurationProperties annotation,
Validator validator) {
Validator validator,
Map<ConfigurationPropertyName, ConfigurationPropertyName> fallbacks) {
BindHandler handler = BindHandler.DEFAULT;
if (annotation.ignoreInvalidFields()) {
handler = new IgnoreErrorsBindHandler(handler);
Expand All @@ -109,6 +116,9 @@ private BindHandler getBindHandler(ConfigurationProperties annotation,
if (validator != null) {
handler = new ValidationBindHandler(handler, validator);
}
if (!fallbacks.isEmpty()) {
handler = new FallbackBindHandler(fallbacks);
}
return handler;
}

Expand All @@ -123,6 +133,13 @@ private String getAnnotationDetails(ConfigurationProperties annotation) {
return details.toString();
}

private Map<ConfigurationPropertyName, ConfigurationPropertyName> getConfigurationPropertyFallbacks(
Object bean, String prefix) {
ConfigurationPropertyValueReader annotationReader = new ConfigurationPropertyValueReader(
bean, prefix);
return annotationReader.getPropertyFallbacks();
}

/**
* {@link Validator} implementation that wraps {@link Validator} instances and chains
* their execution.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* 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;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Annotation to be used on accessor methods or fields when the class is annotated with
* {@link ConfigurationProperties}. It allows to specify additional metadata for a single
* property.
* <p>
* Annotating a method that is not a getter or setter in the sense of the JavaBeans spec
* will cause an exception.
*
* @author Tom Hombergs
* @since 2.0.0
* @see ConfigurationProperties
*/
@Target({ ElementType.FIELD, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigurationPropertyValue {

/**
* Name of the property whose value to use if the property with the name of the
* annotated field itself is not defined.
* <p>
* The fallback property name has to be specified including potential prefixes defined
* in {@link ConfigurationProperties} annotations.
*
* @return the name of the fallback property
*/
String fallback() default "";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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;

import org.springframework.util.ClassUtils;

/**
* Exception thrown when a {@code @ConfigurationPropertyValue} annotation on a field or
* method conflicts with another annotation.
*
* @author Tom Hombergs
* @since 2.0.0
*/
public final class ConfigurationPropertyValueBindingException extends RuntimeException {

private ConfigurationPropertyValueBindingException(String message) {
super(message);
}

public static ConfigurationPropertyValueBindingException invalidUseOnMethod(
Class<?> targetClass, String methodName, String reason) {
return new ConfigurationPropertyValueBindingException(String.format(
"Invalid use of annotation %s on method '%s' of class '%s': %s",
ClassUtils.getShortName(ConfigurationPropertyValue.class), methodName,
targetClass.getName(), reason));
}

public static ConfigurationPropertyValueBindingException invalidUseOnField(
Class<?> targetClass, String fieldName, String reason) {
return new ConfigurationPropertyValueBindingException(String.format(
"Invalid use of annotation %s on field '%s' of class '%s': %s",
ClassUtils.getShortName(ConfigurationPropertyValue.class), fieldName,
targetClass.getName(), reason));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* 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;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

/**
* Utility class that reads
* {@link org.springframework.boot.context.properties.ConfigurationPropertyValue}
* annotations from getters and fields of a bean and provides methods to access the
* metadata contained in the annotations.
*
* @author Tom Hombergs
* @since 2.0.0
* @see org.springframework.boot.context.properties.ConfigurationPropertyValue
*/
public class ConfigurationPropertyValueReader {

private Map<ConfigurationPropertyName, ConfigurationPropertyValue> annotations = new HashMap<>();

public ConfigurationPropertyValueReader(Object bean, String prefix) {
this.annotations = findAnnotations(bean, prefix);
}

/**
* Returns a map that maps configuration property names to their fallback properties.
* If this map does not contain a value for a certain configuration property, it means
* that there is no fallback specified for this property.
*
* @return a map of configuration property names to their fallback property names.
*/
public Map<ConfigurationPropertyName, ConfigurationPropertyName> getPropertyFallbacks() {
return this.annotations.entrySet().stream()
.filter(entry -> !StringUtils.isEmpty(entry.getValue().fallback()))
.collect(Collectors.toMap(Map.Entry::getKey,
entry -> ConfigurationPropertyName
.of(entry.getValue().fallback())));
}

/**
* Walks through the methods and fields of the specified bean to extract
* {@link ConfigurationPropertyValue} annotations. A field can be annotated either by
* directly annotating the field or by annotating the corresponding getter or setter
* method. This method will throw a {@link BeanCreationException} if multiple
* annotations are found for the same field.
*
* @param bean the bean whose annotations to retrieve.
* @param prefix the prefix of the superordinate {@link ConfigurationProperties}
* annotation. May be null.
* @return a map that maps configuration property names to the annotations that were
* found for them.
* @throws ConfigurationPropertyValueBindingException if multiple
* {@link ConfigurationPropertyValue} annotations have been found for the same field.
*/
private Map<ConfigurationPropertyName, ConfigurationPropertyValue> findAnnotations(
Object bean, String prefix) {
Map<ConfigurationPropertyName, ConfigurationPropertyValue> fieldAnnotations = new HashMap<>();
ReflectionUtils.doWithMethods(bean.getClass(), method -> {
ConfigurationPropertyValue annotation = AnnotationUtils.findAnnotation(method,
ConfigurationPropertyValue.class);
if (annotation != null) {
PropertyDescriptor propertyDescriptor = findPropertyDescriptorOrFail(
method);
ConfigurationPropertyName name = getConfigurationPropertyName(prefix,
propertyDescriptor.getName());
if (fieldAnnotations.containsKey(name)) {
throw ConfigurationPropertyValueBindingException.invalidUseOnMethod(
bean.getClass(), method.getName(),
"You may either annotate a field, a getter or a setter but not two of these.");
}
fieldAnnotations.put(name, annotation);
}
});

ReflectionUtils.doWithFields(bean.getClass(), field -> {
ConfigurationPropertyValue annotation = AnnotationUtils.findAnnotation(field,
ConfigurationPropertyValue.class);
if (annotation != null) {
ConfigurationPropertyName name = getConfigurationPropertyName(prefix,
field.getName());
if (fieldAnnotations.containsKey(name)) {
throw ConfigurationPropertyValueBindingException.invalidUseOnField(
bean.getClass(), field.getName(),
"You may either annotate a field, a getter or a setter but not two of these.");
}
fieldAnnotations.put(name, annotation);
}
});
return fieldAnnotations;
}

private PropertyDescriptor findPropertyDescriptorOrFail(Method method) {
PropertyDescriptor propertyDescriptor = BeanUtils.findPropertyForMethod(method);
if (propertyDescriptor == null) {
throw ConfigurationPropertyValueBindingException.invalidUseOnMethod(
method.getDeclaringClass(), method.getName(),
"This annotation may only be used on getter and setter methods or fields.");
}
return propertyDescriptor;
}

private ConfigurationPropertyName getConfigurationPropertyName(String prefix,
String fieldName) {
if (StringUtils.isEmpty(prefix)) {
return ConfigurationPropertyName.of(fieldName);
}
else {
return ConfigurationPropertyName.of(prefix + "." + fieldName);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,9 @@ public void onFinish(ConfigurationPropertyName name, Bindable<?> target,
this.parent.onFinish(name, target, context, result);
}

@Override
public Object onNull(ConfigurationPropertyName name, Bindable<?> target,
BindContext context) {
return this.parent.onNull(name, target, context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,4 +89,16 @@ default void onFinish(ConfigurationPropertyName name, Bindable<?> target,
BindContext context, Object result) throws Exception {
}

/**
* Called when binding resolves to null.
* @param name the name of the element being bound
* @param target the item being bound
* @param context the bind context
* @return the actual result that should be used instead of the null value.
*/
default Object onNull(ConfigurationPropertyName name, Bindable<?> target,
BindContext context) {
return null;
}

}
Loading