Skip to content

Commit f72da4c

Browse files
committed
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
1 parent 08a1d66 commit f72da4c

File tree

5 files changed

+192
-5
lines changed

5 files changed

+192
-5
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ dependencies {
4848
testCompileOnly("com.google.code.findbugs:jsr305")
4949

5050
testRuntimeOnly("ch.qos.logback:logback-classic")
51+
testRuntimeOnly("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
5152
}
5253

5354
tasks.named("compileTestJava") {

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,11 @@
2020
import com.fasterxml.jackson.databind.ObjectMapper;
2121
import com.fasterxml.jackson.databind.SerializationFeature;
2222

23-
import org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper;
2423
import org.springframework.boot.autoconfigure.AutoConfiguration;
2524
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2625
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
2726
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2827
import org.springframework.context.annotation.Bean;
29-
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
3028

3129
/**
3230
* {@link EnableAutoConfiguration Auto-configuration} for Endpoint Jackson 2 support.
@@ -36,15 +34,15 @@
3634
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3.
3735
*/
3836
@AutoConfiguration
39-
@ConditionalOnClass(ObjectMapper.class)
37+
@ConditionalOnClass({ ObjectMapper.class, org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.class })
4038
@Deprecated(since = "4.0.0", forRemoval = true)
4139
@SuppressWarnings("removal")
4240
public final class Jackson2EndpointAutoConfiguration {
4341

4442
@Bean
4543
@ConditionalOnBooleanProperty(name = "management.endpoints.jackson.isolated-object-mapper", matchIfMissing = true)
46-
EndpointJackson2ObjectMapper jackson2EndpointJsonMapper() {
47-
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json()
44+
org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper jackson2EndpointJsonMapper() {
45+
ObjectMapper objectMapper = org.springframework.http.converter.json.Jackson2ObjectMapperBuilder.json()
4846
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
4947
SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
5048
.serializationInclusion(Include.NON_NULL)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.actuate.autoconfigure.endpoint.jackson;
18+
19+
import java.time.Duration;
20+
import java.time.Instant;
21+
import java.time.format.DateTimeFormatter;
22+
import java.util.HashMap;
23+
import java.util.Map;
24+
25+
import com.fasterxml.jackson.databind.ObjectMapper;
26+
import org.junit.jupiter.api.Test;
27+
import tools.jackson.databind.json.JsonMapper;
28+
29+
import org.springframework.boot.actuate.endpoint.jackson.EndpointJsonMapper;
30+
import org.springframework.boot.autoconfigure.AutoConfigurations;
31+
import org.springframework.boot.test.context.FilteredClassLoader;
32+
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
33+
import org.springframework.context.annotation.Bean;
34+
import org.springframework.context.annotation.Configuration;
35+
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
36+
37+
import static org.assertj.core.api.Assertions.assertThat;
38+
39+
/**
40+
* Tests for {@link Jackson2EndpointAutoConfiguration}.
41+
*
42+
* @author Phillip Webb
43+
* @author Andy Wilkinson
44+
* @deprecated since 4.0.0 for removal in 4.2.0 in favor of Jackson 3
45+
*/
46+
@SuppressWarnings("removal")
47+
@Deprecated(since = "4.0.0", forRemoval = true)
48+
class Jackson2EndpointAutoConfigurationTests {
49+
50+
private final ApplicationContextRunner runner = new ApplicationContextRunner()
51+
.withConfiguration(AutoConfigurations.of(Jackson2EndpointAutoConfiguration.class));
52+
53+
@Test
54+
void endpointObjectMapperWhenNoProperty() {
55+
this.runner.run((context) -> assertThat(context)
56+
.hasSingleBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class));
57+
}
58+
59+
@Test
60+
void endpointObjectMapperWhenPropertyTrue() {
61+
this.runner.run((context) -> assertThat(context)
62+
.hasSingleBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class));
63+
}
64+
65+
@Test
66+
void endpointObjectMapperWhenPropertyFalse() {
67+
this.runner.withPropertyValues("management.endpoints.jackson.isolated-object-mapper=false")
68+
.run((context) -> assertThat(context)
69+
.doesNotHaveBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class));
70+
}
71+
72+
@Test
73+
void endpointObjectMapperWhenSpringWebIsAbsent() {
74+
this.runner.withClassLoader(new FilteredClassLoader(Jackson2ObjectMapperBuilder.class))
75+
.run((context) -> assertThat(context)
76+
.doesNotHaveBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class));
77+
}
78+
79+
@Test
80+
void endpointObjectMapperDoesNotSerializeDatesAsTimestamps() {
81+
this.runner.run((context) -> {
82+
ObjectMapper objectMapper = context
83+
.getBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class)
84+
.get();
85+
Instant now = Instant.now();
86+
String json = objectMapper.writeValueAsString(Map.of("timestamp", now));
87+
assertThat(json).contains(DateTimeFormatter.ISO_INSTANT.format(now));
88+
});
89+
}
90+
91+
@Test
92+
void endpointObjectMapperDoesNotSerializeDurationsAsTimestamps() {
93+
this.runner.run((context) -> {
94+
ObjectMapper objectMapper = context
95+
.getBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class)
96+
.get();
97+
Duration duration = Duration.ofSeconds(42);
98+
String json = objectMapper.writeValueAsString(Map.of("duration", duration));
99+
assertThat(json).contains(duration.toString());
100+
});
101+
}
102+
103+
@Test
104+
void endpointObjectMapperDoesNotSerializeNullValues() {
105+
this.runner.run((context) -> {
106+
ObjectMapper objectMapper = context
107+
.getBean(org.springframework.boot.actuate.endpoint.jackson.EndpointJackson2ObjectMapper.class)
108+
.get();
109+
HashMap<String, String> map = new HashMap<>();
110+
map.put("key", null);
111+
String json = objectMapper.writeValueAsString(map);
112+
assertThat(json).isEqualTo("{}");
113+
});
114+
}
115+
116+
@Configuration(proxyBeanMethods = false)
117+
static class TestEndpointMapperConfiguration {
118+
119+
@Bean
120+
TestEndpointJsonMapper testEndpointJsonMapper() {
121+
return new TestEndpointJsonMapper();
122+
}
123+
124+
}
125+
126+
static class TestEndpointJsonMapper implements EndpointJsonMapper {
127+
128+
@Override
129+
public JsonMapper get() {
130+
return new JsonMapper();
131+
}
132+
133+
}
134+
135+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ dependencies {
3131
testImplementation(project(":starter:spring-boot-starter-webmvc-test")) {
3232
exclude module: 'spring-boot-starter-jackson'
3333
}
34+
testImplementation(project(":test-support:spring-boot-test-support"))
3435
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2012-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package smoketest.jackson2.only;
18+
19+
import java.lang.management.ManagementFactory;
20+
import java.util.Map;
21+
22+
import javax.management.MBeanServer;
23+
import javax.management.ObjectName;
24+
25+
import org.junit.jupiter.api.Test;
26+
27+
import org.springframework.boot.test.context.SpringBootTest;
28+
import org.springframework.boot.testsupport.classpath.ClassPathExclusions;
29+
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
32+
/**
33+
* Tests for Actuator running in {@link SampleJackson2OnlyApplication} with
34+
* {@code spring-web} on the classpath.
35+
*
36+
* @author Andy Wilkinson
37+
*/
38+
@ClassPathExclusions("spring-web-*")
39+
@SpringBootTest(properties = "spring.jmx.enabled=true")
40+
class SampleJackson2OnlyWithoutSpringWebApplicationTests {
41+
42+
@Test
43+
@SuppressWarnings("unchecked")
44+
void jmxEndpointsShouldWork() throws Exception {
45+
MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
46+
Map<String, Object> result = (Map<String, Object>) mbeanServer.invoke(
47+
ObjectName.getInstance("org.springframework.boot:type=Endpoint,name=Configprops"),
48+
"configurationProperties", new Object[0], null);
49+
assertThat(result).containsOnlyKeys("contexts");
50+
}
51+
52+
}

0 commit comments

Comments
 (0)