diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java
index 8e7de08c1ea..818487264de 100644
--- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java
+++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java
@@ -24,10 +24,12 @@ import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
@@ -38,6 +40,8 @@ import com.fasterxml.jackson.databind.ser.PropertyWriter;
import com.fasterxml.jackson.databind.ser.SerializerFactory;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.boot.actuate.endpoint.Sanitizer;
@@ -68,7 +72,7 @@ import org.springframework.util.StringUtils;
@Endpoint(id = "configprops")
public class ConfigurationPropertiesReportEndpoint implements ApplicationContextAware {
- private static final String CGLIB_FILTER_ID = "cglibFilter";
+ private static final String CONFIGURATION_PROPERTIES_FILTER_ID = "configurationPropertiesFilter";
private final Sanitizer sanitizer = new Sanitizer();
@@ -167,7 +171,7 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
protected void configureObjectMapper(ObjectMapper mapper) {
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
mapper.setSerializationInclusion(Include.NON_NULL);
- applyCglibFilters(mapper);
+ applyConfigurationPropertiesFilter(mapper);
applySerializationModifier(mapper);
}
@@ -181,15 +185,11 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
mapper.setSerializerFactory(factory);
}
- /**
- * Configure PropertyFilter to make sure Jackson doesn't process CGLIB generated bean
- * properties.
- * @param mapper the object mapper
- */
- private void applyCglibFilters(ObjectMapper mapper) {
- mapper.setAnnotationIntrospector(new CglibAnnotationIntrospector());
- mapper.setFilterProvider(new SimpleFilterProvider().addFilter(CGLIB_FILTER_ID,
- new CglibBeanPropertyFilter()));
+ private void applyConfigurationPropertiesFilter(ObjectMapper mapper) {
+ mapper.setAnnotationIntrospector(
+ new ConfigurationPropertiesAnnotationIntrospector());
+ mapper.setFilterProvider(new SimpleFilterProvider()
+ .setDefaultFilter(new ConfigurationPropertiesPropertyFilter()));
}
/**
@@ -268,14 +268,14 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
* properties.
*/
@SuppressWarnings("serial")
- private static class CglibAnnotationIntrospector
+ private static class ConfigurationPropertiesAnnotationIntrospector
extends JacksonAnnotationIntrospector {
@Override
public Object findFilterId(Annotated a) {
Object id = super.findFilterId(a);
if (id == null) {
- id = CGLIB_FILTER_ID;
+ id = CONFIGURATION_PROPERTIES_FILTER_ID;
}
return id;
}
@@ -283,10 +283,20 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
}
/**
- * {@link SimpleBeanPropertyFilter} to filter out all bean properties whose names
- * start with '$$'.
+ * {@link SimpleBeanPropertyFilter} for serialization of
+ * {@link ConfigurationProperties} beans. The filter hides:
+ *
+ *
+ * - Properties that have a name starting with '$$'.
+ *
- Properties that are self-referential.
+ *
- Properties that throw an exception when retrieving their value.
+ *
*/
- private static class CglibBeanPropertyFilter extends SimpleBeanPropertyFilter {
+ private static class ConfigurationPropertiesPropertyFilter
+ extends SimpleBeanPropertyFilter {
+
+ private static final Log logger = LogFactory
+ .getLog(ConfigurationPropertiesPropertyFilter.class);
@Override
protected boolean include(BeanPropertyWriter writer) {
@@ -302,6 +312,31 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
return !name.startsWith("$$");
}
+ @Override
+ public void serializeAsField(Object pojo, JsonGenerator jgen,
+ SerializerProvider provider, PropertyWriter writer) throws Exception {
+ if (writer instanceof BeanPropertyWriter) {
+ try {
+ if (pojo == ((BeanPropertyWriter) writer).get(pojo)) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Skipping '" + writer.getFullName() + "' on '"
+ + pojo.getClass().getName()
+ + "' as it is self-referential");
+ }
+ return;
+ }
+ }
+ catch (Exception ex) {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Skipping '" + writer.getFullName() + "' on '"
+ + pojo.getClass().getName() + "' as an exception "
+ + "was thrown when retrieving its value", ex);
+ }
+ return;
+ }
+ }
+ super.serializeAsField(pojo, jgen, provider, writer);
+ }
}
/**
diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java
index ffe38d66fd2..4698754c105 100644
--- a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java
+++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpointSerializationTests.java
@@ -22,6 +22,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import com.zaxxer.hikari.HikariDataSource;
+import org.junit.Ignore;
import org.junit.Test;
import org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint.ConfigurationPropertiesBeanDescriptor;
@@ -87,9 +89,10 @@ public class ConfigurationPropertiesReportEndpointSerializationTests {
}
@Test
- public void testCycle() throws Exception {
+ @SuppressWarnings("unchecked")
+ public void testSelfReferentialProperty() throws Exception {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
- .withUserConfiguration(CycleConfig.class)
+ .withUserConfiguration(SelfReferentialConfig.class)
.withPropertyValues("foo.name:foo");
contextRunner.run((context) -> {
ConfigurationPropertiesReportEndpoint endpoint = context
@@ -97,12 +100,34 @@ public class ConfigurationPropertiesReportEndpointSerializationTests {
ConfigurationPropertiesDescriptor properties = endpoint
.configurationProperties();
ConfigurationPropertiesBeanDescriptor foo = properties.getBeans().get("foo");
- assertThat(foo).isNotNull();
assertThat(foo.getPrefix()).isEqualTo("foo");
Map map = foo.getProperties();
assertThat(map).isNotNull();
- assertThat(map).hasSize(1);
- assertThat(map.get("error")).isEqualTo("Cannot serialize 'foo'");
+ assertThat(map).containsOnlyKeys("bar", "name");
+ assertThat(map).containsEntry("name", "foo");
+ Map bar = (Map) map.get("bar");
+ assertThat(bar).containsOnlyKeys("name");
+ assertThat(bar).containsEntry("name", "123456");
+ });
+ }
+
+ @Test
+ @Ignore("gh-11037")
+ public void testCycle() {
+ ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withUserConfiguration(CycleConfig.class);
+ contextRunner.run((context) -> {
+ ConfigurationPropertiesReportEndpoint endpoint = context
+ .getBean(ConfigurationPropertiesReportEndpoint.class);
+ ConfigurationPropertiesDescriptor properties = endpoint
+ .configurationProperties();
+ ConfigurationPropertiesBeanDescriptor cycle = properties.getBeans()
+ .get("cycle");
+ assertThat(cycle.getPrefix()).isEqualTo("cycle");
+ Map map = cycle.getProperties();
+ assertThat(map).isNotNull();
+ assertThat(map).containsOnlyKeys("error");
+ assertThat(map).containsEntry("error", "Cannot serialize 'cycle'");
});
}
@@ -191,7 +216,6 @@ public class ConfigurationPropertiesReportEndpointSerializationTests {
@Test
@SuppressWarnings("unchecked")
-
public void testInitializedMapAndList() throws Exception {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withUserConfiguration(InitializedMapAndListPropertiesConfig.class)
@@ -213,6 +237,22 @@ public class ConfigurationPropertiesReportEndpointSerializationTests {
});
}
+ @Test
+ public void hikariDataSourceConfigurationPropertiesBeanCanBeSerialized() {
+ ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withUserConfiguration(HikariDataSourceConfig.class);
+ contextRunner.run((context) -> {
+ ConfigurationPropertiesReportEndpoint endpoint = context
+ .getBean(ConfigurationPropertiesReportEndpoint.class);
+ ConfigurationPropertiesDescriptor properties = endpoint
+ .configurationProperties();
+ ConfigurationPropertiesBeanDescriptor hikariDataSource = properties.getBeans()
+ .get("hikariDataSource");
+ Map nestedProperties = hikariDataSource.getProperties();
+ assertThat(nestedProperties).doesNotContainKey("error");
+ });
+ }
+
@Configuration
@EnableConfigurationProperties
public static class Base {
@@ -238,12 +278,12 @@ public class ConfigurationPropertiesReportEndpointSerializationTests {
@Configuration
@Import(Base.class)
- public static class CycleConfig {
+ public static class SelfReferentialConfig {
@Bean
@ConfigurationProperties(prefix = "foo")
- public Cycle foo() {
- return new Cycle();
+ public SelfReferential foo() {
+ return new SelfReferential();
}
}
@@ -254,8 +294,8 @@ public class ConfigurationPropertiesReportEndpointSerializationTests {
@Bean
@ConfigurationProperties(prefix = "bar")
- public Cycle foo() {
- return new Cycle();
+ public SelfReferential foo() {
+ return new SelfReferential();
}
}
@@ -363,11 +403,11 @@ public class ConfigurationPropertiesReportEndpointSerializationTests {
}
- public static class Cycle extends Foo {
+ public static class SelfReferential extends Foo {
private Foo self;
- public Cycle() {
+ public SelfReferential() {
this.self = this;
}
@@ -439,4 +479,58 @@ public class ConfigurationPropertiesReportEndpointSerializationTests {
}
+ static class Cycle {
+
+ private final Alpha alpha = new Alpha(this);
+
+ public Alpha getAlpha() {
+ return this.alpha;
+ }
+
+ static class Alpha {
+
+ private final Cycle cycle;
+
+ Alpha(Cycle cycle) {
+ this.cycle = cycle;
+ }
+
+ public Cycle getCycle() {
+ return this.cycle;
+ }
+
+ }
+
+ }
+
+ @Configuration
+ @Import(Base.class)
+ static class CycleConfig {
+
+ @Bean
+ // gh-11037
+ // @ConfigurationProperties(prefix = "cycle")
+ public Cycle cycle() {
+ return new Cycle();
+ }
+
+ }
+
+ @Configuration
+ @EnableConfigurationProperties
+ static class HikariDataSourceConfig {
+
+ @Bean
+ public ConfigurationPropertiesReportEndpoint endpoint() {
+ return new ConfigurationPropertiesReportEndpoint();
+ }
+
+ @Bean
+ @ConfigurationProperties(prefix = "test.datasource")
+ public HikariDataSource hikariDataSource() {
+ return new HikariDataSource();
+ }
+
+ }
+
}