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: + * + * */ - 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(); + } + + } + }