16
16
17
17
package org .springframework .boot .actuate .endpoint ;
18
18
19
+ import java .io .IOException ;
19
20
import java .util .ArrayList ;
21
+ import java .util .Collection ;
22
+ import java .util .Collections ;
20
23
import java .util .HashMap ;
24
+ import java .util .HashSet ;
21
25
import java .util .List ;
22
26
import java .util .Map ;
27
+ import java .util .Set ;
23
28
29
+ import org .apache .commons .logging .Log ;
30
+ import org .apache .commons .logging .LogFactory ;
31
+ import org .springframework .beans .BeanWrapperImpl ;
24
32
import org .springframework .beans .BeansException ;
25
33
import org .springframework .boot .context .properties .ConfigurationBeanFactoryMetaData ;
26
34
import org .springframework .boot .context .properties .ConfigurationProperties ;
27
35
import org .springframework .context .ApplicationContext ;
28
36
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 ;
31
40
import org .springframework .util .StringUtils ;
32
41
42
+ import com .fasterxml .jackson .core .JsonParseException ;
33
43
import com .fasterxml .jackson .databind .BeanDescription ;
44
+ import com .fasterxml .jackson .databind .JsonMappingException ;
34
45
import com .fasterxml .jackson .databind .ObjectMapper ;
35
46
import com .fasterxml .jackson .databind .SerializationConfig ;
36
47
import com .fasterxml .jackson .databind .SerializationFeature ;
@@ -64,12 +75,17 @@ public class ConfigurationPropertiesReportEndpoint extends
64
75
65
76
private static final String CGLIB_FILTER_ID = "cglibFilter" ;
66
77
78
+ private static final Log logger = LogFactory
79
+ .getLog (ConfigurationPropertiesReportEndpoint .class );
80
+
67
81
private final Sanitizer sanitizer = new Sanitizer ();
68
82
69
83
private ApplicationContext context ;
70
84
71
85
private ConfigurationBeanFactoryMetaData beanFactoryMetaData ;
72
86
87
+ private ConfigurationPropertiesMetaData metadata = new ConfigurationPropertiesMetaData ();
88
+
73
89
public ConfigurationPropertiesReportEndpoint () {
74
90
super ("configprops" );
75
91
}
@@ -97,7 +113,6 @@ public Map<String, Object> invoke() {
97
113
* Extract beans annotated {@link ConfigurationProperties} and serialize into
98
114
* {@link Map}.
99
115
*/
100
- @ SuppressWarnings ("unchecked" )
101
116
protected Map <String , Object > extract (ApplicationContext context ) {
102
117
103
118
Map <String , Object > result = new HashMap <String , Object >();
@@ -116,8 +131,9 @@ protected Map<String, Object> extract(ApplicationContext context) {
116
131
String beanName = entry .getKey ();
117
132
Object bean = entry .getValue ();
118
133
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 )));
121
137
result .put (beanName , root );
122
138
}
123
139
@@ -128,12 +144,31 @@ protected Map<String, Object> extract(ApplicationContext context) {
128
144
return result ;
129
145
}
130
146
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
+
131
165
/**
132
166
* Configure Jackson's {@link ObjectMapper} to be used to serialize the
133
167
* {@link ConfigurationProperties} objects into a {@link Map} structure.
134
168
*/
135
169
protected void configureObjectMapper (ObjectMapper mapper ) {
136
170
mapper .configure (SerializationFeature .FAIL_ON_EMPTY_BEANS , false );
171
+ mapper .configure (SerializationFeature .WRITE_NULL_MAP_VALUES , false );
137
172
applyCglibFilters (mapper );
138
173
applySerializationModifier (mapper );
139
174
}
@@ -148,7 +183,7 @@ private void applySerializationModifier(ObjectMapper mapper) {
148
183
}
149
184
150
185
/**
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
152
187
* properties.
153
188
*/
154
189
private void applyCglibFilters (ObjectMapper mapper ) {
@@ -239,25 +274,182 @@ private boolean include(String name) {
239
274
240
275
protected static class GenericSerializerModifier extends BeanSerializerModifier {
241
276
242
- private ConversionService conversionService = new DefaultConversionService ();
243
-
244
277
@ Override
245
278
public List <BeanPropertyWriter > changeProperties (SerializationConfig config ,
246
279
BeanDescription beanDesc , List <BeanPropertyWriter > beanProperties ) {
247
280
List <BeanPropertyWriter > result = new ArrayList <BeanPropertyWriter >();
248
281
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 ) {
255
284
result .add (writer );
256
285
}
257
286
}
258
287
return result ;
259
288
}
260
289
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
+
261
453
}
262
454
263
455
}
0 commit comments