spring ConfigurationPropertiesReportEndpoint 源码
springboot ConfigurationPropertiesReportEndpoint 代码
文件路径:/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java
/*
* Copyright 2012-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.actuate.context.properties;
import java.lang.reflect.Constructor;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.MapperFeature;
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;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerFactory;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
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 com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.boot.actuate.endpoint.SanitizableData;
import org.springframework.boot.actuate.endpoint.Sanitizer;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.boot.context.properties.BoundConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.ConfigurationPropertiesBean;
import org.springframework.boot.context.properties.ConfigurationPropertiesBindConstructorProvider;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Name;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.origin.Origin;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.env.PropertySource;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* {@link Endpoint @Endpoint} to expose application properties from
* {@link ConfigurationProperties @ConfigurationProperties} annotated beans.
*
* <p>
* To protect sensitive information from being exposed, certain property values are masked
* if their names end with a set of configurable values (default "password" and "secret").
* Configure property names by using
* {@code management.endpoint.configprops.keys-to-sanitize} in your Spring Boot
* application configuration.
*
* @author Christian Dupuis
* @author Dave Syer
* @author Stephane Nicoll
* @author Madhura Bhave
* @author Andy Wilkinson
* @author Chris Bono
* @since 2.0.0
*/
@Endpoint(id = "configprops")
public class ConfigurationPropertiesReportEndpoint implements ApplicationContextAware {
private static final String CONFIGURATION_PROPERTIES_FILTER_ID = "configurationPropertiesFilter";
private final Sanitizer sanitizer;
private ApplicationContext context;
private ObjectMapper objectMapper;
public ConfigurationPropertiesReportEndpoint() {
this(Collections.emptyList());
}
public ConfigurationPropertiesReportEndpoint(Iterable<SanitizingFunction> sanitizingFunctions) {
this.sanitizer = new Sanitizer(sanitizingFunctions);
}
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context = context;
}
public void setKeysToSanitize(String... keysToSanitize) {
this.sanitizer.setKeysToSanitize(keysToSanitize);
}
public void keysToSanitize(String... keysToSanitize) {
this.sanitizer.keysToSanitize(keysToSanitize);
}
@ReadOperation
public ApplicationConfigurationProperties configurationProperties() {
return extract(this.context, (bean) -> true);
}
@ReadOperation
public ApplicationConfigurationProperties configurationPropertiesWithPrefix(@Selector String prefix) {
return extract(this.context, (bean) -> bean.getAnnotation().prefix().startsWith(prefix));
}
private ApplicationConfigurationProperties extract(ApplicationContext context,
Predicate<ConfigurationPropertiesBean> beanFilterPredicate) {
ObjectMapper mapper = getObjectMapper();
Map<String, ContextConfigurationProperties> contexts = new HashMap<>();
ApplicationContext target = context;
while (target != null) {
contexts.put(target.getId(), describeBeans(mapper, target, beanFilterPredicate));
target = target.getParent();
}
return new ApplicationConfigurationProperties(contexts);
}
private ObjectMapper getObjectMapper() {
if (this.objectMapper == null) {
JsonMapper.Builder builder = JsonMapper.builder();
configureJsonMapper(builder);
this.objectMapper = builder.build();
}
return this.objectMapper;
}
/**
* Configure Jackson's {@link JsonMapper} to be used to serialize the
* {@link ConfigurationProperties @ConfigurationProperties} objects into a {@link Map}
* structure.
* @param builder the json mapper builder
* @since 2.6.0
*/
protected void configureJsonMapper(JsonMapper.Builder builder) {
builder.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
builder.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
builder.configure(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS, false);
JsonMapper.builder();
builder.configure(MapperFeature.USE_STD_BEAN_NAMING, true);
builder.serializationInclusion(Include.NON_NULL);
applyConfigurationPropertiesFilter(builder);
applySerializationModifier(builder);
builder.addModule(new JavaTimeModule());
}
private void applyConfigurationPropertiesFilter(JsonMapper.Builder builder) {
builder.annotationIntrospector(new ConfigurationPropertiesAnnotationIntrospector());
builder.filterProvider(
new SimpleFilterProvider().setDefaultFilter(new ConfigurationPropertiesPropertyFilter()));
}
/**
* Ensure only bindable and non-cyclic bean properties are reported.
* @param builder the JsonMapper builder
*/
private void applySerializationModifier(JsonMapper.Builder builder) {
SerializerFactory factory = BeanSerializerFactory.instance
.withSerializerModifier(new GenericSerializerModifier());
builder.serializerFactory(factory);
}
private ContextConfigurationProperties describeBeans(ObjectMapper mapper, ApplicationContext context,
Predicate<ConfigurationPropertiesBean> beanFilterPredicate) {
Map<String, ConfigurationPropertiesBean> beans = ConfigurationPropertiesBean.getAll(context);
Map<String, ConfigurationPropertiesBeanDescriptor> descriptors = beans.values().stream()
.filter(beanFilterPredicate)
.collect(Collectors.toMap(ConfigurationPropertiesBean::getName, (bean) -> describeBean(mapper, bean)));
return new ContextConfigurationProperties(descriptors,
(context.getParent() != null) ? context.getParent().getId() : null);
}
private ConfigurationPropertiesBeanDescriptor describeBean(ObjectMapper mapper, ConfigurationPropertiesBean bean) {
String prefix = bean.getAnnotation().prefix();
Map<String, Object> serialized = safeSerialize(mapper, bean.getInstance(), prefix);
Map<String, Object> properties = sanitize(prefix, serialized);
Map<String, Object> inputs = getInputs(prefix, serialized);
return new ConfigurationPropertiesBeanDescriptor(prefix, properties, inputs);
}
/**
* Cautiously serialize the bean to a map (returning a map with an error message
* instead of throwing an exception if there is a problem).
* @param mapper the object mapper
* @param bean the source bean
* @param prefix the prefix
* @return the serialized instance
*/
@SuppressWarnings({ "unchecked" })
private Map<String, Object> safeSerialize(ObjectMapper mapper, Object bean, String prefix) {
try {
return new HashMap<>(mapper.convertValue(bean, Map.class));
}
catch (Exception ex) {
return new HashMap<>(Collections.singletonMap("error", "Cannot serialize '" + prefix + "'"));
}
}
/**
* Sanitize all unwanted configuration properties to avoid leaking of sensitive
* information.
* @param prefix the property prefix
* @param map the source map
* @return the sanitized map
*/
@SuppressWarnings("unchecked")
private Map<String, Object> sanitize(String prefix, Map<String, Object> map) {
map.forEach((key, value) -> {
String qualifiedKey = getQualifiedKey(prefix, key);
if (value instanceof Map) {
map.put(key, sanitize(qualifiedKey, (Map<String, Object>) value));
}
else if (value instanceof List) {
map.put(key, sanitize(qualifiedKey, (List<Object>) value));
}
else {
map.put(key, sanitizeWithPropertySourceIfPresent(qualifiedKey, value));
}
});
return map;
}
private Object sanitizeWithPropertySourceIfPresent(String qualifiedKey, Object value) {
ConfigurationPropertyName currentName = getCurrentName(qualifiedKey);
ConfigurationProperty candidate = getCandidate(currentName);
PropertySource<?> propertySource = getPropertySource(candidate);
if (propertySource != null) {
SanitizableData data = new SanitizableData(propertySource, qualifiedKey, value);
return this.sanitizer.sanitize(data);
}
SanitizableData data = new SanitizableData(null, qualifiedKey, value);
return this.sanitizer.sanitize(data);
}
private PropertySource<?> getPropertySource(ConfigurationProperty configurationProperty) {
if (configurationProperty == null) {
return null;
}
ConfigurationPropertySource source = configurationProperty.getSource();
Object underlyingSource = (source != null) ? source.getUnderlyingSource() : null;
return (underlyingSource instanceof PropertySource<?>) ? (PropertySource<?>) underlyingSource : null;
}
private ConfigurationPropertyName getCurrentName(String qualifiedKey) {
return ConfigurationPropertyName.adapt(qualifiedKey, '.');
}
private ConfigurationProperty getCandidate(ConfigurationPropertyName currentName) {
BoundConfigurationProperties bound = BoundConfigurationProperties.get(this.context);
if (bound == null) {
return null;
}
ConfigurationProperty candidate = bound.get(currentName);
if (candidate == null && currentName.isLastElementIndexed()) {
candidate = bound.get(currentName.chop(currentName.getNumberOfElements() - 1));
}
return candidate;
}
@SuppressWarnings("unchecked")
private List<Object> sanitize(String prefix, List<Object> list) {
List<Object> sanitized = new ArrayList<>();
int index = 0;
for (Object item : list) {
String name = prefix + "[" + index++ + "]";
if (item instanceof Map) {
sanitized.add(sanitize(name, (Map<String, Object>) item));
}
else if (item instanceof List) {
sanitized.add(sanitize(name, (List<Object>) item));
}
else {
sanitized.add(sanitizeWithPropertySourceIfPresent(name, item));
}
}
return sanitized;
}
@SuppressWarnings("unchecked")
private Map<String, Object> getInputs(String prefix, Map<String, Object> map) {
Map<String, Object> augmented = new LinkedHashMap<>(map);
map.forEach((key, value) -> {
String qualifiedKey = getQualifiedKey(prefix, key);
if (value instanceof Map) {
augmented.put(key, getInputs(qualifiedKey, (Map<String, Object>) value));
}
else if (value instanceof List) {
augmented.put(key, getInputs(qualifiedKey, (List<Object>) value));
}
else {
augmented.put(key, applyInput(qualifiedKey));
}
});
return augmented;
}
@SuppressWarnings("unchecked")
private List<Object> getInputs(String prefix, List<Object> list) {
List<Object> augmented = new ArrayList<>();
int index = 0;
for (Object item : list) {
String name = prefix + "[" + index++ + "]";
if (item instanceof Map) {
augmented.add(getInputs(name, (Map<String, Object>) item));
}
else if (item instanceof List) {
augmented.add(getInputs(name, (List<Object>) item));
}
else {
augmented.add(applyInput(name));
}
}
return augmented;
}
private Map<String, Object> applyInput(String qualifiedKey) {
ConfigurationPropertyName currentName = getCurrentName(qualifiedKey);
ConfigurationProperty candidate = getCandidate(currentName);
PropertySource<?> propertySource = getPropertySource(candidate);
if (propertySource != null) {
Object value = stringifyIfNecessary(candidate.getValue());
SanitizableData data = new SanitizableData(propertySource, currentName.toString(), value);
return getInput(candidate, this.sanitizer.sanitize(data));
}
return Collections.emptyMap();
}
private Map<String, Object> getInput(ConfigurationProperty candidate, Object sanitizedValue) {
Map<String, Object> input = new LinkedHashMap<>();
Origin origin = Origin.from(candidate);
List<Origin> originParents = Origin.parentsFrom(candidate);
input.put("value", sanitizedValue);
input.put("origin", (origin != null) ? origin.toString() : "none");
if (!originParents.isEmpty()) {
input.put("originParents", originParents.stream().map(Object::toString).toArray(String[]::new));
}
return input;
}
private Object stringifyIfNecessary(Object value) {
if (value == null || value.getClass().isPrimitive()) {
return value;
}
if (CharSequence.class.isAssignableFrom(value.getClass())) {
return value.toString();
}
return "Complex property value " + value.getClass().getName();
}
private String getQualifiedKey(String prefix, String key) {
return (prefix.isEmpty() ? prefix : prefix + ".") + key;
}
/**
* Extension to {@link JacksonAnnotationIntrospector} to suppress CGLIB generated bean
* properties.
*/
private static class ConfigurationPropertiesAnnotationIntrospector extends JacksonAnnotationIntrospector {
@Override
public Object findFilterId(Annotated a) {
Object id = super.findFilterId(a);
if (id == null) {
id = CONFIGURATION_PROPERTIES_FILTER_ID;
}
return id;
}
}
/**
* {@link SimpleBeanPropertyFilter} for serialization of
* {@link ConfigurationProperties @ConfigurationProperties} beans. The filter hides:
*
* <ul>
* <li>Properties that have a name starting with '$$'.
* <li>Properties that are self-referential.
* <li>Properties that throw an exception when retrieving their value.
* </ul>
*/
private static class ConfigurationPropertiesPropertyFilter extends SimpleBeanPropertyFilter {
private static final Log logger = LogFactory.getLog(ConfigurationPropertiesPropertyFilter.class);
@Override
protected boolean include(BeanPropertyWriter writer) {
return include(writer.getFullName().getSimpleName());
}
@Override
protected boolean include(PropertyWriter writer) {
return include(writer.getFullName().getSimpleName());
}
private boolean include(String name) {
return !name.startsWith("$$");
}
@Override
public void serializeAsField(Object pojo, JsonGenerator jgen, SerializerProvider provider,
PropertyWriter writer) throws Exception {
if (writer instanceof BeanPropertyWriter beanPropertyWriter) {
try {
if (pojo == beanPropertyWriter.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);
}
}
/**
* {@link BeanSerializerModifier} to return only relevant configuration properties.
*/
protected static class GenericSerializerModifier extends BeanSerializerModifier {
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
@Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription beanDesc,
List<BeanPropertyWriter> beanProperties) {
List<BeanPropertyWriter> result = new ArrayList<>();
Class<?> beanClass = beanDesc.getType().getRawClass();
Bindable<?> bindable = Bindable.of(ClassUtils.getUserClass(beanClass));
Constructor<?> bindConstructor = ConfigurationPropertiesBindConstructorProvider.INSTANCE
.getBindConstructor(bindable, false);
for (BeanPropertyWriter writer : beanProperties) {
if (isCandidate(beanDesc, writer, bindConstructor)) {
result.add(writer);
}
}
return result;
}
private boolean isCandidate(BeanDescription beanDesc, BeanPropertyWriter writer, Constructor<?> constructor) {
if (constructor != null) {
Parameter[] parameters = constructor.getParameters();
String[] names = PARAMETER_NAME_DISCOVERER.getParameterNames(constructor);
if (names == null) {
names = new String[parameters.length];
}
for (int i = 0; i < parameters.length; i++) {
String name = MergedAnnotations.from(parameters[i]).get(Name.class)
.getValue(MergedAnnotation.VALUE, String.class)
.orElse((names[i] != null) ? names[i] : parameters[i].getName());
if (name.equals(writer.getName())) {
return true;
}
}
}
return isReadable(beanDesc, writer);
}
private boolean isReadable(BeanDescription beanDesc, BeanPropertyWriter writer) {
Class<?> parentType = beanDesc.getType().getRawClass();
Class<?> type = writer.getType().getRawClass();
AnnotatedMethod setter = findSetter(beanDesc, writer);
// If there's a setter, we assume it's OK to report on the value,
// similarly, if there's no setter but the package names match, we assume
// that it is a nested class used solely for binding to config props, so it
// should be kosher. Lists and Maps are also auto-detected by default since
// that's what the metadata generator does. This filter is not used if there
// is JSON metadata for the property, so it's mainly for user-defined beans.
return (setter != null) || ClassUtils.getPackageName(parentType).equals(ClassUtils.getPackageName(type))
|| Map.class.isAssignableFrom(type) || Collection.class.isAssignableFrom(type);
}
private AnnotatedMethod findSetter(BeanDescription beanDesc, BeanPropertyWriter writer) {
String name = "set" + determineAccessorSuffix(writer.getName());
Class<?> type = writer.getType().getRawClass();
AnnotatedMethod setter = beanDesc.findMethod(name, new Class<?>[] { type });
// The enabled property of endpoints returns a boolean primitive but is set
// using a Boolean class
if (setter == null && type.equals(Boolean.TYPE)) {
setter = beanDesc.findMethod(name, new Class<?>[] { Boolean.class });
}
return setter;
}
/**
* Determine the accessor suffix of the specified {@code propertyName}, see
* section 8.8 "Capitalization of inferred names" of the JavaBean specs for more
* details.
* @param propertyName the property name to turn into an accessor suffix
* @return the accessor suffix for {@code propertyName}
*/
private String determineAccessorSuffix(String propertyName) {
if (propertyName.length() > 1 && Character.isUpperCase(propertyName.charAt(1))) {
return propertyName;
}
return StringUtils.capitalize(propertyName);
}
}
/**
* A description of an application's
* {@link ConfigurationProperties @ConfigurationProperties} beans. Primarily intended
* for serialization to JSON.
*/
public static final class ApplicationConfigurationProperties {
private final Map<String, ContextConfigurationProperties> contexts;
private ApplicationConfigurationProperties(Map<String, ContextConfigurationProperties> contexts) {
this.contexts = contexts;
}
public Map<String, ContextConfigurationProperties> getContexts() {
return this.contexts;
}
}
/**
* A description of an application context's
* {@link ConfigurationProperties @ConfigurationProperties} beans. Primarily intended
* for serialization to JSON.
*/
public static final class ContextConfigurationProperties {
private final Map<String, ConfigurationPropertiesBeanDescriptor> beans;
private final String parentId;
private ContextConfigurationProperties(Map<String, ConfigurationPropertiesBeanDescriptor> beans,
String parentId) {
this.beans = beans;
this.parentId = parentId;
}
public Map<String, ConfigurationPropertiesBeanDescriptor> getBeans() {
return this.beans;
}
public String getParentId() {
return this.parentId;
}
}
/**
* A description of a {@link ConfigurationProperties @ConfigurationProperties} bean.
* Primarily intended for serialization to JSON.
*/
public static final class ConfigurationPropertiesBeanDescriptor {
private final String prefix;
private final Map<String, Object> properties;
private final Map<String, Object> inputs;
private ConfigurationPropertiesBeanDescriptor(String prefix, Map<String, Object> properties,
Map<String, Object> inputs) {
this.prefix = prefix;
this.properties = properties;
this.inputs = inputs;
}
public String getPrefix() {
return this.prefix;
}
public Map<String, Object> getProperties() {
return this.properties;
}
public Map<String, Object> getInputs() {
return this.inputs;
}
}
}
相关信息
相关文章
0
赞
- 所属分类: 后端技术
- 本文标签: Spring Boot Java Spring
热门推荐
-
2、 - 优质文章
-
3、 gate.io
-
8、 golang
-
9、 openharmony
-
10、 Vue中input框自动聚焦