Skip to content

Commit 6ad626d

Browse files
committed
Allows /configprops endpoint to show nested properties
Including maps and lists. Beans with no metadata (in /META-INF/*spring-configuration-metadata.json) are just serialized as they come (so might have problems like cycles). Serialization errors are caught and rendered as an "error" for that bean. Any problems can be fixed by preparing metadata and specifying which properties are to be rendered that way. Fixes gh-1746, fixes gh-1921
1 parent fa64f43 commit 6ad626d

File tree

6 files changed

+554
-18
lines changed

6 files changed

+554
-18
lines changed

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
.project
1010
.settings
1111
.metadata
12+
.factorypath
1213
bin
1314
build
1415
lib/
@@ -28,4 +29,5 @@ overridedb.*
2829
*.ipr
2930
*.iws
3031
.idea
31-
*.jar
32+
*.jar
33+
.DS_Store

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

+206-14
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,32 @@
1616

1717
package org.springframework.boot.actuate.endpoint;
1818

19+
import java.io.IOException;
1920
import java.util.ArrayList;
21+
import java.util.Collection;
22+
import java.util.Collections;
2023
import java.util.HashMap;
24+
import java.util.HashSet;
2125
import java.util.List;
2226
import java.util.Map;
27+
import java.util.Set;
2328

29+
import org.apache.commons.logging.Log;
30+
import org.apache.commons.logging.LogFactory;
31+
import org.springframework.beans.BeanWrapperImpl;
2432
import org.springframework.beans.BeansException;
2533
import org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData;
2634
import org.springframework.boot.context.properties.ConfigurationProperties;
2735
import org.springframework.context.ApplicationContext;
2836
import org.springframework.context.ApplicationContextAware;
29-
import org.springframework.core.convert.ConversionService;
30-
import org.springframework.core.convert.support.DefaultConversionService;
37+
import org.springframework.core.io.Resource;
38+
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
39+
import org.springframework.util.ClassUtils;
3140
import org.springframework.util.StringUtils;
3241

42+
import com.fasterxml.jackson.core.JsonParseException;
3343
import com.fasterxml.jackson.databind.BeanDescription;
44+
import com.fasterxml.jackson.databind.JsonMappingException;
3445
import com.fasterxml.jackson.databind.ObjectMapper;
3546
import com.fasterxml.jackson.databind.SerializationConfig;
3647
import com.fasterxml.jackson.databind.SerializationFeature;
@@ -64,12 +75,17 @@ public class ConfigurationPropertiesReportEndpoint extends
6475

6576
private static final String CGLIB_FILTER_ID = "cglibFilter";
6677

78+
private static final Log logger = LogFactory
79+
.getLog(ConfigurationPropertiesReportEndpoint.class);
80+
6781
private final Sanitizer sanitizer = new Sanitizer();
6882

6983
private ApplicationContext context;
7084

7185
private ConfigurationBeanFactoryMetaData beanFactoryMetaData;
7286

87+
private ConfigurationPropertiesMetaData metadata = new ConfigurationPropertiesMetaData();
88+
7389
public ConfigurationPropertiesReportEndpoint() {
7490
super("configprops");
7591
}
@@ -97,7 +113,6 @@ public Map<String, Object> invoke() {
97113
* Extract beans annotated {@link ConfigurationProperties} and serialize into
98114
* {@link Map}.
99115
*/
100-
@SuppressWarnings("unchecked")
101116
protected Map<String, Object> extract(ApplicationContext context) {
102117

103118
Map<String, Object> result = new HashMap<String, Object>();
@@ -116,8 +131,9 @@ protected Map<String, Object> extract(ApplicationContext context) {
116131
String beanName = entry.getKey();
117132
Object bean = entry.getValue();
118133
Map<String, Object> root = new HashMap<String, Object>();
119-
root.put("prefix", extractPrefix(context, beanName, bean));
120-
root.put("properties", sanitize(mapper.convertValue(bean, Map.class)));
134+
String prefix = extractPrefix(context, beanName, bean);
135+
root.put("prefix", prefix);
136+
root.put("properties", sanitize(safeSerialize(mapper, bean, prefix)));
121137
result.put(beanName, root);
122138
}
123139

@@ -128,12 +144,31 @@ protected Map<String, Object> extract(ApplicationContext context) {
128144
return result;
129145
}
130146

147+
/**
148+
* Cautiously serialize the bean to a map (returning a map with an error message
149+
* instead of throwing an exception if there is a problem).
150+
*/
151+
private Map<String, Object> safeSerialize(ObjectMapper mapper, Object bean,
152+
String prefix) {
153+
try {
154+
@SuppressWarnings("unchecked")
155+
Map<String, Object> result = new HashMap<String, Object>(mapper.convertValue(
156+
this.metadata.extractMap(bean, prefix), Map.class));
157+
return result;
158+
}
159+
catch (Exception e) {
160+
return new HashMap<String, Object>(Collections.<String, Object> singletonMap(
161+
"error", "Cannot serialize '" + prefix + "'"));
162+
}
163+
}
164+
131165
/**
132166
* Configure Jackson's {@link ObjectMapper} to be used to serialize the
133167
* {@link ConfigurationProperties} objects into a {@link Map} structure.
134168
*/
135169
protected void configureObjectMapper(ObjectMapper mapper) {
136170
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
171+
mapper.configure(SerializationFeature.WRITE_NULL_MAP_VALUES, false);
137172
applyCglibFilters(mapper);
138173
applySerializationModifier(mapper);
139174
}
@@ -148,7 +183,7 @@ private void applySerializationModifier(ObjectMapper mapper) {
148183
}
149184

150185
/**
151-
* Configure PropertyFiler to make sure Jackson doesn't process CGLIB generated bean
186+
* Configure PropertyFilter to make sure Jackson doesn't process CGLIB generated bean
152187
* properties.
153188
*/
154189
private void applyCglibFilters(ObjectMapper mapper) {
@@ -239,25 +274,182 @@ private boolean include(String name) {
239274

240275
protected static class GenericSerializerModifier extends BeanSerializerModifier {
241276

242-
private ConversionService conversionService = new DefaultConversionService();
243-
244277
@Override
245278
public List<BeanPropertyWriter> changeProperties(SerializationConfig config,
246279
BeanDescription beanDesc, List<BeanPropertyWriter> beanProperties) {
247280
List<BeanPropertyWriter> result = new ArrayList<BeanPropertyWriter>();
248281
for (BeanPropertyWriter writer : beanProperties) {
249-
AnnotatedMethod setter = beanDesc.findMethod(
250-
"set" + StringUtils.capitalize(writer.getName()),
251-
new Class<?>[] { writer.getPropertyType() });
252-
if (setter != null
253-
&& this.conversionService.canConvert(String.class,
254-
writer.getPropertyType())) {
282+
boolean readable = isReadable(beanDesc, writer);
283+
if (readable) {
255284
result.add(writer);
256285
}
257286
}
258287
return result;
259288
}
260289

290+
private boolean isReadable(BeanDescription beanDesc, BeanPropertyWriter writer) {
291+
String parenType = beanDesc.getType().getTypeName();
292+
String type = writer.getType().getTypeName();
293+
AnnotatedMethod setter = beanDesc.findMethod(
294+
"set" + StringUtils.capitalize(writer.getName()),
295+
new Class<?>[] { writer.getPropertyType() });
296+
// If there's a setter, we assume it's OK to report on the value,
297+
// similarly, if there's no setter but the package names match, we assume
298+
// that its a nested class used solely for binding to config props, so it
299+
// should be kosher. This filter is not used if there is JSON metadata for
300+
// the property, so it's mainly for user-defined beans.
301+
boolean readable = setter != null
302+
|| ClassUtils.getPackageName(parenType).equals(
303+
ClassUtils.getPackageName(type));
304+
return readable;
305+
}
306+
}
307+
308+
/**
309+
* Convenience class for grabbing and caching valid property names from
310+
* /META-INF/spring-configuration-metadata.json so that metadata that is known to be
311+
* valid can be used to pull the correct nested properties out of beans that might
312+
* otherwise be tricky (contain cycles or other unserializable properties).
313+
*/
314+
protected static class ConfigurationPropertiesMetaData {
315+
316+
private Map<String, Set<String>> matched = new HashMap<String, Set<String>>();
317+
private Set<String> keys = null;
318+
319+
public boolean matches(String prefix) {
320+
if (this.matched.containsKey(prefix)) {
321+
return matchesInternal(prefix);
322+
}
323+
synchronized (this.matched) {
324+
if (this.matched.containsKey(prefix)) {
325+
return matchesInternal(prefix);
326+
}
327+
this.matched.put(prefix, findKeys(prefix));
328+
}
329+
return matchesInternal(prefix);
330+
}
331+
332+
private boolean matchesInternal(String prefix) {
333+
if (this.matched.get(prefix) != null) {
334+
return true;
335+
}
336+
else {
337+
return false;
338+
}
339+
}
340+
341+
private Set<String> findKeys(String prefix) {
342+
343+
HashSet<String> set = new HashSet<String>();
344+
345+
try {
346+
if (this.keys == null) {
347+
348+
this.keys = new HashSet<String>();
349+
ObjectMapper mapper = new ObjectMapper();
350+
Resource[] resources = new PathMatchingResourcePatternResolver()
351+
.getResources("classpath*:/META-INF/*spring-configuration-metadata.json");
352+
for (Resource resource : resources) {
353+
addKeys(mapper, resource);
354+
}
355+
356+
}
357+
}
358+
catch (IOException e) {
359+
logger.warn("Could not deserialize config properties metadata", e);
360+
}
361+
for (String key : this.keys) {
362+
if (key.length() > prefix.length()
363+
&& key.startsWith(prefix)
364+
&& ".".equals(key.substring(prefix.length(), prefix.length() + 1))) {
365+
set.add(key.substring(prefix.length() + 1));
366+
}
367+
}
368+
if (set.isEmpty()) {
369+
return null;
370+
}
371+
return set;
372+
}
373+
374+
private void addKeys(ObjectMapper mapper, Resource resource) throws IOException,
375+
JsonParseException, JsonMappingException {
376+
@SuppressWarnings("unchecked")
377+
Map<String, Object> map = mapper.readValue(resource.getInputStream(),
378+
Map.class);
379+
@SuppressWarnings("unchecked")
380+
Collection<Map<String, Object>> metadata = (Collection<Map<String, Object>>) map
381+
.get("properties");
382+
for (Map<String, Object> value : metadata) {
383+
try {
384+
if (value.containsKey("type")) {
385+
this.keys.add((String) value.get("name"));
386+
}
387+
}
388+
catch (Exception e) {
389+
logger.warn("Could not parse config properties metadata", e);
390+
}
391+
}
392+
}
393+
394+
public Object extractMap(Object bean, String prefix) {
395+
if (!matches(prefix)) {
396+
return bean;
397+
}
398+
Map<String, Object> map = new HashMap<String, Object>();
399+
for (String key : this.matched.get(prefix)) {
400+
addProperty(bean, key, map);
401+
}
402+
return map;
403+
}
404+
405+
private void addProperty(Object bean, String key, Map<String, Object> map) {
406+
String prefix = key.contains(".") ? StringUtils.split(key, ".")[0] : key;
407+
String suffix = key.length() > prefix.length() ? key.substring(prefix
408+
.length() + 1) : null;
409+
String property = prefix;
410+
if (bean instanceof Map) {
411+
@SuppressWarnings("unchecked")
412+
Map<String, Object> value = (Map<String, Object>) bean;
413+
bean = new MapHolder(value);
414+
property = "map[" + property + "]";
415+
}
416+
BeanWrapperImpl wrapper = new BeanWrapperImpl(bean);
417+
try {
418+
Object value = wrapper.getPropertyValue(property);
419+
if (value instanceof Map) {
420+
Map<String, Object> nested = new HashMap<String, Object>();
421+
map.put(prefix, nested);
422+
if (suffix != null) {
423+
addProperty(value, suffix, nested);
424+
}
425+
}
426+
else {
427+
map.put(prefix, value);
428+
}
429+
}
430+
catch (Exception e) {
431+
// Probably just lives on a different bean (it happens)
432+
logger.debug("Could not parse config properties metadata '" + key + "': "
433+
+ e.getMessage());
434+
}
435+
}
436+
437+
protected static class MapHolder {
438+
Map<String, Object> map = new HashMap<String, Object>();
439+
440+
public MapHolder(Map<String, Object> bean) {
441+
this.map.putAll(bean);
442+
}
443+
444+
public Map<String, Object> getMap() {
445+
return this.map;
446+
}
447+
448+
public void setMap(Map<String, Object> map) {
449+
this.map = map;
450+
}
451+
}
452+
261453
}
262454

263455
}

0 commit comments

Comments
 (0)