Use deterministic ordering of JavaBean methods

Update `JavaBeanBinder` so that methods and fields are sorted before
being processed. This ensures that setters are called in a deterministic
order, rather than the unspecified and variable order that reflection
provides.

Fixes gh-24068
This commit is contained in:
Phillip Webb 2020-12-09 18:14:58 -08:00
parent e5aea89599
commit 869141766b
2 changed files with 63 additions and 4 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -21,9 +21,12 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.springframework.beans.BeanUtils;
@ -124,13 +127,19 @@ class JavaBeanBinder implements DataObjectBinder {
private void addProperties(Class<?> type) {
while (type != null && !Object.class.equals(type)) {
Method[] declaredMethods = type.getDeclaredMethods();
Field[] declaredFields = type.getDeclaredFields();
Method[] declaredMethods = getSorted(type, Class::getDeclaredMethods, Method::getName);
Field[] declaredFields = getSorted(type, Class::getDeclaredFields, Field::getName);
addProperties(declaredMethods, declaredFields);
type = type.getSuperclass();
}
}
private <S, E> E[] getSorted(S source, Function<S, E[]> elements, Function<E, String> name) {
E[] result = elements.apply(source);
Arrays.sort(result, Comparator.comparing(name));
return result;
}
protected void addProperties(Method[] declaredMethods, Field[] declaredFields) {
for (int i = 0; i < declaredMethods.length; i++) {
if (!isCandidate(declaredMethods[i])) {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 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.
@ -26,6 +26,7 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.Test;
@ -540,6 +541,19 @@ class JavaBeanBinderTests {
assertThat(bean.getProperty()).isEqualTo("test");
}
@Test
void bindUsesConsistentPropertyOrder() {
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
source.put("foo.gamma", "0");
source.put("foo.alpha", "0");
source.put("foo.beta", "0");
this.sources.add(source);
PropertyOrderBean bean = this.binder.bind("foo", Bindable.of(PropertyOrderBean.class)).get();
assertThat(bean.getAlpha()).isEqualTo(0);
assertThat(bean.getBeta()).isEqualTo(1);
assertThat(bean.getGamma()).isEqualTo(2);
}
static class ExampleValueBean {
private int intValue;
@ -1036,4 +1050,40 @@ class JavaBeanBinderTests {
}
static class PropertyOrderBean {
static AtomicInteger atomic = new AtomicInteger();
private int alpha;
private int beta;
private int gamma;
int getAlpha() {
return this.alpha;
}
void setAlpha(int alpha) {
this.alpha = alpha + atomic.getAndIncrement();
}
int getBeta() {
return this.beta;
}
void setBeta(int beta) {
this.beta = beta + atomic.getAndIncrement();
}
int getGamma() {
return this.gamma;
}
void setGamma(int gamma) {
this.gamma = gamma + atomic.getAndIncrement();
}
}
}