From 869141766b18a18c4d6c45ff4e88d12d67291e19 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 9 Dec 2020 18:14:58 -0800 Subject: [PATCH] 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 --- .../properties/bind/JavaBeanBinder.java | 15 ++++-- .../properties/bind/JavaBeanBinderTests.java | 52 ++++++++++++++++++- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java index 35ca1be4808d..13aeaa5064b1 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/JavaBeanBinder.java @@ -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.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 @@ static class Bean { 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 E[] getSorted(S source, Function elements, Function 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])) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java index 9bd77b27bbda..b68604dac5d0 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/JavaBeanBinderTests.java @@ -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.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 @@ void bindWhenHasPackagePrivateSetterShouldBind() { 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 @@ void setProperty(String property) { } + 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(); + } + + } + }