Browse Source

Fix Actuator with Jackson 2 but no spring-web

Previously, if Actuator was being used in a non-web app such that
spring-web was not on the classpath, the app would fail to start
if Jackson 2 was present. This occured as the auto-configuration
for the EndpointJackson2ObjectMapper tried to use spring-web's
Jackson2ObjectMapperBuilder that was not present.

This commit updates the auto-configuration to back off when
Jackson2ObjectMapperBuilder is absent, aligning it with the
behavior of JacksonEndpointAutoConfiguration in 3.5.

Fixes gh-47788
pull/47801/head
Andy Wilkinson 3 months ago
parent
commit
f72da4c77a
  1. 1
      module/spring-boot-actuator-autoconfigure/build.gradle
  2. 8
      module/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/Jackson2EndpointAutoConfiguration.java
  3. 135
      module/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/Jackson2EndpointAutoConfigurationTests.java
  4. 1
      smoke-test/spring-boot-smoke-test-jackson2-only/build.gradle
  5. 52
      smoke-test/spring-boot-smoke-test-jackson2-only/src/test/java/smoketest/jackson2/only/SampleJackson2OnlyWithoutSpringWebApplicationTests.java

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

@ -48,6 +48,7 @@ dependencies { @@ -48,6 +48,7 @@ dependencies {
testCompileOnly("com.google.code.findbugs:jsr305")
testRuntimeOnly("ch.qos.logback:logback-classic")
testRuntimeOnly("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
}
tasks.named("compileTestJava") {

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

@ -20,13 +20,11 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; @@ -20,13 +20,11 @@ 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.
@ -36,15 +34,15 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; @@ -36,15 +34,15 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
*/
@AutoConfiguration
@ConditionalOnClass(ObjectMapper.class)
@ConditionalOnClass({ ObjectMapper.class, org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.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()
org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper jackson2EndpointJsonMapper() {
ObjectMapper objectMapper = org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.json()
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
.serializationInclusion(Include.NON_NULL)

135
module/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/jackson/Jackson2EndpointAutoConfigurationTests.java

@ -0,0 +1,135 @@ @@ -0,0 +1,135 @@
/*
* 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 java.time.Duration;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import tools.jackson.databind.json.JsonMapper;
import org.springframework.boot.actuate.endpoint.jackson.EndpointJsonMapper;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link Jackson2EndpointAutoConfiguration}.
*
* @author Phillip Webb
* @author Andy Wilkinson
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3
*/
@SuppressWarnings("removal")
@Deprecated(since = "4.0.0", forRemoval = true)
class Jackson2EndpointAutoConfigurationTests {
private final ApplicationContextRunner runner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(Jackson2EndpointAutoConfiguration.class));
@Test
void endpointObjectMapperWhenNoProperty() {
this.runner.run((context) -> assertThat(context)
.hasSingleBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class));
}
@Test
void endpointObjectMapperWhenPropertyTrue() {
this.runner.run((context) -> assertThat(context)
.hasSingleBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class));
}
@Test
void endpointObjectMapperWhenPropertyFalse() {
this.runner.withPropertyValues("management.endpoints.jackson.isolated-object-mapper=false")
.run((context) -> assertThat(context)
.doesNotHaveBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class));
}
@Test
void endpointObjectMapperWhenSpringWebIsAbsent() {
this.runner.withClassLoader(new FilteredClassLoader(Jackson2ObjectMapperBuilder.class))
.run((context) -> assertThat(context)
.doesNotHaveBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class));
}
@Test
void endpointObjectMapperDoesNotSerializeDatesAsTimestamps() {
this.runner.run((context) -> {
ObjectMapper objectMapper = context
.getBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class)
.get();
Instant now = Instant.now();
String json = objectMapper.writeValueAsString(Map.of("timestamp", now));
assertThat(json).contains(DateTimeFormatter.ISO_INSTANT.format(now));
});
}
@Test
void endpointObjectMapperDoesNotSerializeDurationsAsTimestamps() {
this.runner.run((context) -> {
ObjectMapper objectMapper = context
.getBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class)
.get();
Duration duration = Duration.ofSeconds(42);
String json = objectMapper.writeValueAsString(Map.of("duration", duration));
assertThat(json).contains(duration.toString());
});
}
@Test
void endpointObjectMapperDoesNotSerializeNullValues() {
this.runner.run((context) -> {
ObjectMapper objectMapper = context
.getBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class)
.get();
HashMap<String, String> map = new HashMap<>();
map.put("key", null);
String json = objectMapper.writeValueAsString(map);
assertThat(json).isEqualTo("{}");
});
}
@Configuration(proxyBeanMethods = false)
static class TestEndpointMapperConfiguration {
@Bean
TestEndpointJsonMapper testEndpointJsonMapper() {
return new TestEndpointJsonMapper();
}
}
static class TestEndpointJsonMapper implements EndpointJsonMapper {
@Override
public JsonMapper get() {
return new JsonMapper();
}
}
}

1
smoke-test/spring-boot-smoke-test-jackson2-only/build.gradle

@ -31,4 +31,5 @@ dependencies { @@ -31,4 +31,5 @@ dependencies {
testImplementation(project(":starter:spring-boot-starter-webmvc-test")) {
exclude module: 'spring-boot-starter-jackson'
}
testImplementation(project(":test-support:spring-boot-test-support"))
}

52
smoke-test/spring-boot-smoke-test-jackson2-only/src/test/java/smoketest/jackson2/only/SampleJackson2OnlyWithoutSpringWebApplicationTests.java

@ -0,0 +1,52 @@ @@ -0,0 +1,52 @@
/*
* 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 smoketest.jackson2.only;
import java.lang.management.ManagementFactory;
import java.util.Map;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for Actuator running in {@link SampleJackson2OnlyApplication} with
* {@code spring-web} on the classpath.
*
* @author Andy Wilkinson
*/
@ClassPathExclusions("spring-web-*")
@SpringBootTest(properties = "spring.jmx.enabled=true")
class SampleJackson2OnlyWithoutSpringWebApplicationTests {
@Test
@SuppressWarnings("unchecked")
void jmxEndpointsShouldWork() throws Exception {
MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
Map<String, Object> result = (Map<String, Object>) mbeanServer.invoke(
ObjectName.getInstance("org.springframework.boot:type=Endpoint,name=Configprops"),
"configurationProperties", new Object[0], null);
assertThat(result).containsOnlyKeys("contexts");
}
}
Loading…
Cancel
Save