Skip to content

Commit da9658e

Browse files
committed
GH-1359 Added support for property precedence
Ensured that default property only takes affect if the actual binding property is not set. For example if "spring.cloud.stream.bindings.output.producer.partitionCount=4" and "spring.cloud.stream.default.producer.partitionCount=1" are both set the actual binidng property (i.e., 4) should take precedence Resolves #1359
1 parent 9f3f49e commit da9658e

File tree

6 files changed

+105
-8
lines changed

6 files changed

+105
-8
lines changed

spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ConsumerProperties.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
import com.fasterxml.jackson.annotation.JsonInclude;
2222

23+
import org.springframework.cloud.stream.config.MergableProperties;
24+
2325
/**
2426
* Common consumer properties.
2527
*
@@ -30,7 +32,7 @@
3032
* @author Oleg Zhurakousky
3133
*/
3234
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
33-
public class ConsumerProperties {
35+
public class ConsumerProperties implements MergableProperties{
3436

3537
/**
3638
* The concurrency setting of the consumer. Default: 1.

spring-cloud-stream/src/main/java/org/springframework/cloud/stream/binder/ProducerProperties.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.fasterxml.jackson.annotation.JsonInclude.Include;
2424
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
2525

26+
import org.springframework.cloud.stream.config.MergableProperties;
2627
import org.springframework.expression.Expression;
2728

2829
/**
@@ -34,7 +35,7 @@
3435
* @author Oleg Zhurakousky
3536
*/
3637
@JsonInclude(Include.NON_DEFAULT)
37-
public class ProducerProperties {
38+
public class ProducerProperties implements MergableProperties {
3839

3940
@JsonSerialize(using = ExpressionSerializer.class)
4041
private Expression partitionKeyExpression;

spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/BindingProperties.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
*/
3838
@JsonInclude(Include.NON_DEFAULT)
3939
@Validated
40-
public class BindingProperties {
40+
public class BindingProperties implements MergableProperties {
4141

4242
public static final MimeType DEFAULT_CONTENT_TYPE = MimeTypeUtils.APPLICATION_JSON;
4343

@@ -153,5 +153,4 @@ public String toString() {
153153
sb.deleteCharAt(sb.lastIndexOf(COMMA));
154154
return "BindingProperties{" + sb.toString() + "}";
155155
}
156-
157156
}

spring-cloud-stream/src/main/java/org/springframework/cloud/stream/config/EnvironmentEntryInitializingTreeMap.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,10 @@ public T get(Object key) {
9595

9696
@Override
9797
public T put(String key, T value) {
98-
// boot 2 call this first
9998
Binder binder = new Binder(ConfigurationPropertySources.get(environment),new PropertySourcesPlaceholdersResolver(environment),this.conversionService, null);
100-
binder.bind(defaultsPrefix, Bindable.ofInstance(value));
99+
T defaultProperties = BeanUtils.instantiateClass(entryClass);
100+
binder.bind(defaultsPrefix, Bindable.ofInstance(defaultProperties));
101+
((MergableProperties)defaultProperties).merge((MergableProperties) value);
101102
return this.delegate.put(key, value);
102103
}
103104

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.stream.config;
18+
19+
import java.beans.PropertyDescriptor;
20+
import java.lang.reflect.Method;
21+
import java.lang.reflect.Modifier;
22+
23+
import org.springframework.beans.BeanUtils;
24+
import org.springframework.beans.BeansException;
25+
import org.springframework.beans.FatalBeanException;
26+
import org.springframework.cloud.stream.binder.ConsumerProperties;
27+
import org.springframework.cloud.stream.binder.ProducerProperties;
28+
import org.springframework.util.ClassUtils;
29+
import org.springframework.util.ObjectUtils;
30+
31+
/**
32+
* NOT INTENDED FOR PUBLIC USE! Was primarily created to address GH-1359.
33+
*
34+
* @see BinderProperties
35+
* @see ProducerProperties
36+
* @see ConsumerProperties
37+
*
38+
* @author Oleg Zhurakousky
39+
*/
40+
public interface MergableProperties {
41+
42+
/**
43+
* A variation of {@link BeanUtils#copyProperties(Object, Object)} specifically designed to copy properties using the following rule:
44+
*
45+
* - If source property is null then override with the same from mergable.
46+
* - If source property is an array and it is empty then override with same from mergable.
47+
* - If source property is mergable then merge.
48+
*/
49+
default void merge(MergableProperties mergable) {
50+
for (PropertyDescriptor targetPd : BeanUtils.getPropertyDescriptors(mergable.getClass())) {
51+
Method writeMethod = targetPd.getWriteMethod();
52+
if (writeMethod != null) {
53+
PropertyDescriptor sourcePd = BeanUtils.getPropertyDescriptor(this.getClass(), targetPd.getName());
54+
if (sourcePd != null) {
55+
Method readMethod = sourcePd.getReadMethod();
56+
if (readMethod != null &&
57+
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
58+
try {
59+
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
60+
readMethod.setAccessible(true);
61+
}
62+
Object value = readMethod.invoke(this);
63+
if (value != null) {
64+
if (value instanceof MergableProperties) {
65+
((MergableProperties)value).merge((MergableProperties)readMethod.invoke(mergable));
66+
}
67+
else {
68+
Object v = readMethod.invoke(mergable);
69+
if (v == null || (ObjectUtils.isArray(v) && ObjectUtils.isEmpty(v))) {
70+
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
71+
writeMethod.setAccessible(true);
72+
}
73+
writeMethod.invoke(mergable, value);
74+
}
75+
}
76+
}
77+
}
78+
catch (Throwable ex) {
79+
throw new FatalBeanException(
80+
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
81+
}
82+
}
83+
}
84+
}
85+
}
86+
}
87+
88+
default void copyProperties(Object source, Object target) throws BeansException {
89+
90+
}
91+
}

spring-cloud-stream/src/test/java/org/springframework/cloud/stream/binder/SourceBindingWithGlobalPropertiesTest.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,9 @@
4242
"spring.cloud.stream.default.contentType=application/json",
4343
"spring.cloud.stream.bindings.output.destination=ticktock",
4444
"spring.cloud.stream.default.producer.requiredGroups=someGroup",
45-
"spring.cloud.stream.bindings.output.producer.headerMode=none" })
45+
"spring.cloud.stream.default.producer.partitionCount=1",
46+
"spring.cloud.stream.bindings.output.producer.headerMode=none",
47+
"spring.cloud.stream.bindings.output.producer.partitionCount=4"})
4648
public class SourceBindingWithGlobalPropertiesTest {
4749

4850
@Autowired
@@ -53,7 +55,8 @@ public void testGlobalPropertiesSet() {
5355
BindingProperties bindingProperties = serviceProperties.getBindingProperties(Source.OUTPUT);
5456
Assertions.assertThat(bindingProperties.getContentType()).isEqualTo("application/json");
5557
Assertions.assertThat(bindingProperties.getDestination()).isEqualTo("ticktock");
56-
Assertions.assertThat(bindingProperties.getProducer().getRequiredGroups()).containsExactly("someGroup");
58+
Assertions.assertThat(bindingProperties.getProducer().getRequiredGroups()).containsExactly("someGroup"); // default propagates to producer
59+
Assertions.assertThat(bindingProperties.getProducer().getPartitionCount()).isEqualTo(4); // validates binding property takes precedence over default
5760
Assertions.assertThat(bindingProperties.getProducer().getHeaderMode()).isEqualTo(HeaderMode.none);
5861
}
5962

0 commit comments

Comments
 (0)