Skip to content

Commit 1f8493f

Browse files
committed
Support isolated actuator ObjectMapper
Add `OperationResponseBody` tagging interface that can be used to trigger the use of an isolated ObjectMapper specifically for actuator responses. The isolated mapper is provided by an `EndpointObjectMapper` bean which is auto-configured unless specifically disabled by the user. WebMVC, WebFlux and Jersey integrations have been updated to provide a link between the `OperationResponseBody` type and the endpoint `ObjectMapper`. See gh-20291
1 parent 72cbb8a commit 1f8493f

File tree

18 files changed

+622
-4
lines changed

18 files changed

+622
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2012-2022 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 com.fasterxml.jackson.databind.ObjectMapper;
20+
21+
import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper;
22+
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
23+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
24+
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
25+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
26+
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
27+
import org.springframework.context.annotation.Bean;
28+
import org.springframework.context.annotation.Configuration;
29+
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
30+
31+
/**
32+
* {@link EnableAutoConfiguration Auto-configuration} for Endpoint Jackson support.
33+
*
34+
* @author Phillip Webb
35+
* @since 3.0.0
36+
*/
37+
@Configuration(proxyBeanMethods = false)
38+
@AutoConfigureAfter(JacksonAutoConfiguration.class)
39+
public class JacksonEndpointAutoConfiguration {
40+
41+
@Bean
42+
@ConditionalOnProperty(name = "management.endpoints.jackson.isolated-object-mapper", matchIfMissing = true)
43+
@ConditionalOnClass({ ObjectMapper.class, Jackson2ObjectMapperBuilder.class })
44+
public EndpointObjectMapper endpointObjectMapper() {
45+
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().build();
46+
return () -> objectMapper;
47+
}
48+
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright 2012-2022 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+
/**
18+
* Actuator Jackson auto-configuration.
19+
*/
20+
package org.springframework.boot.actuate.autoconfigure.endpoint.jackson;

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

+34
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@
2323
import java.util.List;
2424
import java.util.Objects;
2525

26+
import com.fasterxml.jackson.databind.ObjectMapper;
27+
import jakarta.annotation.Priority;
28+
import jakarta.ws.rs.Priorities;
29+
import jakarta.ws.rs.ext.ContextResolver;
2630
import org.glassfish.jersey.server.ResourceConfig;
2731
import org.glassfish.jersey.server.model.Resource;
2832

@@ -35,7 +39,9 @@
3539
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
3640
import org.springframework.boot.actuate.endpoint.EndpointId;
3741
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
42+
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
3843
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
44+
import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper;
3945
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
4046
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
4147
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@@ -53,6 +59,7 @@
5359
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
5460
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
5561
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
62+
import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer;
5663
import org.springframework.context.annotation.Bean;
5764
import org.springframework.core.env.Environment;
5865
import org.springframework.util.StringUtils;
@@ -98,6 +105,13 @@ JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar jerseyDifferentP
98105
return new JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(health, healthEndpointGroups);
99106
}
100107

108+
@Bean
109+
@ConditionalOnBean(EndpointObjectMapper.class)
110+
ResourceConfigCustomizer endpointObjectMapperResourceConfigCustomizer(EndpointObjectMapper endpointObjectMapper) {
111+
return (config) -> config.register(new EndpointObjectMapperContextResolver(endpointObjectMapper),
112+
ContextResolver.class);
113+
}
114+
101115
private boolean shouldRegisterLinksMapping(WebEndpointProperties properties, Environment environment,
102116
String basePath) {
103117
return properties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath)
@@ -192,4 +206,24 @@ private void register(Collection<Resource> resources, ResourceConfig config) {
192206

193207
}
194208

209+
/**
210+
* {@link ContextResolver} used to obtain the {@link ObjectMapper} that should be used
211+
* for {@link OperationResponseBody} instances.
212+
*/
213+
@Priority(Priorities.USER - 100)
214+
private static final class EndpointObjectMapperContextResolver implements ContextResolver<ObjectMapper> {
215+
216+
private final EndpointObjectMapper endpointObjectMapper;
217+
218+
private EndpointObjectMapperContextResolver(EndpointObjectMapper endpointObjectMapper) {
219+
this.endpointObjectMapper = endpointObjectMapper;
220+
}
221+
222+
@Override
223+
public ObjectMapper getContext(Class<?> type) {
224+
return OperationResponseBody.class.isAssignableFrom(type) ? this.endpointObjectMapper.get() : null;
225+
}
226+
227+
}
228+
195229
}

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

+68-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2021 the original author or authors.
2+
* Copyright 2012-2022 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.
@@ -17,9 +17,16 @@
1717
package org.springframework.boot.actuate.autoconfigure.endpoint.web.reactive;
1818

1919
import java.util.ArrayList;
20+
import java.util.Arrays;
2021
import java.util.Collection;
22+
import java.util.Collections;
2123
import java.util.List;
2224

25+
import com.fasterxml.jackson.databind.ObjectMapper;
26+
27+
import org.springframework.beans.BeansException;
28+
import org.springframework.beans.factory.config.BeanDefinition;
29+
import org.springframework.beans.factory.config.BeanPostProcessor;
2330
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
2431
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
2532
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
@@ -28,7 +35,9 @@
2835
import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort;
2936
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
3037
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
38+
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
3139
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
40+
import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper;
3241
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
3342
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
3443
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@@ -48,7 +57,14 @@
4857
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
4958
import org.springframework.boot.context.properties.EnableConfigurationProperties;
5059
import org.springframework.context.annotation.Bean;
60+
import org.springframework.context.annotation.Role;
61+
import org.springframework.core.codec.Encoder;
5162
import org.springframework.core.env.Environment;
63+
import org.springframework.http.MediaType;
64+
import org.springframework.http.codec.EncoderHttpMessageWriter;
65+
import org.springframework.http.codec.HttpMessageWriter;
66+
import org.springframework.http.codec.ServerCodecConfigurer;
67+
import org.springframework.http.codec.json.Jackson2JsonEncoder;
5268
import org.springframework.http.server.reactive.HttpHandler;
5369
import org.springframework.util.StringUtils;
5470
import org.springframework.web.reactive.DispatcherHandler;
@@ -114,4 +130,55 @@ public ControllerEndpointHandlerMapping controllerEndpointHandlerMapping(
114130
corsProperties.toCorsConfiguration());
115131
}
116132

133+
@Bean
134+
@ConditionalOnBean(EndpointObjectMapper.class)
135+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
136+
static ServerCodecConfigurerEndpointObjectMapperBeanPostProcessor serverCodecConfigurerEndpointObjectMapperBeanPostProcessor(
137+
EndpointObjectMapper endpointObjectMapper) {
138+
return new ServerCodecConfigurerEndpointObjectMapperBeanPostProcessor(endpointObjectMapper);
139+
}
140+
141+
/**
142+
* {@link BeanPostProcessor} to apply {@link EndpointObjectMapper} for
143+
* {@link OperationResponseBody} to {@link Jackson2JsonEncoder} instances.
144+
*/
145+
private static class ServerCodecConfigurerEndpointObjectMapperBeanPostProcessor implements BeanPostProcessor {
146+
147+
private static final List<MediaType> MEDIA_TYPES = Collections
148+
.unmodifiableList(Arrays.asList(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")));
149+
150+
private final EndpointObjectMapper endpointObjectMapper;
151+
152+
ServerCodecConfigurerEndpointObjectMapperBeanPostProcessor(EndpointObjectMapper endpointObjectMapper) {
153+
this.endpointObjectMapper = endpointObjectMapper;
154+
}
155+
156+
@Override
157+
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
158+
if (bean instanceof ServerCodecConfigurer) {
159+
process((ServerCodecConfigurer) bean);
160+
}
161+
return bean;
162+
}
163+
164+
private void process(ServerCodecConfigurer configurer) {
165+
for (HttpMessageWriter<?> writer : configurer.getWriters()) {
166+
if (writer instanceof EncoderHttpMessageWriter) {
167+
process(((EncoderHttpMessageWriter<?>) writer).getEncoder());
168+
}
169+
}
170+
}
171+
172+
private void process(Encoder<?> encoder) {
173+
if (encoder instanceof Jackson2JsonEncoder) {
174+
Jackson2JsonEncoder jackson2JsonEncoder = (Jackson2JsonEncoder) encoder;
175+
jackson2JsonEncoder.registerObjectMappersForType(OperationResponseBody.class, (associations) -> {
176+
ObjectMapper objectMapper = this.endpointObjectMapper.get();
177+
MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, objectMapper));
178+
});
179+
}
180+
}
181+
182+
}
183+
117184
}

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

+54
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@
1717
package org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet;
1818

1919
import java.util.ArrayList;
20+
import java.util.Arrays;
2021
import java.util.Collection;
22+
import java.util.Collections;
2123
import java.util.List;
2224

25+
import com.fasterxml.jackson.databind.ObjectMapper;
26+
27+
import org.springframework.beans.factory.config.BeanDefinition;
2328
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
2429
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
2530
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
@@ -28,7 +33,9 @@
2833
import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort;
2934
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
3035
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
36+
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
3137
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
38+
import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper;
3239
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
3340
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
3441
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@@ -49,9 +56,14 @@
4956
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
5057
import org.springframework.boot.context.properties.EnableConfigurationProperties;
5158
import org.springframework.context.annotation.Bean;
59+
import org.springframework.context.annotation.Role;
5260
import org.springframework.core.env.Environment;
61+
import org.springframework.http.MediaType;
62+
import org.springframework.http.converter.HttpMessageConverter;
63+
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
5364
import org.springframework.util.StringUtils;
5465
import org.springframework.web.servlet.DispatcherServlet;
66+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
5567

5668
/**
5769
* {@link ManagementContextConfiguration @ManagementContextConfiguration} for Spring MVC
@@ -116,4 +128,46 @@ public ControllerEndpointHandlerMapping controllerEndpointHandlerMapping(
116128
corsProperties.toCorsConfiguration());
117129
}
118130

131+
@Bean
132+
@ConditionalOnBean(EndpointObjectMapper.class)
133+
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
134+
static EndpointObjectMapperWebMvcConfigurer endpointObjectMapperWebMvcConfigurer(
135+
EndpointObjectMapper endpointObjectMapper) {
136+
return new EndpointObjectMapperWebMvcConfigurer(endpointObjectMapper);
137+
}
138+
139+
/**
140+
* {@link WebMvcConfigurer} to apply {@link EndpointObjectMapper} for
141+
* {@link OperationResponseBody} to {@link MappingJackson2HttpMessageConverter}
142+
* instances.
143+
*/
144+
static class EndpointObjectMapperWebMvcConfigurer implements WebMvcConfigurer {
145+
146+
private static final List<MediaType> MEDIA_TYPES = Collections
147+
.unmodifiableList(Arrays.asList(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")));
148+
149+
private final EndpointObjectMapper endpointObjectMapper;
150+
151+
EndpointObjectMapperWebMvcConfigurer(EndpointObjectMapper endpointObjectMapper) {
152+
this.endpointObjectMapper = endpointObjectMapper;
153+
}
154+
155+
@Override
156+
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
157+
for (HttpMessageConverter<?> converter : converters) {
158+
if (converter instanceof MappingJackson2HttpMessageConverter) {
159+
configure((MappingJackson2HttpMessageConverter) converter);
160+
}
161+
}
162+
}
163+
164+
private void configure(MappingJackson2HttpMessageConverter converter) {
165+
converter.registerObjectMappersForType(OperationResponseBody.class, (associations) -> {
166+
ObjectMapper objectMapper = this.endpointObjectMapper.get();
167+
MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, objectMapper));
168+
});
169+
}
170+
171+
}
172+
119173
}

Diff for: spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json

+6
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@
6060
"type": "java.lang.Boolean",
6161
"description": "Whether to enable or disable all endpoints by default."
6262
},
63+
{
64+
"name": "management.endpoints.jackson.isolated-object-mapper",
65+
"type": "java.lang.Boolean",
66+
"description": "Whether to use an isolated object mapper to serialize endpoint JSON.",
67+
"defaultValue": true
68+
},
6369
{
6470
"name": "management.endpoints.jmx.domain",
6571
"defaultValue": "org.springframework.boot"

Diff for: spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ org.springframework.boot.actuate.autoconfigure.couchbase.CouchbaseReactiveHealth
1717
org.springframework.boot.actuate.autoconfigure.data.elasticsearch.ElasticsearchReactiveHealthContributorAutoConfiguration
1818
org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticsearchRestHealthContributorAutoConfiguration
1919
org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration
20+
org.springframework.boot.actuate.autoconfigure.endpoint.jackson.JacksonEndpointAutoConfiguration
2021
org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration
2122
org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration
2223
org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfiguration

0 commit comments

Comments
 (0)