fix: fix parsing ConfigurationProperties value with RefreshScope. (#1780)

Skip recursive parsing of JDK built-in classes and limit recursion depth to avoid OOM.
pull/1781/head
shedfreewu 1 week ago committed by shedfreewu
parent 939f0f4386
commit 55b7a7cc4b

@ -14,3 +14,4 @@
- [feat: support kafka lane.](https://github.com/Tencent/spring-cloud-tencent/pull/1765)
- [fix: ApplicationContextAwareUtils may not be ready in postProcessAfterInitialization.](https://github.com/Tencent/spring-cloud-tencent/pull/1779)
- [refactor:optimize metadata context operation.](https://github.com/Tencent/spring-cloud-tencent/pull/1773)
- [fix: fix parsing ConfigurationProperties value with RefreshScope.](https://github.com/Tencent/spring-cloud-tencent/pull/1780)

@ -117,6 +117,28 @@ public class SpringValueProcessor extends PolarisProcessor implements BeanDefini
return clazz.isArray() || Collection.class.isAssignableFrom(clazz) || Map.class.isAssignableFrom(clazz);
}
/**
* whether the class is enum.
* @param clazz the class under analysis.
* @return true if the class is enum, otherwise false.
*/
private static boolean isEnum(Class<?> clazz) {
return clazz.isEnum();
}
/**
* whether the class is jdk built-in class.
* @param clazz the class under analysis.
* @return true if the class is jdk built-in class, otherwise false.
*/
private static boolean isJdkBuiltInClass(Class<?> clazz) {
String packageName = clazz.getPackageName();
// match java.*, javax.* and jakarta.* as built-in classes
return packageName.startsWith("java.")
|| packageName.startsWith("javax.")
|| packageName.startsWith("jakarta.");
}
@Override
public void postProcessBeanFactory(@NonNull ConfigurableListableBeanFactory beanFactory)
throws BeansException {
@ -256,24 +278,38 @@ public class SpringValueProcessor extends PolarisProcessor implements BeanDefini
* @param prefix prefix or subclass's prefix of @ConfigurationProperties bean.
*/
private void parseConfigKeys(Class<?> configClazz, String prefix) {
parseConfigKeys(configClazz, prefix, 0);
}
private void parseConfigKeys(Class<?> configClazz, String prefix, int depth) {
// Prevent infinite recursion and StackOverflowError
if (depth > 5) {
LOGGER.debug("Recursion depth limit reached for class: {}, prefix: {}", configClazz.getName(), prefix);
return;
}
for (Field field : findAllField(configClazz)) {
if (isPrimitiveOrWrapper(field.getType())) {
if (field.getType().equals(configClazz)) {
// ignore self reference
continue;
}
if (isCollection(field.getType())) {
// lowerCamel format
springValueRegistry.putRefreshScopeKey(prefix + field.getName());
springValueRegistry.putRefreshScopePrefixKey(prefix + field.getName());
// lower-hyphen format
springValueRegistry.putRefreshScopeKey(
springValueRegistry.putRefreshScopePrefixKey(
prefix + CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, field.getName()));
}
else if (isCollection(field.getType())) {
springValueRegistry.putRefreshScopePrefixKey(prefix + field.getName());
springValueRegistry.putRefreshScopePrefixKey(
else if (isPrimitiveOrWrapper(field.getType()) || isEnum(field.getType()) || isJdkBuiltInClass(field.getType())) {
springValueRegistry.putRefreshScopeKey(prefix + field.getName());
springValueRegistry.putRefreshScopeKey(
prefix + CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, field.getName()));
}
else {
// complex type, recursive parse
parseConfigKeys(field.getType(), prefix + field.getName() + ".");
parseConfigKeys(field.getType(), prefix + field.getName() + ".", depth + 1);
parseConfigKeys(field.getType(),
prefix + CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, field.getName()) + ".");
prefix + CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_HYPHEN, field.getName()) + ".", depth + 1);
}
}
}

@ -18,11 +18,15 @@
package com.tencent.cloud.polaris.config.spring.annotation;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.ServerSocket;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import com.tencent.cloud.polaris.config.PolarisConfigBootstrapAutoConfiguration;
import com.tencent.cloud.polaris.config.enums.RefreshType;
@ -88,6 +92,7 @@ public class RefreshScopeSpringProcessorTest {
.withConfiguration(AutoConfigurations.of(TestBeanProperties1.class))
.withConfiguration(AutoConfigurations.of(TestBeanProperties2.class))
.withConfiguration(AutoConfigurations.of(TestBeanProperties3.class))
.withConfiguration(AutoConfigurations.of(ComplexConfigurationProperties.class))
.withConfiguration(AutoConfigurations.of(PolarisConfigAutoConfiguration.class))
.withAllowBeanDefinitionOverriding(true)
.withPropertyValues("spring.application.name=" + "conditionalOnConfigReflectEnabledTest")
@ -116,6 +121,7 @@ public class RefreshScopeSpringProcessorTest {
// @RefreshScope and @Bean on method, @ConfigurationProperties bean in class
assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.name")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.inner.name")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.inner2.name")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.set")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.list")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.list[0]")).isTrue();
@ -123,6 +129,9 @@ public class RefreshScopeSpringProcessorTest {
assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.array[0]")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.map")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.map.key")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.timeUnit")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.time-unit")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.date")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.properties2.notExist")).isFalse();
// @RefreshScope and @ConfigurationProperties on @Component bean
@ -130,6 +139,68 @@ public class RefreshScopeSpringProcessorTest {
assertThat(springValueRegistry.isRefreshScopeKey("test.properties3.notExist")).isFalse();
// @RefreshScope and @Bean on method, @Value bean in class
assertThat(springValueRegistry.isRefreshScopeKey("test.bean5.name")).isTrue();
// Complex ConfigurationProperties tests
// Primitive fields
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.name")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.port")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.enabled")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.timeout")).isTrue();
// Enum and JDK built-in types
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.timeUnit")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.time-unit")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.createdDate")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.created-date")).isTrue();
// Collections
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1List")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1-list")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1Map")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1-map")).isTrue();
// Level 1 nested properties
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level1Name")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level1-name")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level1Value")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level1-value")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level1List")).isTrue();
// Level 2 nested properties
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level2Name")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level2-name")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level2Enabled")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level2-enabled")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level2Map")).isTrue();
// Level 3 nested properties
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level3.level3Name")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level3.level3-name")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level3.level3Value")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level3.level3-value")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level3.level3Array")).isTrue();
// Level 4 nested properties
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level3.level4.level4Name")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level3.level4.level4-name")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level3.level4.level4Value")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level3.level4.level4-value")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level3.level4.level4Set")).isTrue();
// Level 5 nested properties
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level3.level4.level5.level5Name")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level3.level4.level5.level5-name")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level3.level4.level5.level5Value")).isTrue();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level3.level4.level5.level5-value")).isTrue();
// Level 6 should not be registered due to depth limit (depth > 5)
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level3.level4.level5.level6.level6Name")).isFalse();
assertThat(springValueRegistry.isRefreshScopeKey("test.complex.properties.level1.level2.level3.level4.level5.level6.level6-name")).isFalse();
// not self reference
Field refreshScopeKeysField = SpringValueRegistry.class.getDeclaredField("refreshScopeKeys");
refreshScopeKeysField.setAccessible(true);
Set<String> callbackMap = (Set<String>) refreshScopeKeysField.get(springValueRegistry);
long selfCont = callbackMap.stream().filter(key -> key.contains("self")).count();
assertThat(selfCont).isEqualTo(0);
});
}
@ -154,12 +225,11 @@ public class RefreshScopeSpringProcessorTest {
@RefreshScope
private static class ValueTest {
ValueTest() {
}
private static String name;
@Value("${timeout:1000}")
private int timeout;
ValueTest() {
}
public int getTimeout() {
return timeout;
@ -274,6 +344,12 @@ public class RefreshScopeSpringProcessorTest {
private InnerProperties inner;
private InnerProperties inner2;
private TimeUnit timeUnit;
private Date date;
public String getName() {
return name;
}
@ -321,6 +397,30 @@ public class RefreshScopeSpringProcessorTest {
public void setInner(InnerProperties inner) {
this.inner = inner;
}
public InnerProperties getInner2() {
return inner2;
}
public void setInner2(InnerProperties inner2) {
this.inner2 = inner2;
}
public TimeUnit getTimeUnit() {
return timeUnit;
}
public void setTimeUnit(TimeUnit timeUnit) {
this.timeUnit = timeUnit;
}
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
}
@Component
@ -350,4 +450,387 @@ public class RefreshScopeSpringProcessorTest {
}
}
/**
* Complex ConfigurationProperties for testing various scenarios:
* - Self-referencing nested properties
* - Deep nesting (more than 5 levels)
* - Static fields
* - Final fields
* - Collections with complex types
* - Multiple levels of nested objects
*/
@Component
@RefreshScope
@ConfigurationProperties(prefix = "test.complex.properties")
static class ComplexConfigurationProperties {
// Static field - should be ignored by Spring
private static String staticField = "static-value";
// Final field with default value
private final String finalField = "final-value";
// Primitive types
private String name;
private int port;
private boolean enabled;
private long timeout;
// Self-referencing nested property
private ComplexConfigurationProperties self;
// Level 1 nested object
private Level1Properties level1;
// Collection of complex types
private ArrayList<Level1Properties> level1List;
private HashMap<String, Level1Properties> level1Map;
// Enum type
private TimeUnit timeUnit;
// JDK built-in types
private Date createdDate;
public static String getStaticField() {
return staticField;
}
public static void setStaticField(String staticField) {
ComplexConfigurationProperties.staticField = staticField;
}
public String getFinalField() {
return finalField;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public long getTimeout() {
return timeout;
}
public void setTimeout(long timeout) {
this.timeout = timeout;
}
public ComplexConfigurationProperties getSelf() {
return self;
}
public void setSelf(ComplexConfigurationProperties self) {
this.self = self;
}
public Level1Properties getLevel1() {
return level1;
}
public void setLevel1(Level1Properties level1) {
this.level1 = level1;
}
public ArrayList<Level1Properties> getLevel1List() {
return level1List;
}
public void setLevel1List(ArrayList<Level1Properties> level1List) {
this.level1List = level1List;
}
public HashMap<String, Level1Properties> getLevel1Map() {
return level1Map;
}
public void setLevel1Map(HashMap<String, Level1Properties> level1Map) {
this.level1Map = level1Map;
}
public TimeUnit getTimeUnit() {
return timeUnit;
}
public void setTimeUnit(TimeUnit timeUnit) {
this.timeUnit = timeUnit;
}
public Date getCreatedDate() {
return createdDate;
}
public void setCreatedDate(Date createdDate) {
this.createdDate = createdDate;
}
}
/**
* Level 1 nested properties
*/
static class Level1Properties {
private String level1Name;
private int level1Value;
// Level 2 nested object
private Level2Properties level2;
// Collection at level 1
private ArrayList<String> level1List;
public String getLevel1Name() {
return level1Name;
}
public void setLevel1Name(String level1Name) {
this.level1Name = level1Name;
}
public int getLevel1Value() {
return level1Value;
}
public void setLevel1Value(int level1Value) {
this.level1Value = level1Value;
}
public Level2Properties getLevel2() {
return level2;
}
public void setLevel2(Level2Properties level2) {
this.level2 = level2;
}
public ArrayList<String> getLevel1List() {
return level1List;
}
public void setLevel1List(ArrayList<String> level1List) {
this.level1List = level1List;
}
}
/**
* Level 2 nested properties
*/
static class Level2Properties {
private String level2Name;
private boolean level2Enabled;
// Level 3 nested object
private Level3Properties level3;
// Map at level 2
private HashMap<String, String> level2Map;
public String getLevel2Name() {
return level2Name;
}
public void setLevel2Name(String level2Name) {
this.level2Name = level2Name;
}
public boolean isLevel2Enabled() {
return level2Enabled;
}
public void setLevel2Enabled(boolean level2Enabled) {
this.level2Enabled = level2Enabled;
}
public Level3Properties getLevel3() {
return level3;
}
public void setLevel3(Level3Properties level3) {
this.level3 = level3;
}
public HashMap<String, String> getLevel2Map() {
return level2Map;
}
public void setLevel2Map(HashMap<String, String> level2Map) {
this.level2Map = level2Map;
}
}
/**
* Level 3 nested properties
*/
static class Level3Properties {
private String level3Name;
private double level3Value;
// Level 4 nested object
private Level4Properties level4;
// Array at level 3
private String[] level3Array;
public String getLevel3Name() {
return level3Name;
}
public void setLevel3Name(String level3Name) {
this.level3Name = level3Name;
}
public double getLevel3Value() {
return level3Value;
}
public void setLevel3Value(double level3Value) {
this.level3Value = level3Value;
}
public Level4Properties getLevel4() {
return level4;
}
public void setLevel4(Level4Properties level4) {
this.level4 = level4;
}
public String[] getLevel3Array() {
return level3Array;
}
public void setLevel3Array(String[] level3Array) {
this.level3Array = level3Array;
}
}
/**
* Level 4 nested properties
*/
static class Level4Properties {
private String level4Name;
private float level4Value;
// Level 5 nested object
private Level5Properties level5;
// Set at level 4
private HashSet<String> level4Set;
public String getLevel4Name() {
return level4Name;
}
public void setLevel4Name(String level4Name) {
this.level4Name = level4Name;
}
public float getLevel4Value() {
return level4Value;
}
public void setLevel4Value(float level4Value) {
this.level4Value = level4Value;
}
public Level5Properties getLevel5() {
return level5;
}
public void setLevel5(Level5Properties level5) {
this.level5 = level5;
}
public HashSet<String> getLevel4Set() {
return level4Set;
}
public void setLevel4Set(HashSet<String> level4Set) {
this.level4Set = level4Set;
}
}
/**
* Level 5 nested properties
*/
static class Level5Properties {
// Final field at deep level
private final String level5FinalField = "level5-final";
private String level5Name;
private byte level5Value;
// Level 6 nested object - this will exceed the depth limit
private Level6Properties level6;
public String getLevel5Name() {
return level5Name;
}
public void setLevel5Name(String level5Name) {
this.level5Name = level5Name;
}
public byte getLevel5Value() {
return level5Value;
}
public void setLevel5Value(byte level5Value) {
this.level5Value = level5Value;
}
public Level6Properties getLevel6() {
return level6;
}
public void setLevel6(Level6Properties level6) {
this.level6 = level6;
}
public String getLevel5FinalField() {
return level5FinalField;
}
}
/**
* Level 6 nested properties - exceeds depth limit
*/
static class Level6Properties {
private String level6Name;
private short level6Value;
public String getLevel6Name() {
return level6Name;
}
public void setLevel6Name(String level6Name) {
this.level6Name = level6Name;
}
public short getLevel6Value() {
return level6Value;
}
public void setLevel6Value(short level6Value) {
this.level6Value = level6Value;
}
}
}

@ -24,6 +24,7 @@ import java.util.concurrent.TimeoutException;
import com.tencent.cloud.common.constant.MetadataConstant;
import com.tencent.cloud.quickstart.callee.config.DataSourceProperties;
import com.tencent.cloud.quickstart.callee.config.RefreshScopeProperties;
import com.tencent.cloud.quickstart.callee.pojo.User;
import com.tencent.cloud.quickstart.callee.service.FaultToleranceService;
import org.slf4j.Logger;
@ -70,6 +71,9 @@ public class QuickstartCalleeController {
@Autowired
private FaultToleranceService faultToleranceService;
@Autowired
private RefreshScopeProperties refreshScopeProperties;
/**
* Get sum of two value.
* @param value1 value 1
@ -88,8 +92,8 @@ public class QuickstartCalleeController {
*/
@GetMapping("/info")
public String info(@RequestParam(required = false) String param) {
LOG.info("Quickstart [{}] Service [{}:{}] is called with param [{}]. datasource = [{}].", appName, ip, port, param, dataSourceProperties);
return String.format("Quickstart [%s] Service [%s:%s] is called with param [%s]. datasource = [%s].", appName, ip, port, param, dataSourceProperties);
LOG.info("Quickstart [{}] Service [{}:{}] is called with param [{}]. datasource = [{}]. refreshscope = [{}].", appName, ip, port, param, dataSourceProperties, refreshScopeProperties);
return String.format("Quickstart [%s] Service [%s:%s] is called with param [%s]. datasource = [%s]. refreshscope = [%s].", appName, ip, port, param, dataSourceProperties, refreshScopeProperties);
}
@PostMapping("/user")

@ -0,0 +1,63 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. All rights reserved.
*
* Licensed under the BSD 3-Clause License (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://opensource.org/licenses/BSD-3-Clause
*
* 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 com.tencent.cloud.quickstart.callee.config;
import java.util.concurrent.TimeUnit;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;
/**
* Example for ConfigurationProperties with @RefreshScope.
*
* @author Haotian Zhang
*/
@Component
@ConfigurationProperties("refreshscope")
@RefreshScope
public class RefreshScopeProperties {
private static String staticString;
private TimeUnit timeUnit = TimeUnit.SECONDS;
String getStaticString() {
return staticString;
}
void setStaticString(String staticString) {
RefreshScopeProperties.staticString = staticString;
}
TimeUnit getTimeUnit() {
return timeUnit;
}
void setTimeUnit(TimeUnit timeUnit) {
this.timeUnit = timeUnit;
}
@Override
public String toString() {
return "RefreshScopeProperties{" +
"staticString='" + staticString + '\'' +
", timeUnit=" + timeUnit +
'}';
}
}
Loading…
Cancel
Save