Browse Source

Support actuator with Jackson 2 only

See gh-47688
pull/47694/head
Phillip Webb 2 months ago
parent
commit
183d765b4d
  1. 1
      integration-test/spring-boot-sni-integration-tests/spring-boot-sni-reactive-app/build.gradle
  2. 2
      integration-test/spring-boot-sni-integration-tests/spring-boot-sni-servlet-app/build.gradle
  3. 4
      module/spring-boot-actuator-autoconfigure/build.gradle
  4. 55
      module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/Jackson2EndpointAutoConfiguration.java
  5. 2
      module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.java
  6. 5
      module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java
  7. 1
      module/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  8. 2
      module/spring-boot-actuator/build.gradle
  9. 34
      module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/BeanSerializer.java
  10. 276
      module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java
  11. 284
      module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/Jackson2BeanSerializer.java
  12. 275
      module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/JacksonBeanSerializer.java
  13. 44
      module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jackson/EndpointJackson2ObjectMapper.java
  14. 1
      module/spring-boot-webflux/build.gradle
  15. 77
      module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/actuate/web/WebFluxEndpointManagementContextConfiguration.java
  16. 1
      module/spring-boot-webmvc/build.gradle
  17. 65
      module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/actuate/web/WebMvcEndpointManagementContextConfiguration.java

1
integration-test/spring-boot-sni-integration-tests/spring-boot-sni-reactive-app/build.gradle

@ -50,6 +50,7 @@ dependencies { @@ -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")

2
integration-test/spring-boot-sni-integration-tests/spring-boot-sni-servlet-app/build.gradle

@ -45,7 +45,7 @@ dependencies { @@ -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")

4
module/spring-boot-actuator-autoconfigure/build.gradle

@ -29,13 +29,13 @@ dependencies { @@ -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
module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/Jackson2EndpointAutoConfiguration.java

@ -0,0 +1,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;
}
}

2
module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/JacksonEndpointAutoConfiguration.java

@ -34,11 +34,11 @@ import org.springframework.context.annotation.Bean; @@ -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(

5
module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java

@ -42,6 +42,7 @@ import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpointDisco @@ -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 { @@ -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
module/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@ -5,6 +5,7 @@ org.springframework.boot.actuate.autoconfigure.condition.ConditionsReportEndpoin @@ -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

2
module/spring-boot-actuator/build.gradle

@ -27,6 +27,8 @@ description = "Spring Boot Actuator" @@ -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
module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/BeanSerializer.java

@ -0,0 +1,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);
}

276
module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/ConfigurationPropertiesReportEndpoint.java

@ -16,10 +16,7 @@ @@ -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; @@ -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; @@ -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; @@ -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 @@ -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;
}
/**
* 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);
if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)) {
return new Jackson2BeanSerializer();
}
return (bean) -> {
throw new IllegalStateException("Jackson is required for the 'configprops' endpoint");
};
}
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 @@ -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 @@ -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
module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/Jackson2BeanSerializer.java

@ -0,0 +1,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
module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/context/properties/JacksonBeanSerializer.java

@ -0,0 +1,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
module/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/jackson/EndpointJackson2ObjectMapper.java

@ -0,0 +1,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();
}

1
module/spring-boot-webflux/build.gradle

@ -39,6 +39,7 @@ dependencies { @@ -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")))

77
module/spring-boot-webflux/src/main/java/org/springframework/boot/webflux/autoconfigure/actuate/web/WebFluxEndpointManagementContextConfiguration.java

@ -86,6 +86,9 @@ import org.springframework.web.reactive.DispatcherHandler; @@ -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,72 @@ public class WebFluxEndpointManagementContextConfiguration { @@ -148,20 +151,72 @@ 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;
ServerCodecConfigurerEndpointJsonMapperBeanPostProcessor(Supplier<EndpointJsonMapper> 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 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<EndpointJsonMapper> endpointJsonMapper;
private final Supplier<org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper> mapper;
ServerCodecConfigurerEndpointJsonMapperBeanPostProcessor(Supplier<EndpointJsonMapper> endpointJsonMapper) {
this.endpointJsonMapper = endpointJsonMapper;
ServerCodecConfigurerEndpointJackson2JsonMapperBeanPostProcessor(
Supplier<org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper> mapper) {
this.mapper = mapper;
}
@Override
@ -181,10 +236,10 @@ public class WebFluxEndpointManagementContextConfiguration { @@ -181,10 +236,10 @@ 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 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));
});
}
}

1
module/spring-boot-webmvc/build.gradle

@ -43,6 +43,7 @@ dependencies { @@ -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")))

65
module/spring-boot-webmvc/src/main/java/org/springframework/boot/webmvc/autoconfigure/actuate/web/WebMvcEndpointManagementContextConfiguration.java

@ -78,6 +78,9 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -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 { @@ -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 endpointJsonMapper;
private final EndpointJsonMapper mapper;
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));
});
}

Loading…
Cancel
Save