Skip to content

Commit c6dae57

Browse files
mbhavephilwebb
andcommitted
Add bindOrCreate for constructor based binding
Deprecate the existing `BindResult.orElseCreate` method in favor of `bindOrCreate` methods on the `Binder`. These new methods allow us to implement custom creation logic depending on the type of object being bound. Specifically, it allows constructor based binding to create new instances that respect the `@DefaultValue` annotations. Closes gh-17098 Co-authored-by: Phillip Webb <[email protected]>
1 parent 38fb639 commit c6dae57

File tree

14 files changed

+372
-59
lines changed

14 files changed

+372
-59
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/template/PathBasedTemplateAvailabilityProvider.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,7 @@ public boolean isTemplateAvailable(String view, Environment environment, ClassLo
5353
ResourceLoader resourceLoader) {
5454
if (ClassUtils.isPresent(this.className, classLoader)) {
5555
Binder binder = Binder.get(environment);
56-
TemplateAvailabilityProperties properties = binder.bind(this.propertyPrefix, this.propertiesClass)
57-
.orElseCreate(this.propertiesClass);
56+
TemplateAvailabilityProperties properties = binder.bindOrCreate(this.propertyPrefix, this.propertiesClass);
5857
return isTemplateAvailable(view, resourceLoader, properties);
5958
}
6059
return false;

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBeanDefinition.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ private static <T> Supplier<T> createBean(ConfigurableListableBeanFactory beanFa
5555
ConfigurationPropertiesBinder binder = beanFactory.getBean(ConfigurationPropertiesBinder.BEAN_NAME,
5656
ConfigurationPropertiesBinder.class);
5757
try {
58-
return binder.bind(bindable).orElseCreate(type);
58+
return binder.bindOrCreate(bindable);
5959
}
6060
catch (Exception ex) {
6161
throw new ConfigurationPropertiesBindException(beanName, type, annotation, ex);

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBinder.java

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,21 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
8686
}
8787

8888
public <T> BindResult<T> bind(Bindable<T> target) {
89+
ConfigurationProperties annotation = getAnnotation(target);
90+
BindHandler bindHandler = getBindHandler(target, annotation);
91+
return getBinder().bind(annotation.prefix(), target, bindHandler);
92+
}
93+
94+
public <T> T bindOrCreate(Bindable<T> target) {
95+
ConfigurationProperties annotation = getAnnotation(target);
96+
BindHandler bindHandler = getBindHandler(target, annotation);
97+
return getBinder().bindOrCreate(annotation.prefix(), target, bindHandler);
98+
}
99+
100+
private <T> ConfigurationProperties getAnnotation(Bindable<?> target) {
89101
ConfigurationProperties annotation = target.getAnnotation(ConfigurationProperties.class);
90102
Assert.state(annotation != null, () -> "Missing @ConfigurationProperties on " + target);
91-
List<Validator> validators = getValidators(target);
92-
BindHandler bindHandler = getBindHandler(annotation, validators);
93-
return getBinder().bind(annotation.prefix(), target, bindHandler);
103+
return annotation;
94104
}
95105

96106
private Validator getConfigurationPropertiesValidator(ApplicationContext applicationContext,
@@ -101,6 +111,25 @@ private Validator getConfigurationPropertiesValidator(ApplicationContext applica
101111
return null;
102112
}
103113

114+
private <T> BindHandler getBindHandler(Bindable<T> target, ConfigurationProperties annotation) {
115+
List<Validator> validators = getValidators(target);
116+
BindHandler handler = new IgnoreTopLevelConverterNotFoundBindHandler();
117+
if (annotation.ignoreInvalidFields()) {
118+
handler = new IgnoreErrorsBindHandler(handler);
119+
}
120+
if (!annotation.ignoreUnknownFields()) {
121+
UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter();
122+
handler = new NoUnboundElementsBindHandler(handler, filter);
123+
}
124+
if (!validators.isEmpty()) {
125+
handler = new ValidationBindHandler(handler, validators.toArray(new Validator[0]));
126+
}
127+
for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) {
128+
handler = advisor.apply(handler);
129+
}
130+
return handler;
131+
}
132+
104133
private List<Validator> getValidators(Bindable<?> target) {
105134
List<Validator> validators = new ArrayList<>(3);
106135
if (this.configurationPropertiesValidator != null) {
@@ -122,24 +151,6 @@ private Validator getJsr303Validator() {
122151
return this.jsr303Validator;
123152
}
124153

125-
private BindHandler getBindHandler(ConfigurationProperties annotation, List<Validator> validators) {
126-
BindHandler handler = new IgnoreTopLevelConverterNotFoundBindHandler();
127-
if (annotation.ignoreInvalidFields()) {
128-
handler = new IgnoreErrorsBindHandler(handler);
129-
}
130-
if (!annotation.ignoreUnknownFields()) {
131-
UnboundElementsSourceFilter filter = new UnboundElementsSourceFilter();
132-
handler = new NoUnboundElementsBindHandler(handler, filter);
133-
}
134-
if (!validators.isEmpty()) {
135-
handler = new ValidationBindHandler(handler, validators.toArray(new Validator[0]));
136-
}
137-
for (ConfigurationPropertiesBindHandlerAdvisor advisor : getBindHandlerAdvisors()) {
138-
handler = advisor.apply(handler);
139-
}
140-
return handler;
141-
}
142-
143154
private List<ConfigurationPropertiesBindHandlerAdvisor> getBindHandlerAdvisors() {
144155
return this.applicationContext.getBeanProvider(ConfigurationPropertiesBindHandlerAdvisor.class).orderedStream()
145156
.collect(Collectors.toList());

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BeanBinder.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,13 @@ interface BeanBinder {
3939
*/
4040
<T> T bind(ConfigurationPropertyName name, Bindable<T> target, Context context, BeanPropertyBinder propertyBinder);
4141

42+
/**
43+
* Return a new instance for the specified type.
44+
* @param type the type used for creating a new instance
45+
* @param context the bind context
46+
* @param <T> the source type
47+
* @return the created instance
48+
*/
49+
<T> T create(Class<T> type, Context context);
50+
4251
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindHandler.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,25 @@ default Object onSuccess(ConfigurationPropertyName name, Bindable<?> target, Bin
6060
return result;
6161
}
6262

63+
/**
64+
* Called when binding of an element ends with an unbound result and a newly created
65+
* instance is about to be returned. Implementations may change the ultimately
66+
* returned result or perform addition validation.
67+
* @param name the name of the element being bound
68+
* @param target the item being bound
69+
* @param context the bind context
70+
* @param result the newly created instance (never {@code null})
71+
* @return the actual result that should be used (must not be {@code null})
72+
* @since 2.2.2
73+
*/
74+
default Object onCreate(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
75+
return result;
76+
}
77+
6378
/**
6479
* Called when binding fails for any reason (including failures from
65-
* {@link #onSuccess} calls). Implementations may choose to swallow exceptions and
66-
* return an alternative result.
80+
* {@link #onSuccess} or {@link #onCreate} calls). Implementations may choose to
81+
* swallow exceptions and return an alternative result.
6782
* @param name the name of the element being bound
6883
* @param target the item being bound
6984
* @param context the bind context

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/BindResult.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,9 @@ public T orElseGet(Supplier<? extends T> other) {
118118
* value has been bound.
119119
* @param type the type to create if no value was bound
120120
* @return the value, if bound, otherwise a new instance of {@code type}
121+
* @deprecated since 2.2.0 in favor of {@link Binder#bindOrCreate}
121122
*/
123+
@Deprecated
122124
public T orElseCreate(Class<? extends T> type) {
123125
Assert.notNull(type, "Type must not be null");
124126
return (this.value != null) ? this.value : BeanUtils.instantiateClass(type);

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/Binder.java

Lines changed: 99 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,16 @@
1717
package org.springframework.boot.context.properties.bind;
1818

1919
import java.util.ArrayDeque;
20-
import java.util.ArrayList;
2120
import java.util.Arrays;
2221
import java.util.Collection;
2322
import java.util.Collections;
2423
import java.util.Deque;
2524
import java.util.HashSet;
2625
import java.util.List;
2726
import java.util.Map;
28-
import java.util.Objects;
2927
import java.util.Set;
3028
import java.util.function.Consumer;
3129
import java.util.function.Supplier;
32-
import java.util.stream.Stream;
3330

3431
import org.springframework.beans.PropertyEditorRegistry;
3532
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
@@ -58,14 +55,7 @@ public class Binder {
5855
private static final Set<Class<?>> NON_BEAN_CLASSES = Collections
5956
.unmodifiableSet(new HashSet<>(Arrays.asList(Object.class, Class.class)));
6057

61-
private static final List<BeanBinder> BEAN_BINDERS;
62-
63-
static {
64-
List<BeanBinder> binders = new ArrayList<>();
65-
binders.add(new ConstructorParametersBinder());
66-
binders.add(new JavaBeanBinder());
67-
BEAN_BINDERS = Collections.unmodifiableList(binders);
68-
}
58+
private static final BeanBinder[] BEAN_BINDERS = { new ConstructorParametersBinder(), new JavaBeanBinder() };
6959

7060
private final Iterable<ConfigurationPropertySource> sources;
7161

@@ -196,40 +186,122 @@ public <T> BindResult<T> bind(String name, Bindable<T> target, BindHandler handl
196186
* @return the binding result (never {@code null})
197187
*/
198188
public <T> BindResult<T> bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler) {
189+
T bound = bind(name, target, handler, false);
190+
return BindResult.of(bound);
191+
}
192+
193+
/**
194+
* Bind the specified target {@link Class} using this binder's
195+
* {@link ConfigurationPropertySource property sources} or create a new instance using
196+
* the type of the {@link Bindable} if the result of the binding is {@code null}.
197+
* @param name the configuration property name to bind
198+
* @param target the target class
199+
* @param <T> the bound type
200+
* @return the bound or created object
201+
* @since 2.2.0
202+
* @see #bind(ConfigurationPropertyName, Bindable, BindHandler)
203+
*/
204+
public <T> T bindOrCreate(String name, Class<T> target) {
205+
return bindOrCreate(name, Bindable.of(target));
206+
}
207+
208+
/**
209+
* Bind the specified target {@link Bindable} using this binder's
210+
* {@link ConfigurationPropertySource property sources} or create a new instance using
211+
* the type of the {@link Bindable} if the result of the binding is {@code null}.
212+
* @param name the configuration property name to bind
213+
* @param target the target bindable
214+
* @param <T> the bound type
215+
* @return the bound or created object
216+
* @since 2.2.0
217+
* @see #bindOrCreate(ConfigurationPropertyName, Bindable, BindHandler)
218+
*/
219+
public <T> T bindOrCreate(String name, Bindable<T> target) {
220+
return bindOrCreate(ConfigurationPropertyName.of(name), target, null);
221+
}
222+
223+
/**
224+
* Bind the specified target {@link Bindable} using this binder's
225+
* {@link ConfigurationPropertySource property sources} or create a new instance using
226+
* the type of the {@link Bindable} if the result of the binding is {@code null}.
227+
* @param name the configuration property name to bind
228+
* @param target the target bindable
229+
* @param handler the bind handler
230+
* @param <T> the bound type
231+
* @return the bound or created object
232+
* @since 2.2.0
233+
* @see #bindOrCreate(ConfigurationPropertyName, Bindable, BindHandler)
234+
*/
235+
public <T> T bindOrCreate(String name, Bindable<T> target, BindHandler handler) {
236+
return bindOrCreate(ConfigurationPropertyName.of(name), target, handler);
237+
}
238+
239+
/**
240+
* Bind the specified target {@link Bindable} using this binder's
241+
* {@link ConfigurationPropertySource property sources} or create a new instance using
242+
* the type of the {@link Bindable} if the result of the binding is {@code null}.
243+
* @param name the configuration property name to bind
244+
* @param target the target bindable
245+
* @param handler the bind handler (may be {@code null})
246+
* @param <T> the bound or created type
247+
* @since 2.2.0
248+
* @return the bound or created object
249+
*/
250+
public <T> T bindOrCreate(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler) {
251+
return bind(name, target, handler, true);
252+
}
253+
254+
private <T> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, boolean create) {
199255
Assert.notNull(name, "Name must not be null");
200256
Assert.notNull(target, "Target must not be null");
201257
handler = (handler != null) ? handler : BindHandler.DEFAULT;
202258
Context context = new Context();
203-
T bound = bind(name, target, handler, context, false);
204-
return BindResult.of(bound);
259+
return bind(name, target, handler, context, false, create);
205260
}
206261

207-
protected final <T> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context,
208-
boolean allowRecursiveBinding) {
262+
private <T> T bind(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler, Context context,
263+
boolean allowRecursiveBinding, boolean create) {
209264
context.clearConfigurationProperty();
210265
try {
211266
target = handler.onStart(name, target, context);
212267
if (target == null) {
213-
return null;
268+
return handleBindResult(name, target, handler, context, null, create);
214269
}
215270
Object bound = bindObject(name, target, handler, context, allowRecursiveBinding);
216-
return handleBindResult(name, target, handler, context, bound);
271+
return handleBindResult(name, target, handler, context, bound, create);
217272
}
218273
catch (Exception ex) {
219274
return handleBindError(name, target, handler, context, ex);
220275
}
221276
}
222277

223278
private <T> T handleBindResult(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler,
224-
Context context, Object result) throws Exception {
279+
Context context, Object result, boolean create) throws Exception {
225280
if (result != null) {
226281
result = handler.onSuccess(name, target, context, result);
227282
result = context.getConverter().convert(result, target);
228283
}
284+
if (result == null && create) {
285+
result = createBean(target, context);
286+
result = handler.onCreate(name, target, context, result);
287+
result = context.getConverter().convert(result, target);
288+
Assert.state(result != null, () -> "Unable to create instance for " + target.getType());
289+
}
229290
handler.onFinish(name, target, context, result);
230291
return context.getConverter().convert(result, target);
231292
}
232293

294+
private Object createBean(Bindable<?> target, Context context) {
295+
Class<?> type = target.getType().resolve();
296+
for (BeanBinder beanBinder : BEAN_BINDERS) {
297+
Object bean = beanBinder.create(type, context);
298+
if (bean != null) {
299+
return bean;
300+
}
301+
}
302+
return null;
303+
}
304+
233305
private <T> T handleBindError(ConfigurationPropertyName name, Bindable<T> target, BindHandler handler,
234306
Context context, Exception error) {
235307
try {
@@ -288,7 +360,7 @@ private <T> Object bindAggregate(ConfigurationPropertyName name, Bindable<T> tar
288360
Context context, AggregateBinder<?> aggregateBinder) {
289361
AggregateElementBinder elementBinder = (itemName, itemTarget, source) -> {
290362
boolean allowRecursiveBinding = aggregateBinder.isAllowRecursiveBinding(source);
291-
Supplier<?> supplier = () -> bind(itemName, itemTarget, handler, context, allowRecursiveBinding);
363+
Supplier<?> supplier = () -> bind(itemName, itemTarget, handler, context, allowRecursiveBinding, false);
292364
return context.withSource(source, supplier);
293365
};
294366
return context.withIncreasedDepth(() -> aggregateBinder.bind(name, target, elementBinder));
@@ -325,10 +397,15 @@ private Object bindBean(ConfigurationPropertyName name, Bindable<?> target, Bind
325397
return null;
326398
}
327399
BeanPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName),
328-
propertyTarget, handler, context, false);
400+
propertyTarget, handler, context, false, false);
329401
return context.withBean(type, () -> {
330-
Stream<?> boundBeans = BEAN_BINDERS.stream().map((b) -> b.bind(name, target, context, propertyBinder));
331-
return boundBeans.filter(Objects::nonNull).findFirst().orElse(null);
402+
for (BeanBinder beanBinder : BEAN_BINDERS) {
403+
Object bean = beanBinder.bind(name, target, context, propertyBinder);
404+
if (bean != null) {
405+
return bean;
406+
}
407+
}
408+
return null;
332409
});
333410
}
334411

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/ConstructorParametersBinder.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,45 @@ public <T> T bind(ConfigurationPropertyName name, Bindable<T> target, Binder.Con
5757
return null;
5858
}
5959
List<Object> bound = bind(propertyBinder, bean, context.getConverter());
60-
return (T) BeanUtils.instantiateClass(bean.getConstructor(), bound.toArray());
60+
return (bound != null) ? (T) BeanUtils.instantiateClass(bean.getConstructor(), bound.toArray()) : null;
61+
}
62+
63+
@Override
64+
@SuppressWarnings("unchecked")
65+
public <T> T create(Class<T> type, Binder.Context context) {
66+
Bean bean = getBean(type);
67+
if (bean == null) {
68+
return null;
69+
}
70+
Collection<ConstructorParameter> parameters = bean.getParameters().values();
71+
List<Object> parameterValues = new ArrayList<>(parameters.size());
72+
for (ConstructorParameter parameter : parameters) {
73+
Object boundParameter = getDefaultValue(parameter, context.getConverter());
74+
parameterValues.add(boundParameter);
75+
}
76+
return (T) BeanUtils.instantiateClass(bean.getConstructor(), parameterValues.toArray());
77+
}
78+
79+
private <T> Bean getBean(Class<T> type) {
80+
if (KOTLIN_PRESENT && KotlinDetector.isKotlinType(type)) {
81+
return KotlinBeanProvider.get(type);
82+
}
83+
return SimpleBeanProvider.get(type);
6184
}
6285

6386
private List<Object> bind(BeanPropertyBinder propertyBinder, Bean bean, BindConverter converter) {
6487
Collection<ConstructorParameter> parameters = bean.getParameters().values();
6588
List<Object> boundParameters = new ArrayList<>(parameters.size());
89+
int unboundParameterCount = 0;
6690
for (ConstructorParameter parameter : parameters) {
6791
Object boundParameter = bind(parameter, propertyBinder);
6892
if (boundParameter == null) {
93+
unboundParameterCount++;
6994
boundParameter = getDefaultValue(parameter, converter);
7095
}
7196
boundParameters.add(boundParameter);
7297
}
73-
return boundParameters;
98+
return (unboundParameterCount != parameters.size()) ? boundParameters : null;
7499
}
75100

76101
private Object getDefaultValue(ConstructorParameter parameter, BindConverter converter) {

0 commit comments

Comments
 (0)