Skip to content

Commit 97af0b2

Browse files
committed
Add actuator specific ObjectMapper
Prior to this commit, Actuator endpoints would use the application ObjectMapper instance for serializing payloads as JSON. This was problematic in several cases: * application-specific configuration would change the actuator endpoint output. * choosing a different JSON mapper implementation in the application would break completely some endpoints. Spring Boot Actuator already has a hard dependency on Jackson, and this commit uses that fact to configure a shared `ObjectMapper` instance that will be used by the Actuator infrastructure consistently, without polluting the application context. This `ObjectMapper` is used in Actuator for: * JMX endpoints * Spring MVC endpoints with an HTTP message converter * Spring WebFlux endpoints with an `Encoder` * Jersey endpoints with a `ContextResolver<ObjectMapper>` For all web endpoints, this configuration is limited to the actuator-specific media types such as `"application/vnd.spring-boot.actuator.v3+json"`. Fixes gh-12951
1 parent 420af17 commit 97af0b2

File tree

14 files changed

+373
-117
lines changed

14 files changed

+373
-117
lines changed

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@
2525
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
2626
import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper;
2727
import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor;
28+
import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider;
2829
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
2930
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3031
import org.springframework.boot.convert.ApplicationConversionService;
@@ -75,4 +76,9 @@ public CachingOperationInvokerAdvisor endpointCachingOperationInvokerAdvisor(Env
7576
return new CachingOperationInvokerAdvisor(new EndpointIdTimeToLivePropertyFunction(environment));
7677
}
7778

79+
@Bean
80+
public ActuatorJsonMapperProvider actuatorJsonMapperProvider() {
81+
return new ActuatorJsonMapperProvider();
82+
}
83+
7884
}

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,8 +20,6 @@
2020

2121
import javax.management.MBeanServer;
2222

23-
import com.fasterxml.jackson.databind.ObjectMapper;
24-
2523
import org.springframework.beans.factory.ObjectProvider;
2624
import org.springframework.boot.actuate.autoconfigure.endpoint.ExposeExcludePropertyEndpointFilter;
2725
import org.springframework.boot.actuate.endpoint.EndpointFilter;
@@ -35,6 +33,7 @@
3533
import org.springframework.boot.actuate.endpoint.jmx.JmxEndpointsSupplier;
3634
import org.springframework.boot.actuate.endpoint.jmx.JmxOperationResponseMapper;
3735
import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpointDiscoverer;
36+
import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider;
3837
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
3938
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
4039
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -85,12 +84,12 @@ public JmxEndpointDiscoverer jmxAnnotationEndpointDiscoverer(ParameterValueMappe
8584
@Bean
8685
@ConditionalOnSingleCandidate(MBeanServer.class)
8786
public JmxEndpointExporter jmxMBeanExporter(MBeanServer mBeanServer, Environment environment,
88-
ObjectProvider<ObjectMapper> objectMapper, JmxEndpointsSupplier jmxEndpointsSupplier) {
87+
ActuatorJsonMapperProvider actuatorJsonMapperProvider, JmxEndpointsSupplier jmxEndpointsSupplier) {
8988
String contextId = ObjectUtils.getIdentityHexString(this.applicationContext);
9089
EndpointObjectNameFactory objectNameFactory = new DefaultEndpointObjectNameFactory(this.properties, environment,
9190
mBeanServer, contextId);
9291
JmxOperationResponseMapper responseMapper = new JacksonJmxOperationResponseMapper(
93-
objectMapper.getIfAvailable());
92+
actuatorJsonMapperProvider.getInstance());
9493
return new JmxEndpointExporter(mBeanServer, objectNameFactory, responseMapper,
9594
jmxEndpointsSupplier.getEndpoints());
9695

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,7 +22,10 @@
2222
import java.util.List;
2323

2424
import javax.annotation.PostConstruct;
25+
import javax.ws.rs.Produces;
26+
import javax.ws.rs.ext.ContextResolver;
2527

28+
import com.fasterxml.jackson.databind.ObjectMapper;
2629
import org.glassfish.jersey.server.ResourceConfig;
2730
import org.glassfish.jersey.server.model.Resource;
2831

@@ -32,6 +35,8 @@
3235
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
3336
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
3437
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
38+
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
39+
import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider;
3540
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
3641
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
3742
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@@ -84,6 +89,12 @@ private boolean shouldRegisterLinksMapping(Environment environment, String baseP
8489
|| ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT);
8590
}
8691

92+
@Bean
93+
ResourceConfigCustomizer actuatorResourceConfigCustomizer(ActuatorJsonMapperProvider jsonMapperProvider) {
94+
return (ResourceConfig config) -> config.register(
95+
new ActuatorJsonMapperContextResolver(jsonMapperProvider.getInstance()), ContextResolver.class);
96+
}
97+
8798
/**
8899
* Register endpoints with the {@link ResourceConfig}. The
89100
* {@link ResourceConfigCustomizer} cannot be used because we don't want to apply
@@ -145,4 +156,20 @@ private void register(Collection<Resource> resources) {
145156

146157
}
147158

159+
@Produces({ ActuatorMediaType.V3_JSON, ActuatorMediaType.V2_JSON })
160+
private static final class ActuatorJsonMapperContextResolver implements ContextResolver<ObjectMapper> {
161+
162+
private final ObjectMapper objectMapper;
163+
164+
private ActuatorJsonMapperContextResolver(ObjectMapper objectMapper) {
165+
this.objectMapper = objectMapper;
166+
}
167+
168+
@Override
169+
public ObjectMapper getContext(Class<?> type) {
170+
return this.objectMapper;
171+
}
172+
173+
}
174+
148175
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,6 +26,8 @@
2626
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
2727
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
2828
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
29+
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
30+
import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider;
2931
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
3032
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
3133
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@@ -40,8 +42,13 @@
4042
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
4143
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
4244
import org.springframework.boot.context.properties.EnableConfigurationProperties;
45+
import org.springframework.boot.web.codec.CodecCustomizer;
4346
import org.springframework.context.annotation.Bean;
47+
import org.springframework.core.annotation.Order;
4448
import org.springframework.core.env.Environment;
49+
import org.springframework.http.MediaType;
50+
import org.springframework.http.codec.CodecConfigurer;
51+
import org.springframework.http.codec.json.Jackson2JsonEncoder;
4552
import org.springframework.http.server.reactive.HttpHandler;
4653
import org.springframework.util.StringUtils;
4754
import org.springframework.web.reactive.DispatcherHandler;
@@ -52,6 +59,7 @@
5259
*
5360
* @author Andy Wilkinson
5461
* @author Phillip Webb
62+
* @author Brian Clozel
5563
* @since 2.0.0
5664
*/
5765
@ManagementContextConfiguration(proxyBeanMethods = false)
@@ -93,4 +101,16 @@ public ControllerEndpointHandlerMapping controllerEndpointHandlerMapping(
93101
corsProperties.toCorsConfiguration());
94102
}
95103

104+
@Bean
105+
@Order(-1)
106+
public CodecCustomizer actuatorJsonCodec(ActuatorJsonMapperProvider actuatorJsonMapperProvider) {
107+
return (configurer) -> {
108+
MediaType v3MediaType = MediaType.parseMediaType(ActuatorMediaType.V3_JSON);
109+
MediaType v2MediaType = MediaType.parseMediaType(ActuatorMediaType.V2_JSON);
110+
CodecConfigurer.CustomCodecs customCodecs = configurer.customCodecs();
111+
customCodecs.register(
112+
new Jackson2JsonEncoder(actuatorJsonMapperProvider.getInstance(), v3MediaType, v2MediaType));
113+
};
114+
}
115+
96116
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,12 +20,16 @@
2020
import java.util.Collection;
2121
import java.util.List;
2222

23+
import com.fasterxml.jackson.databind.ObjectMapper;
24+
2325
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
2426
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
2527
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
2628
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
2729
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
2830
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
31+
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
32+
import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider;
2933
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
3034
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
3135
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@@ -42,9 +46,15 @@
4246
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
4347
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4448
import org.springframework.context.annotation.Bean;
49+
import org.springframework.context.annotation.Configuration;
50+
import org.springframework.core.Ordered;
4551
import org.springframework.core.env.Environment;
52+
import org.springframework.http.MediaType;
53+
import org.springframework.http.converter.HttpMessageConverter;
54+
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
4655
import org.springframework.util.StringUtils;
4756
import org.springframework.web.servlet.DispatcherServlet;
57+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
4858

4959
/**
5060
* {@link ManagementContextConfiguration @ManagementContextConfiguration} for Spring MVC
@@ -91,4 +101,35 @@ public ControllerEndpointHandlerMapping controllerEndpointHandlerMapping(
91101
corsProperties.toCorsConfiguration());
92102
}
93103

104+
@Configuration(proxyBeanMethods = false)
105+
public static class JsonWebMvcConfigurer implements WebMvcConfigurer, Ordered {
106+
107+
private final ActuatorJsonMapperProvider actuatorJsonMapperProvider;
108+
109+
public JsonWebMvcConfigurer(ActuatorJsonMapperProvider objectMapperFactory) {
110+
this.actuatorJsonMapperProvider = objectMapperFactory;
111+
}
112+
113+
@Override
114+
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
115+
converters.add(new ActuatorJsonHttpMessageConverter(this.actuatorJsonMapperProvider.getInstance()));
116+
}
117+
118+
// WebMvcAutoConfiguration is ordered at 0
119+
@Override
120+
public int getOrder() {
121+
return -1;
122+
}
123+
124+
}
125+
126+
static class ActuatorJsonHttpMessageConverter extends AbstractJackson2HttpMessageConverter {
127+
128+
ActuatorJsonHttpMessageConverter(ObjectMapper objectMapper) {
129+
super(objectMapper, MediaType.parseMediaType(ActuatorMediaType.V3_JSON),
130+
MediaType.parseMediaType(ActuatorMediaType.V2_JSON));
131+
}
132+
133+
}
134+
94135
}

spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,9 +16,13 @@
1616

1717
package org.springframework.boot.actuate.autoconfigure.web.servlet;
1818

19+
import java.util.List;
20+
1921
import org.springframework.beans.factory.ListableBeanFactory;
22+
import org.springframework.beans.factory.ObjectProvider;
2023
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
2124
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextType;
25+
import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider;
2226
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
2327
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
2428
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -33,11 +37,15 @@
3337
import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter;
3438
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
3539
import org.springframework.context.annotation.Bean;
40+
import org.springframework.context.annotation.Configuration;
3641
import org.springframework.core.Ordered;
42+
import org.springframework.http.converter.HttpMessageConverter;
43+
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
3744
import org.springframework.web.context.request.RequestContextListener;
3845
import org.springframework.web.filter.RequestContextFilter;
3946
import org.springframework.web.servlet.DispatcherServlet;
4047
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
48+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
4149

4250
/**
4351
* {@link ManagementContextConfiguration @ManagementContextConfiguration} for Spring MVC
@@ -108,6 +116,29 @@ RequestContextFilter requestContextFilter() {
108116
return new OrderedRequestContextFilter();
109117
}
110118

119+
/**
120+
* Since {@code WebMvcEndpointManagementContextConfiguration} is adding an
121+
* actuator-specific JSON message converter, {@code @EnableWebMvc} will not register
122+
* default converters. We need to register a JSON converter for plain
123+
* {@code "application/json"} still.
124+
* WebMvcEndpointChildContextConfigurationIntegrationTests
125+
*/
126+
@Configuration(proxyBeanMethods = false)
127+
public static class FallbackJsonConverterConfigurer implements WebMvcConfigurer {
128+
129+
private final ActuatorJsonMapperProvider actuatorJsonMapperProvider;
130+
131+
FallbackJsonConverterConfigurer(ObjectProvider<ActuatorJsonMapperProvider> objectMapperSupplier) {
132+
this.actuatorJsonMapperProvider = objectMapperSupplier.getIfAvailable(ActuatorJsonMapperProvider::new);
133+
}
134+
135+
@Override
136+
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
137+
converters.add(new MappingJackson2HttpMessageConverter(this.actuatorJsonMapperProvider.getInstance()));
138+
}
139+
140+
}
141+
111142
/**
112143
* {@link WebServerFactoryCustomizer} to add an {@link ErrorPage} so that the
113144
* {@link ManagementErrorEndpoint} can be used.

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ScheduledTasksEndpointDocumentationTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -52,7 +52,7 @@ class ScheduledTasksEndpointDocumentationTests extends MockMvcEndpointDocumentat
5252

5353
@Test
5454
void scheduledTasks() throws Exception {
55-
this.mockMvc.perform(get("/actuator/scheduledtasks")).andExpect(status().isOk())
55+
this.mockMvc.perform(get("/actuator/scheduledtasks").accept("application/json")).andExpect(status().isOk())
5656
.andDo(document("scheduled-tasks",
5757
preprocessResponse(replacePattern(
5858
Pattern.compile("org.*\\.ScheduledTasksEndpointDocumentationTests\\$TestConfiguration"),

spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfigurationTests.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
2121
import org.glassfish.jersey.server.ResourceConfig;
2222
import org.junit.jupiter.api.Test;
2323

24+
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
2425
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
2526
import org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey.JerseyWebEndpointManagementContextConfiguration.JerseyWebEndpointsResourcesRegistrar;
2627
import org.springframework.boot.actuate.autoconfigure.web.jersey.JerseySameManagementContextConfiguration;
@@ -41,8 +42,8 @@
4142
class JerseyWebEndpointManagementContextConfigurationTests {
4243

4344
private final WebApplicationContextRunner runner = new WebApplicationContextRunner()
44-
.withConfiguration(AutoConfigurations.of(WebEndpointAutoConfiguration.class,
45-
JerseyWebEndpointManagementContextConfiguration.class))
45+
.withConfiguration(AutoConfigurations.of(EndpointAutoConfiguration.class,
46+
WebEndpointAutoConfiguration.class, JerseyWebEndpointManagementContextConfiguration.class))
4647
.withBean(WebEndpointsSupplier.class, () -> Collections::emptyList);
4748

4849
@Test

0 commit comments

Comments
 (0)