Skip to content

Commit 85dc89e

Browse files
committed
Make serialization of @ConfigurationProperties beans more defensive
Previously, serialization of a @ConfigurationProperties bean to JSON would fail if: - A property on the bean returned the bean (the bean was self-referential) - An exception was thrown when attempting to retrieve a property's value. This commit makes the serialization more defensive by skipping any property that is affected by either of the problems described above. Debug logging has been added to aid diagnosis of missing properties. Closes gh-10846
1 parent 2e320ef commit 85dc89e

File tree

2 files changed

+150
-39
lines changed

2 files changed

+150
-39
lines changed

spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpoint.java

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@
2323
import java.util.List;
2424
import java.util.Map;
2525

26+
import com.fasterxml.jackson.core.JsonGenerator;
2627
import com.fasterxml.jackson.databind.BeanDescription;
2728
import com.fasterxml.jackson.databind.ObjectMapper;
2829
import com.fasterxml.jackson.databind.SerializationConfig;
2930
import com.fasterxml.jackson.databind.SerializationFeature;
31+
import com.fasterxml.jackson.databind.SerializerProvider;
3032
import com.fasterxml.jackson.databind.introspect.Annotated;
3133
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod;
3234
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
@@ -37,6 +39,8 @@
3739
import com.fasterxml.jackson.databind.ser.SerializerFactory;
3840
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
3941
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;
42+
import org.apache.commons.logging.Log;
43+
import org.apache.commons.logging.LogFactory;
4044

4145
import org.springframework.beans.BeansException;
4246
import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData;
@@ -64,7 +68,7 @@
6468
public class ConfigurationPropertiesReportEndpoint
6569
extends AbstractEndpoint<Map<String, Object>> implements ApplicationContextAware {
6670

67-
private static final String CGLIB_FILTER_ID = "cglibFilter";
71+
private static final String CONFIGURATION_PROPERTIES_FILTER_ID = "configurationPropertiesFilter";
6872

6973
private final Sanitizer sanitizer = new Sanitizer();
7074

@@ -174,7 +178,7 @@ private Map<String, Object> safeSerialize(ObjectMapper mapper, Object bean,
174178
protected void configureObjectMapper(ObjectMapper mapper) {
175179
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
176180
mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
177-
applyCglibFilters(mapper);
181+
applyConfigurationPropertiesFilter(mapper);
178182
applySerializationModifier(mapper);
179183
}
180184

@@ -188,15 +192,11 @@ private void applySerializationModifier(ObjectMapper mapper) {
188192
mapper.setSerializerFactory(factory);
189193
}
190194

191-
/**
192-
* Configure PropertyFilter to make sure Jackson doesn't process CGLIB generated bean
193-
* properties.
194-
* @param mapper the object mapper
195-
*/
196-
private void applyCglibFilters(ObjectMapper mapper) {
197-
mapper.setAnnotationIntrospector(new CglibAnnotationIntrospector());
198-
mapper.setFilterProvider(new SimpleFilterProvider().addFilter(CGLIB_FILTER_ID,
199-
new CglibBeanPropertyFilter()));
195+
private void applyConfigurationPropertiesFilter(ObjectMapper mapper) {
196+
mapper.setAnnotationIntrospector(
197+
new ConfigurationPropertiesAnnotationIntrospector());
198+
mapper.setFilterProvider(new SimpleFilterProvider()
199+
.setDefaultFilter(new ConfigurationPropertiesPropertyFilter()));
200200
}
201201

202202
/**
@@ -275,25 +275,35 @@ else if (item instanceof List) {
275275
* properties.
276276
*/
277277
@SuppressWarnings("serial")
278-
private static class CglibAnnotationIntrospector
278+
private static class ConfigurationPropertiesAnnotationIntrospector
279279
extends JacksonAnnotationIntrospector {
280280

281281
@Override
282282
public Object findFilterId(Annotated a) {
283283
Object id = super.findFilterId(a);
284284
if (id == null) {
285-
id = CGLIB_FILTER_ID;
285+
id = CONFIGURATION_PROPERTIES_FILTER_ID;
286286
}
287287
return id;
288288
}
289289

290290
}
291291

292292
/**
293-
* {@link SimpleBeanPropertyFilter} to filter out all bean properties whose names
294-
* start with '$$'.
293+
* {@link SimpleBeanPropertyFilter} for serialization of
294+
* {@link ConfigurationProperties} beans. The filter hides:
295+
*
296+
* <ul>
297+
* <li>Properties that have a name starting with '$$'.
298+
* <li>Properties that are self-referential.
299+
* <li>Properties that throw an exception when retrieving their value.
300+
* </ul>
295301
*/
296-
private static class CglibBeanPropertyFilter extends SimpleBeanPropertyFilter {
302+
private static class ConfigurationPropertiesPropertyFilter
303+
extends SimpleBeanPropertyFilter {
304+
305+
private static final Log logger = LogFactory
306+
.getLog(ConfigurationPropertiesPropertyFilter.class);
297307

298308
@Override
299309
protected boolean include(BeanPropertyWriter writer) {
@@ -309,6 +319,31 @@ private boolean include(String name) {
309319
return !name.startsWith("$$");
310320
}
311321

322+
@Override
323+
public void serializeAsField(Object pojo, JsonGenerator jgen,
324+
SerializerProvider provider, PropertyWriter writer) throws Exception {
325+
if (writer instanceof BeanPropertyWriter) {
326+
try {
327+
if (pojo == ((BeanPropertyWriter) writer).get(pojo)) {
328+
if (logger.isDebugEnabled()) {
329+
logger.debug("Skipping '" + writer.getFullName() + "' on '"
330+
+ pojo.getClass().getName()
331+
+ "' as it is self-referential");
332+
}
333+
return;
334+
}
335+
}
336+
catch (Exception ex) {
337+
if (logger.isDebugEnabled()) {
338+
logger.debug("Skipping '" + writer.getFullName() + "' on '"
339+
+ pojo.getClass().getName() + "' as an exception "
340+
+ "was thrown when retrieving its value", ex);
341+
}
342+
return;
343+
}
344+
}
345+
super.serializeAsField(pojo, jgen, provider, writer);
346+
}
312347
}
313348

314349
/**

spring-boot-actuator/src/test/java/org/springframework/boot/actuate/endpoint/ConfigurationPropertiesReportEndpointSerializationTests.java

Lines changed: 99 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.util.List;
2323
import java.util.Map;
2424

25+
import com.zaxxer.hikari.HikariDataSource;
2526
import org.junit.After;
2627
import org.junit.Before;
2728
import org.junit.Test;
@@ -100,8 +101,8 @@ public void testNestedNaming() throws Exception {
100101

101102
@Test
102103
@SuppressWarnings("unchecked")
103-
public void testCycle() throws Exception {
104-
this.context.register(CycleConfig.class);
104+
public void testSelfReferentialProperty() throws Exception {
105+
this.context.register(SelfReferentialConfig.class);
105106
EnvironmentTestUtils.addEnvironment(this.context, "foo.name:foo");
106107
this.context.refresh();
107108
ConfigurationPropertiesReportEndpoint report = this.context
@@ -114,8 +115,30 @@ public void testCycle() throws Exception {
114115
Map<String, Object> map = (Map<String, Object>) nestedProperties
115116
.get("properties");
116117
assertThat(map).isNotNull();
117-
assertThat(map).hasSize(1);
118-
assertThat(map.get("error")).isEqualTo("Cannot serialize 'foo'");
118+
assertThat(map).containsOnlyKeys("bar", "name");
119+
assertThat(map).containsEntry("name", "foo");
120+
Map<String, Object> bar = (Map<String, Object>) map.get("bar");
121+
assertThat(bar).containsOnlyKeys("name");
122+
assertThat(bar).containsEntry("name", "123456");
123+
}
124+
125+
@Test
126+
@SuppressWarnings("unchecked")
127+
public void testCycle() {
128+
this.context.register(CycleConfig.class);
129+
this.context.refresh();
130+
ConfigurationPropertiesReportEndpoint report = this.context
131+
.getBean(ConfigurationPropertiesReportEndpoint.class);
132+
Map<String, Object> properties = report.invoke();
133+
Map<String, Object> nestedProperties = (Map<String, Object>) properties
134+
.get("cycle");
135+
assertThat(nestedProperties).isNotNull();
136+
assertThat(nestedProperties.get("prefix")).isEqualTo("cycle");
137+
Map<String, Object> map = (Map<String, Object>) nestedProperties
138+
.get("properties");
139+
assertThat(map).isNotNull();
140+
assertThat(map).containsOnlyKeys("error");
141+
assertThat(map).containsEntry("error", "Cannot serialize 'cycle'");
119142
}
120143

121144
@Test
@@ -149,7 +172,6 @@ public void testEmptyMapIsNotAdded() throws Exception {
149172
Map<String, Object> nestedProperties = (Map<String, Object>) properties
150173
.get("foo");
151174
assertThat(nestedProperties).isNotNull();
152-
System.err.println(nestedProperties);
153175
assertThat(nestedProperties.get("prefix")).isEqualTo("foo");
154176
Map<String, Object> map = (Map<String, Object>) nestedProperties
155177
.get("properties");
@@ -190,7 +212,6 @@ public void testInetAddress() throws Exception {
190212
Map<String, Object> nestedProperties = (Map<String, Object>) properties
191213
.get("foo");
192214
assertThat(nestedProperties).isNotNull();
193-
System.err.println(nestedProperties);
194215
assertThat(nestedProperties.get("prefix")).isEqualTo("foo");
195216
Map<String, Object> map = (Map<String, Object>) nestedProperties
196217
.get("properties");
@@ -223,6 +244,20 @@ public void testInitializedMapAndList() throws Exception {
223244
assertThat(list).containsExactly("abc");
224245
}
225246

247+
@Test
248+
@SuppressWarnings("unchecked")
249+
public void hikariDataSourceConfigurationPropertiesBeanCanBeSerialized() {
250+
this.context = new AnnotationConfigApplicationContext();
251+
this.context.register(HikariDataSourceConfig.class);
252+
this.context.refresh();
253+
ConfigurationPropertiesReportEndpoint endpoint = this.context
254+
.getBean(ConfigurationPropertiesReportEndpoint.class);
255+
Map<String, Object> properties = endpoint.invoke();
256+
Map<String, Object> nestedProperties = (Map<String, Object>) ((Map<String, Object>) properties
257+
.get("hikariDataSource")).get("properties");
258+
assertThat(nestedProperties).doesNotContainKey("error");
259+
}
260+
226261
@Configuration
227262
@EnableConfigurationProperties
228263
public static class Base {
@@ -248,24 +283,12 @@ public Foo foo() {
248283

249284
@Configuration
250285
@Import(Base.class)
251-
public static class CycleConfig {
286+
public static class SelfReferentialConfig {
252287

253288
@Bean
254289
@ConfigurationProperties(prefix = "foo")
255-
public Cycle foo() {
256-
return new Cycle();
257-
}
258-
259-
}
260-
261-
@Configuration
262-
@Import(Base.class)
263-
public static class MetadataCycleConfig {
264-
265-
@Bean
266-
@ConfigurationProperties(prefix = "bar")
267-
public Cycle foo() {
268-
return new Cycle();
290+
public SelfReferential foo() {
291+
return new SelfReferential();
269292
}
270293

271294
}
@@ -373,11 +396,11 @@ public void setName(String name) {
373396

374397
}
375398

376-
public static class Cycle extends Foo {
399+
public static class SelfReferential extends Foo {
377400

378401
private Foo self;
379402

380-
public Cycle() {
403+
public SelfReferential() {
381404
this.self = this;
382405
}
383406

@@ -449,4 +472,57 @@ public List<String> getList() {
449472

450473
}
451474

475+
static class Cycle {
476+
477+
private final Alpha alpha = new Alpha(this);
478+
479+
public Alpha getAlpha() {
480+
return this.alpha;
481+
}
482+
483+
static class Alpha {
484+
485+
private final Cycle cycle;
486+
487+
Alpha(Cycle cycle) {
488+
this.cycle = cycle;
489+
}
490+
491+
public Cycle getCycle() {
492+
return this.cycle;
493+
}
494+
495+
}
496+
497+
}
498+
499+
@Configuration
500+
@Import(Base.class)
501+
static class CycleConfig {
502+
503+
@Bean
504+
@ConfigurationProperties(prefix = "cycle")
505+
public Cycle cycle() {
506+
return new Cycle();
507+
}
508+
509+
}
510+
511+
@Configuration
512+
@EnableConfigurationProperties
513+
static class HikariDataSourceConfig {
514+
515+
@Bean
516+
public ConfigurationPropertiesReportEndpoint endpoint() {
517+
return new ConfigurationPropertiesReportEndpoint();
518+
}
519+
520+
@Bean
521+
@ConfigurationProperties(prefix = "test.datasource")
522+
public HikariDataSource hikariDataSource() {
523+
return new HikariDataSource();
524+
}
525+
526+
}
527+
452528
}

0 commit comments

Comments
 (0)