mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-05-03 03:43:54 +01:00
Support actuator with Jackson 2 only
See gh-47688
This commit is contained in:
+1
@@ -50,6 +50,7 @@ dependencies {
|
||||
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
|
||||
implementation("org.springframework.boot:spring-boot-starter")
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
implementation("org.springframework.boot:spring-boot-starter-jackson")
|
||||
|
||||
app(files(sourceSets.main.output))
|
||||
app("org.springframework.boot:spring-boot-webflux")
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ dependencies {
|
||||
compileOnly("jakarta.servlet:jakarta.servlet-api:6.0.0")
|
||||
|
||||
implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))
|
||||
implementation("org.springframework.boot:spring-boot-starter-web") {
|
||||
implementation("org.springframework.boot:spring-boot-starter-webmvc") {
|
||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
|
||||
}
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
|
||||
@@ -29,13 +29,13 @@ dependencies {
|
||||
api(project(":core:spring-boot-autoconfigure"))
|
||||
api(project(":module:spring-boot-actuator"))
|
||||
|
||||
implementation("tools.jackson.core:jackson-databind")
|
||||
|
||||
optional(project(":module:spring-boot-web-server"))
|
||||
optional("com.fasterxml.jackson.core:jackson-databind")
|
||||
optional("io.micrometer:micrometer-core")
|
||||
optional("io.projectreactor:reactor-core")
|
||||
optional("jakarta.servlet:jakarta.servlet-api")
|
||||
optional("org.springframework.security:spring-security-config")
|
||||
optional("tools.jackson.core:jackson-databind")
|
||||
|
||||
testFixturesImplementation(project(":core:spring-boot-test"))
|
||||
testFixturesImplementation(project(":test-support:spring-boot-test-support"))
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.autoconfigure.endpoint.jackson;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
|
||||
/**
|
||||
* {@link EnableAutoConfiguration Auto-configuration} for Endpoint Jackson 2 support.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 3.0.0
|
||||
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(ObjectMapper.class)
|
||||
@Deprecated(since = "4.0.0", forRemoval = true)
|
||||
@SuppressWarnings("removal")
|
||||
public final class Jackson2EndpointAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBooleanProperty(name = "management.endpoints.jackson.isolated-object-mapper", matchIfMissing = true)
|
||||
EndpointJackson2ObjectMapper jackson2EndpointJsonMapper() {
|
||||
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
|
||||
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
|
||||
SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
|
||||
.serializationInclusion(Include.NON_NULL)
|
||||
.build();
|
||||
return () -> objectMapper;
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -34,11 +34,11 @@ import org.springframework.context.annotation.Bean;
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ConditionalOnClass(ObjectMapper.class)
|
||||
public final class JacksonEndpointAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBooleanProperty(name = "management.endpoints.jackson.isolated-object-mapper", matchIfMissing = true)
|
||||
@ConditionalOnClass(ObjectMapper.class)
|
||||
EndpointJsonMapper endpointJsonMapper() {
|
||||
JsonMapper jsonMapper = JsonMapper.builder()
|
||||
.changeDefaultPropertyInclusion(
|
||||
|
||||
+4
-1
@@ -42,6 +42,7 @@ import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpointDisco
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
|
||||
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
|
||||
@@ -99,15 +100,17 @@ public final class JmxEndpointAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnSingleCandidate(MBeanServer.class)
|
||||
@ConditionalOnClass(ObjectMapper.class)
|
||||
JmxEndpointExporter jmxMBeanExporter(MBeanServer mBeanServer, EndpointObjectNameFactory endpointObjectNameFactory,
|
||||
ObjectProvider<ObjectMapper> objectMapper, JmxEndpointsSupplier jmxEndpointsSupplier) {
|
||||
JmxOperationResponseMapper responseMapper = new JacksonJmxOperationResponseMapper(
|
||||
objectMapper.getIfAvailable());
|
||||
return new JmxEndpointExporter(mBeanServer, endpointObjectNameFactory, responseMapper,
|
||||
jmxEndpointsSupplier.getEndpoints());
|
||||
|
||||
}
|
||||
|
||||
// FIXME
|
||||
|
||||
@Bean
|
||||
IncludeExcludeEndpointFilter<ExposableJmxEndpoint> jmxIncludeExcludePropertyEndpointFilter() {
|
||||
JmxEndpointProperties.Exposure exposure = this.properties.getExposure();
|
||||
|
||||
+1
@@ -5,6 +5,7 @@ org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpoin
|
||||
org.springframework.boot.actuate.autoconfigure.context.ShutdownEndpointAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.context.properties.ConfigurationPropertiesReportEndpointAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.endpoint.jackson.Jackson2EndpointAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.endpoint.jackson.JacksonEndpointAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration
|
||||
org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration
|
||||
|
||||
@@ -27,6 +27,8 @@ description = "Spring Boot Actuator"
|
||||
dependencies {
|
||||
api(project(":core:spring-boot"))
|
||||
|
||||
optional("com.fasterxml.jackson.core:jackson-databind")
|
||||
optional("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
|
||||
optional("com.github.ben-manes.caffeine:caffeine")
|
||||
optional("com.google.code.findbugs:jsr305")
|
||||
optional("com.zaxxer:HikariCP")
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.util.Map;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Strategy used by {@link ConfigurationPropertiesReportEndpoint} to serialize beans into
|
||||
* a {@link Map}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@FunctionalInterface
|
||||
interface BeanSerializer {
|
||||
|
||||
Map<String, @Nullable Object> serialize(@Nullable Object bean);
|
||||
|
||||
}
|
||||
+21
-255
@@ -16,10 +16,7 @@
|
||||
|
||||
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;
|
||||
@@ -28,31 +25,7 @@ import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tools.jackson.core.JsonGenerator;
|
||||
import tools.jackson.databind.BeanDescription;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.SerializationConfig;
|
||||
import tools.jackson.databind.SerializationContext;
|
||||
import tools.jackson.databind.SerializationFeature;
|
||||
import tools.jackson.databind.cfg.MapperConfig;
|
||||
import tools.jackson.databind.introspect.Annotated;
|
||||
import tools.jackson.databind.introspect.AnnotatedMethod;
|
||||
import tools.jackson.databind.introspect.DefaultAccessorNamingStrategy;
|
||||
import tools.jackson.databind.introspect.JacksonAnnotationIntrospector;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
import tools.jackson.databind.module.SimpleModule;
|
||||
import tools.jackson.databind.ser.BeanPropertyWriter;
|
||||
import tools.jackson.databind.ser.BeanSerializerFactory;
|
||||
import tools.jackson.databind.ser.PropertyWriter;
|
||||
import tools.jackson.databind.ser.SerializerFactory;
|
||||
import tools.jackson.databind.ser.ValueSerializerModifier;
|
||||
import tools.jackson.databind.ser.std.SimpleBeanPropertyFilter;
|
||||
import tools.jackson.databind.ser.std.SimpleFilterProvider;
|
||||
import tools.jackson.databind.ser.std.ToStringSerializer;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
|
||||
@@ -66,24 +39,15 @@ 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.bind.BindConstructorProvider;
|
||||
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.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
/**
|
||||
* {@link Endpoint @Endpoint} to expose application properties from
|
||||
@@ -107,20 +71,19 @@ import org.springframework.util.unit.DataSize;
|
||||
@Endpoint(id = "configprops")
|
||||
public class ConfigurationPropertiesReportEndpoint implements ApplicationContextAware {
|
||||
|
||||
private static final String CONFIGURATION_PROPERTIES_FILTER_ID = "configurationPropertiesFilter";
|
||||
|
||||
private final Sanitizer sanitizer;
|
||||
|
||||
private final Show showValues;
|
||||
|
||||
private final BeanSerializer serializer;
|
||||
|
||||
@SuppressWarnings("NullAway.Init")
|
||||
private ApplicationContext context;
|
||||
|
||||
private @Nullable ObjectMapper objectMapper;
|
||||
|
||||
public ConfigurationPropertiesReportEndpoint(Iterable<SanitizingFunction> sanitizingFunctions, Show showValues) {
|
||||
this.sanitizer = new Sanitizer(sanitizingFunctions);
|
||||
this.showValues = showValues;
|
||||
this.serializer = getBeanSerializer();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -151,74 +114,46 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
|
||||
|
||||
private ConfigurationPropertiesDescriptor getConfigurationProperties(ApplicationContext context,
|
||||
Predicate<ConfigurationPropertiesBean> beanFilterPredicate, boolean showUnsanitized) {
|
||||
ObjectMapper mapper = getObjectMapper();
|
||||
Map<@Nullable String, ContextConfigurationPropertiesDescriptor> contexts = new HashMap<>();
|
||||
ApplicationContext target = context;
|
||||
|
||||
while (target != null) {
|
||||
contexts.put(target.getId(), describeBeans(mapper, target, beanFilterPredicate, showUnsanitized));
|
||||
contexts.put(target.getId(), describeBeans(target, beanFilterPredicate, showUnsanitized));
|
||||
target = target.getParent();
|
||||
}
|
||||
return new ConfigurationPropertiesDescriptor(contexts);
|
||||
}
|
||||
|
||||
private ObjectMapper getObjectMapper() {
|
||||
if (this.objectMapper == null) {
|
||||
JsonMapper.Builder builder = JsonMapper.builder();
|
||||
configureJsonMapper(builder);
|
||||
this.objectMapper = builder.build();
|
||||
@SuppressWarnings("removal")
|
||||
private static BeanSerializer getBeanSerializer() {
|
||||
ClassLoader classLoader = ConfigurationPropertiesReportEndpoint.class.getClassLoader();
|
||||
if (ClassUtils.isPresent("tools.jackson.databind.json.JsonMapper", classLoader)) {
|
||||
return new JacksonBeanSerializer();
|
||||
}
|
||||
return this.objectMapper;
|
||||
if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)) {
|
||||
return new Jackson2BeanSerializer();
|
||||
}
|
||||
return (bean) -> {
|
||||
throw new IllegalStateException("Jackson is required for the 'configprops' endpoint");
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.changeDefaultPropertyInclusion((value) -> value.withValueInclusion(Include.NON_NULL));
|
||||
builder.accessorNaming(new DefaultAccessorNamingStrategy.Provider().withFirstCharAcceptance(true, false));
|
||||
applyConfigurationPropertiesFilter(builder);
|
||||
applySerializationModifier(builder);
|
||||
builder.addModule(new ConfigurationPropertiesModule());
|
||||
}
|
||||
|
||||
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 ContextConfigurationPropertiesDescriptor describeBeans(ObjectMapper mapper, ApplicationContext context,
|
||||
private ContextConfigurationPropertiesDescriptor describeBeans(ApplicationContext context,
|
||||
Predicate<ConfigurationPropertiesBean> beanFilterPredicate, boolean showUnsanitized) {
|
||||
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, showUnsanitized)));
|
||||
(bean) -> describeBean(bean, showUnsanitized)));
|
||||
return new ContextConfigurationPropertiesDescriptor(descriptors,
|
||||
(context.getParent() != null) ? context.getParent().getId() : null);
|
||||
}
|
||||
|
||||
private ConfigurationPropertiesBeanDescriptor describeBean(ObjectMapper mapper, ConfigurationPropertiesBean bean,
|
||||
private ConfigurationPropertiesBeanDescriptor describeBean(ConfigurationPropertiesBean bean,
|
||||
boolean showUnsanitized) {
|
||||
String prefix = bean.getAnnotation().prefix();
|
||||
Map<String, @Nullable Object> serialized = safeSerialize(mapper, bean.getInstance(), prefix);
|
||||
Map<String, @Nullable Object> serialized = safeSerialize(bean.getInstance(), prefix);
|
||||
Map<String, @Nullable Object> properties = sanitize(prefix, serialized, showUnsanitized);
|
||||
Map<String, Object> inputs = getInputs(prefix, serialized, showUnsanitized);
|
||||
return new ConfigurationPropertiesBeanDescriptor(prefix, properties, inputs);
|
||||
@@ -227,15 +162,13 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
|
||||
/**
|
||||
* 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, @Nullable Object> safeSerialize(ObjectMapper mapper, @Nullable Object bean, String prefix) {
|
||||
private Map<String, @Nullable Object> safeSerialize(@Nullable Object bean, String prefix) {
|
||||
try {
|
||||
return new HashMap<>(mapper.convertValue(bean, Map.class));
|
||||
return new HashMap<>(this.serializer.serialize(bean));
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return new HashMap<>(Collections.singletonMap("error", "Cannot serialize '" + prefix + "'"));
|
||||
@@ -401,173 +334,6 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
|
||||
return (prefix.isEmpty() ? prefix : prefix + ".") + key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension to {@link JacksonAnnotationIntrospector} to suppress CGLIB generated bean
|
||||
* properties.
|
||||
*/
|
||||
private static final class ConfigurationPropertiesAnnotationIntrospector extends JacksonAnnotationIntrospector {
|
||||
|
||||
@Override
|
||||
public Object findFilterId(MapperConfig<?> config, Annotated a) {
|
||||
Object id = super.findFilterId(config, 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 final 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 serializeAsProperty(Object pojo, JsonGenerator jgen, SerializationContext context,
|
||||
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.serializeAsProperty(pojo, jgen, context, writer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link SimpleModule} for configuring the serializer.
|
||||
*/
|
||||
private static final class ConfigurationPropertiesModule extends SimpleModule {
|
||||
|
||||
private ConfigurationPropertiesModule() {
|
||||
addSerializer(DataSize.class, ToStringSerializer.instance);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ValueSerializerModifier} to return only relevant configuration properties.
|
||||
*/
|
||||
protected static class GenericSerializerModifier extends ValueSerializerModifier {
|
||||
|
||||
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
|
||||
|
||||
@Override
|
||||
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription.Supplier beanDesc,
|
||||
List<BeanPropertyWriter> beanProperties) {
|
||||
List<BeanPropertyWriter> result = new ArrayList<>();
|
||||
Class<?> beanClass = beanDesc.getType().getRawClass();
|
||||
Bindable<?> bindable = Bindable.of(ClassUtils.getUserClass(beanClass));
|
||||
Constructor<?> bindConstructor = BindConstructorProvider.DEFAULT.getBindConstructor(bindable, false);
|
||||
for (BeanPropertyWriter writer : beanProperties) {
|
||||
if (isCandidate(beanDesc, writer, bindConstructor)) {
|
||||
result.add(writer);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isCandidate(BeanDescription.Supplier beanDesc, BeanPropertyWriter writer,
|
||||
@Nullable Constructor<?> constructor) {
|
||||
if (constructor != null) {
|
||||
Parameter[] parameters = constructor.getParameters();
|
||||
@Nullable String @Nullable [] 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 != null && name.equals(writer.getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isReadable(beanDesc, writer);
|
||||
}
|
||||
|
||||
private boolean isReadable(BeanDescription.Supplier beanDesc, BeanPropertyWriter writer) {
|
||||
Class<?> parentType = beanDesc.get().getType().getRawClass();
|
||||
Class<?> type = writer.getType().getRawClass();
|
||||
AnnotatedMethod setter = findSetter(beanDesc.get(), 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 @Nullable 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Description of an application's
|
||||
* {@link ConfigurationProperties @ConfigurationProperties} beans.
|
||||
|
||||
+284
@@ -0,0 +1,284 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.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.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.module.SimpleModule;
|
||||
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.databind.ser.std.ToStringSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.bind.BindConstructorProvider;
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Name;
|
||||
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.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
/**
|
||||
* {@link BeanSerializer} backed by Jackson 2.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
|
||||
*/
|
||||
@Deprecated(since = "4.0.0", forRemoval = true)
|
||||
class Jackson2BeanSerializer implements BeanSerializer {
|
||||
|
||||
private static final String CONFIGURATION_PROPERTIES_FILTER_ID = "configurationPropertiesFilter";
|
||||
|
||||
private final ObjectMapper mapper;
|
||||
|
||||
Jackson2BeanSerializer() {
|
||||
JsonMapper.Builder builder = JsonMapper.builder();
|
||||
configureMapper(builder);
|
||||
this.mapper = builder.build();
|
||||
}
|
||||
|
||||
private void configureMapper(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);
|
||||
builder.configure(MapperFeature.USE_STD_BEAN_NAMING, true);
|
||||
builder.serializationInclusion(Include.NON_NULL);
|
||||
applyConfigurationPropertiesFilter(builder);
|
||||
applySerializationModifier(builder);
|
||||
if (ClassUtils.isPresent("com.fasterxml.jackson.datatype.jsr310.JavaTimeModule",
|
||||
builder.getClass().getClassLoader())) {
|
||||
builder.addModule(new JavaTimeModule());
|
||||
}
|
||||
builder.addModule(new ConfigurationPropertiesModule());
|
||||
}
|
||||
|
||||
private void applyConfigurationPropertiesFilter(JsonMapper.Builder builder) {
|
||||
builder.annotationIntrospector(new ConfigurationPropertiesAnnotationIntrospector());
|
||||
ConfigurationPropertiesPropertyFilter filter = new ConfigurationPropertiesPropertyFilter();
|
||||
builder.filterProvider(new SimpleFilterProvider().setDefaultFilter(filter));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, @Nullable Object> serialize(@Nullable Object bean) {
|
||||
return this.mapper.convertValue(bean, Map.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension to {@link JacksonAnnotationIntrospector} to suppress CGLIB generated bean
|
||||
* properties.
|
||||
*/
|
||||
private static final class ConfigurationPropertiesAnnotationIntrospector extends JacksonAnnotationIntrospector {
|
||||
|
||||
@Override
|
||||
public Object findFilterId(Annotated a) {
|
||||
Object id = super.findFilterId(a);
|
||||
return (id != null) ? id : CONFIGURATION_PROPERTIES_FILTER_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 final 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 SimpleModule} for configuring the serializer.
|
||||
*/
|
||||
private static final class ConfigurationPropertiesModule extends SimpleModule {
|
||||
|
||||
private ConfigurationPropertiesModule() {
|
||||
addSerializer(DataSize.class, ToStringSerializer.instance);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link BeanSerializerModifier} to return only relevant configuration properties.
|
||||
*/
|
||||
protected static class GenericSerializerModifier extends BeanSerializerModifier {
|
||||
|
||||
private static final ParameterNameDiscoverer parameterNameDiscoverer = 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 = BindConstructorProvider.DEFAULT.getBindConstructor(bindable, false);
|
||||
for (BeanPropertyWriter writer : beanProperties) {
|
||||
if (isCandidate(beanDesc, writer, bindConstructor)) {
|
||||
result.add(writer);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isCandidate(BeanDescription beanDesc, BeanPropertyWriter writer,
|
||||
@Nullable Constructor<?> constructor) {
|
||||
if (constructor != null) {
|
||||
Parameter[] parameters = constructor.getParameters();
|
||||
@Nullable String @Nullable [] names = parameterNameDiscoverer.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 @Nullable 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude.Include;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import tools.jackson.core.JsonGenerator;
|
||||
import tools.jackson.databind.BeanDescription;
|
||||
import tools.jackson.databind.SerializationConfig;
|
||||
import tools.jackson.databind.SerializationContext;
|
||||
import tools.jackson.databind.SerializationFeature;
|
||||
import tools.jackson.databind.cfg.MapperConfig;
|
||||
import tools.jackson.databind.introspect.Annotated;
|
||||
import tools.jackson.databind.introspect.AnnotatedMethod;
|
||||
import tools.jackson.databind.introspect.DefaultAccessorNamingStrategy;
|
||||
import tools.jackson.databind.introspect.JacksonAnnotationIntrospector;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
import tools.jackson.databind.module.SimpleModule;
|
||||
import tools.jackson.databind.ser.BeanPropertyWriter;
|
||||
import tools.jackson.databind.ser.BeanSerializerFactory;
|
||||
import tools.jackson.databind.ser.PropertyWriter;
|
||||
import tools.jackson.databind.ser.SerializerFactory;
|
||||
import tools.jackson.databind.ser.ValueSerializerModifier;
|
||||
import tools.jackson.databind.ser.std.SimpleBeanPropertyFilter;
|
||||
import tools.jackson.databind.ser.std.SimpleFilterProvider;
|
||||
import tools.jackson.databind.ser.std.ToStringSerializer;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.boot.context.properties.bind.BindConstructorProvider;
|
||||
import org.springframework.boot.context.properties.bind.Bindable;
|
||||
import org.springframework.boot.context.properties.bind.Name;
|
||||
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.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
|
||||
/**
|
||||
* {@link BeanSerializer} backed by Jackson.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class JacksonBeanSerializer implements BeanSerializer {
|
||||
|
||||
private static final String CONFIGURATION_PROPERTIES_FILTER_ID = "configurationPropertiesFilter";
|
||||
|
||||
private final JsonMapper mapper;
|
||||
|
||||
JacksonBeanSerializer() {
|
||||
JsonMapper.Builder builder = JsonMapper.builder();
|
||||
configureMapper(builder);
|
||||
this.mapper = builder.build();
|
||||
}
|
||||
|
||||
private void configureMapper(JsonMapper.Builder builder) {
|
||||
builder.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
|
||||
builder.changeDefaultPropertyInclusion((value) -> value.withValueInclusion(Include.NON_NULL));
|
||||
builder.accessorNaming(new DefaultAccessorNamingStrategy.Provider().withFirstCharAcceptance(true, false));
|
||||
applyConfigurationPropertiesFilter(builder);
|
||||
applySerializationModifier(builder);
|
||||
builder.addModule(new ConfigurationPropertiesModule());
|
||||
}
|
||||
|
||||
private void applyConfigurationPropertiesFilter(JsonMapper.Builder builder) {
|
||||
builder.annotationIntrospector(new ConfigurationPropertiesAnnotationIntrospector());
|
||||
ConfigurationPropertiesPropertyFilter filter = new ConfigurationPropertiesPropertyFilter();
|
||||
builder.filterProvider(new SimpleFilterProvider().setDefaultFilter(filter));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public Map<String, @Nullable Object> serialize(@Nullable Object bean) {
|
||||
return this.mapper.convertValue(bean, Map.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension to {@link JacksonAnnotationIntrospector} to suppress CGLIB generated bean
|
||||
* properties.
|
||||
*/
|
||||
private static final class ConfigurationPropertiesAnnotationIntrospector extends JacksonAnnotationIntrospector {
|
||||
|
||||
@Override
|
||||
public Object findFilterId(MapperConfig<?> config, Annotated a) {
|
||||
Object id = super.findFilterId(config, a);
|
||||
return (id != null) ? id : CONFIGURATION_PROPERTIES_FILTER_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 final 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 serializeAsProperty(Object pojo, JsonGenerator jgen, SerializationContext context,
|
||||
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.serializeAsProperty(pojo, jgen, context, writer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link SimpleModule} for configuring the serializer.
|
||||
*/
|
||||
private static final class ConfigurationPropertiesModule extends SimpleModule {
|
||||
|
||||
private ConfigurationPropertiesModule() {
|
||||
addSerializer(DataSize.class, ToStringSerializer.instance);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ValueSerializerModifier} to return only relevant configuration properties.
|
||||
*/
|
||||
protected static class GenericSerializerModifier extends ValueSerializerModifier {
|
||||
|
||||
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
||||
|
||||
@Override
|
||||
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription.Supplier beanDesc,
|
||||
List<BeanPropertyWriter> beanProperties) {
|
||||
List<BeanPropertyWriter> result = new ArrayList<>();
|
||||
Class<?> beanClass = beanDesc.getType().getRawClass();
|
||||
Bindable<?> bindable = Bindable.of(ClassUtils.getUserClass(beanClass));
|
||||
Constructor<?> bindConstructor = BindConstructorProvider.DEFAULT.getBindConstructor(bindable, false);
|
||||
for (BeanPropertyWriter writer : beanProperties) {
|
||||
if (isCandidate(beanDesc, writer, bindConstructor)) {
|
||||
result.add(writer);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private boolean isCandidate(BeanDescription.Supplier beanDesc, BeanPropertyWriter writer,
|
||||
@Nullable Constructor<?> constructor) {
|
||||
if (constructor != null) {
|
||||
Parameter[] parameters = constructor.getParameters();
|
||||
@Nullable String @Nullable [] names = parameterNameDiscoverer.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 != null && name.equals(writer.getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isReadable(beanDesc, writer);
|
||||
}
|
||||
|
||||
private boolean isReadable(BeanDescription.Supplier beanDesc, BeanPropertyWriter writer) {
|
||||
Class<?> parentType = beanDesc.get().getType().getRawClass();
|
||||
Class<?> type = writer.getType().getRawClass();
|
||||
AnnotatedMethod setter = findSetter(beanDesc.get(), 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 @Nullable 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.endpoint.jackson;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
|
||||
|
||||
/**
|
||||
* Interface used to supply the Jackson 2 {@link ObjectMapper} that should be used when
|
||||
* serializing endpoint results.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 4.0.0
|
||||
* @see OperationResponseBody
|
||||
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
|
||||
*/
|
||||
@FunctionalInterface
|
||||
@Deprecated(since = "4.0.0", forRemoval = true)
|
||||
public interface EndpointJackson2ObjectMapper {
|
||||
|
||||
/**
|
||||
* Return the {@link JsonMapper} that should be used to serialize
|
||||
* {@link OperationResponseBody} endpoint results.
|
||||
* @return the object mapper
|
||||
*/
|
||||
ObjectMapper get();
|
||||
|
||||
}
|
||||
@@ -39,6 +39,7 @@ dependencies {
|
||||
optional(project(":module:spring-boot-micrometer-observation"))
|
||||
optional(project(":module:spring-boot-jackson"))
|
||||
optional(project(":module:spring-boot-validation"))
|
||||
optional("com.fasterxml.jackson.core:jackson-databind")
|
||||
optional("org.springframework.security:spring-security-core")
|
||||
|
||||
testFixturesApi(testFixtures(project(":module:spring-boot-actuator")))
|
||||
|
||||
+67
-12
@@ -86,6 +86,9 @@ import org.springframework.web.reactive.DispatcherHandler;
|
||||
@EnableConfigurationProperties(CorsEndpointProperties.class)
|
||||
public class WebFluxEndpointManagementContextConfiguration {
|
||||
|
||||
private static final List<MediaType> MEDIA_TYPES = Collections
|
||||
.unmodifiableList(Arrays.asList(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")));
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@SuppressWarnings("removal")
|
||||
@@ -148,20 +151,26 @@ public class WebFluxEndpointManagementContextConfiguration {
|
||||
SingletonSupplier.of(endpointJsonMapper::getObject));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("removal")
|
||||
@ConditionalOnBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static ServerCodecConfigurerEndpointJackson2JsonMapperBeanPostProcessor serverCodecConfigurerEndpointJackson2JsonMapperBeanPostProcessor(
|
||||
ObjectProvider<org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper> endpointJsonMapper) {
|
||||
return new ServerCodecConfigurerEndpointJackson2JsonMapperBeanPostProcessor(
|
||||
SingletonSupplier.of(endpointJsonMapper::getObject));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link BeanPostProcessor} to apply {@link EndpointJsonMapper} for
|
||||
* {@link OperationResponseBody} to
|
||||
* {@link org.springframework.http.codec.json.Jackson2JsonEncoder} instances.
|
||||
* {@link OperationResponseBody} to {@link JacksonJsonEncoder} instances.
|
||||
*/
|
||||
static class ServerCodecConfigurerEndpointJsonMapperBeanPostProcessor implements BeanPostProcessor {
|
||||
|
||||
private static final List<MediaType> MEDIA_TYPES = Collections
|
||||
.unmodifiableList(Arrays.asList(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")));
|
||||
private final Supplier<EndpointJsonMapper> mapper;
|
||||
|
||||
private final Supplier<EndpointJsonMapper> endpointJsonMapper;
|
||||
|
||||
ServerCodecConfigurerEndpointJsonMapperBeanPostProcessor(Supplier<EndpointJsonMapper> endpointJsonMapper) {
|
||||
this.endpointJsonMapper = endpointJsonMapper;
|
||||
ServerCodecConfigurerEndpointJsonMapperBeanPostProcessor(Supplier<EndpointJsonMapper> mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -181,10 +190,56 @@ public class WebFluxEndpointManagementContextConfiguration {
|
||||
}
|
||||
|
||||
private void process(Encoder<?> encoder) {
|
||||
if (encoder instanceof JacksonJsonEncoder jacksonJsonEncoder) {
|
||||
jacksonJsonEncoder.registerMappersForType(OperationResponseBody.class, (associations) -> {
|
||||
JsonMapper jsonMapper = this.endpointJsonMapper.get().get();
|
||||
MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, jsonMapper));
|
||||
if (encoder instanceof JacksonJsonEncoder jacksonEncoder) {
|
||||
jacksonEncoder.registerMappersForType(OperationResponseBody.class, (associations) -> {
|
||||
JsonMapper mapper = this.mapper.get().get();
|
||||
MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, mapper));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link BeanPostProcessor} to apply
|
||||
* {@link org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper}
|
||||
* for {@link OperationResponseBody} to
|
||||
* {@link org.springframework.http.codec.json.Jackson2JsonEncoder} instances.
|
||||
*
|
||||
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
|
||||
*/
|
||||
@Deprecated(since = "4.0.0", forRemoval = true)
|
||||
@SuppressWarnings("removal")
|
||||
static class ServerCodecConfigurerEndpointJackson2JsonMapperBeanPostProcessor implements BeanPostProcessor {
|
||||
|
||||
private final Supplier<org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper> mapper;
|
||||
|
||||
ServerCodecConfigurerEndpointJackson2JsonMapperBeanPostProcessor(
|
||||
Supplier<org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper> mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
|
||||
if (bean instanceof ServerCodecConfigurer serverCodecConfigurer) {
|
||||
process(serverCodecConfigurer);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
private void process(ServerCodecConfigurer configurer) {
|
||||
for (HttpMessageWriter<?> writer : configurer.getWriters()) {
|
||||
if (writer instanceof EncoderHttpMessageWriter<?> encoderHttpMessageWriter) {
|
||||
process((encoderHttpMessageWriter).getEncoder());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void process(Encoder<?> encoder) {
|
||||
if (encoder instanceof org.springframework.http.codec.json.Jackson2JsonEncoder jacksonEncoder) {
|
||||
jacksonEncoder.registerObjectMappersForType(OperationResponseBody.class, (associations) -> {
|
||||
com.fasterxml.jackson.databind.ObjectMapper mapper = this.mapper.get().get();
|
||||
MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, mapper));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ dependencies {
|
||||
optional(project(":module:spring-boot-tomcat"))
|
||||
optional(project(":module:spring-boot-validation"))
|
||||
optional(project(":module:spring-boot-web-server"))
|
||||
optional("com.fasterxml.jackson.core:jackson-databind")
|
||||
optional("io.projectreactor:reactor-core")
|
||||
|
||||
testFixturesApi(testFixtures(project(":module:spring-boot-actuator")))
|
||||
|
||||
+56
-9
@@ -78,6 +78,9 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
@EnableConfigurationProperties(CorsEndpointProperties.class)
|
||||
public class WebMvcEndpointManagementContextConfiguration {
|
||||
|
||||
private static final List<MediaType> MEDIA_TYPES = Collections
|
||||
.unmodifiableList(Arrays.asList(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")));
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@SuppressWarnings("removal")
|
||||
@@ -157,33 +160,77 @@ public class WebMvcEndpointManagementContextConfiguration {
|
||||
return new EndpointJsonMapperWebMvcConfigurer(endpointJsonMapper);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("removal")
|
||||
@ConditionalOnBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class)
|
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
|
||||
static EndpointJackson2ObjectMapperWebMvcConfigurer endpointJackson2ObjectMapperWebMvcConfigurer(
|
||||
org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper endpointJsonMapper) {
|
||||
return new EndpointJackson2ObjectMapperWebMvcConfigurer(endpointJsonMapper);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link WebMvcConfigurer} to apply {@link EndpointJsonMapper} for
|
||||
* {@link OperationResponseBody} to {@link JacksonJsonHttpMessageConverter} instances.
|
||||
*/
|
||||
static class EndpointJsonMapperWebMvcConfigurer implements WebMvcConfigurer {
|
||||
|
||||
private static final List<MediaType> MEDIA_TYPES = Collections
|
||||
.unmodifiableList(Arrays.asList(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")));
|
||||
private final EndpointJsonMapper mapper;
|
||||
|
||||
private final EndpointJsonMapper endpointJsonMapper;
|
||||
|
||||
EndpointJsonMapperWebMvcConfigurer(EndpointJsonMapper endpointJsonMapper) {
|
||||
this.endpointJsonMapper = endpointJsonMapper;
|
||||
EndpointJsonMapperWebMvcConfigurer(EndpointJsonMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageConverters(ServerBuilder builder) {
|
||||
builder.configureMessageConverters((converter) -> {
|
||||
if (converter instanceof JacksonJsonHttpMessageConverter jacksonJsonHttpMessageConverter) {
|
||||
configure(jacksonJsonHttpMessageConverter);
|
||||
if (converter instanceof JacksonJsonHttpMessageConverter jacksonConverter) {
|
||||
configure(jacksonConverter);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void configure(JacksonJsonHttpMessageConverter converter) {
|
||||
converter.registerMappersForType(OperationResponseBody.class, (associations) -> {
|
||||
JsonMapper jsonMapper = this.endpointJsonMapper.get();
|
||||
JsonMapper jsonMapper = this.mapper.get();
|
||||
MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, jsonMapper));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link WebMvcConfigurer} to apply
|
||||
* {@link org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper}
|
||||
* for {@link OperationResponseBody} to
|
||||
* {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter}
|
||||
* instances.
|
||||
*
|
||||
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
|
||||
*/
|
||||
@Deprecated(since = "4.0.0", forRemoval = true)
|
||||
@SuppressWarnings("removal")
|
||||
static class EndpointJackson2ObjectMapperWebMvcConfigurer implements WebMvcConfigurer {
|
||||
|
||||
private final org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper mapper;
|
||||
|
||||
EndpointJackson2ObjectMapperWebMvcConfigurer(
|
||||
org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageConverters(ServerBuilder builder) {
|
||||
builder.configureMessageConverters((converter) -> {
|
||||
if (converter instanceof org.springframework.http.converter.json.MappingJackson2HttpMessageConverter jacksonConverter) {
|
||||
configure(jacksonConverter);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void configure(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter converter) {
|
||||
converter.registerObjectMappersForType(OperationResponseBody.class, (associations) -> {
|
||||
com.fasterxml.jackson.databind.ObjectMapper jsonMapper = this.mapper.get();
|
||||
MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, jsonMapper));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user