Skip to content

Commit

Permalink
Support isolated actuator ObjectMapper
Browse files Browse the repository at this point in the history
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
  • Loading branch information
philwebb committed Nov 10, 2022
1 parent 72cbb8a commit 1f8493f
Show file tree
Hide file tree
Showing 18 changed files with 622 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2012-2022 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.databind.ObjectMapper;

import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

/**
* {@link EnableAutoConfiguration Auto-configuration} for Endpoint Jackson support.
*
* @author Phillip Webb
* @since 3.0.0
*/
@Configuration(proxyBeanMethods = false)
@AutoConfigureAfter(JacksonAutoConfiguration.class)
public class JacksonEndpointAutoConfiguration {

@Bean
@ConditionalOnProperty(name = "management.endpoints.jackson.isolated-object-mapper", matchIfMissing = true)
@ConditionalOnClass({ ObjectMapper.class, Jackson2ObjectMapperBuilder.class })
public EndpointObjectMapper endpointObjectMapper() {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().build();
return () -> objectMapper;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright 2012-2022 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.
*/

/**
* Actuator Jackson auto-configuration.
*/
package org.springframework.boot.actuate.autoconfigure.endpoint.jackson;
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
import java.util.List;
import java.util.Objects;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.ext.ContextResolver;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.model.Resource;

Expand All @@ -35,7 +39,9 @@
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.EndpointId;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
Expand All @@ -53,6 +59,7 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.autoconfigure.jersey.ResourceConfigCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
Expand Down Expand Up @@ -98,6 +105,13 @@ JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar jerseyDifferentP
return new JerseyAdditionalHealthEndpointPathsManagementResourcesRegistrar(health, healthEndpointGroups);
}

@Bean
@ConditionalOnBean(EndpointObjectMapper.class)
ResourceConfigCustomizer endpointObjectMapperResourceConfigCustomizer(EndpointObjectMapper endpointObjectMapper) {
return (config) -> config.register(new EndpointObjectMapperContextResolver(endpointObjectMapper),
ContextResolver.class);
}

private boolean shouldRegisterLinksMapping(WebEndpointProperties properties, Environment environment,
String basePath) {
return properties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath)
Expand Down Expand Up @@ -192,4 +206,24 @@ private void register(Collection<Resource> resources, ResourceConfig config) {

}

/**
* {@link ContextResolver} used to obtain the {@link ObjectMapper} that should be used
* for {@link OperationResponseBody} instances.
*/
@Priority(Priorities.USER - 100)
private static final class EndpointObjectMapperContextResolver implements ContextResolver<ObjectMapper> {

private final EndpointObjectMapper endpointObjectMapper;

private EndpointObjectMapperContextResolver(EndpointObjectMapper endpointObjectMapper) {
this.endpointObjectMapper = endpointObjectMapper;
}

@Override
public ObjectMapper getContext(Class<?> type) {
return OperationResponseBody.class.isAssignableFrom(type) ? this.endpointObjectMapper.get() : null;
}

}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2022 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.
Expand All @@ -17,9 +17,16 @@
package org.springframework.boot.actuate.autoconfigure.endpoint.web.reactive;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
Expand All @@ -28,7 +35,9 @@
import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
Expand All @@ -48,7 +57,14 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Role;
import org.springframework.core.codec.Encoder;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.DispatcherHandler;
Expand Down Expand Up @@ -114,4 +130,55 @@ public ControllerEndpointHandlerMapping controllerEndpointHandlerMapping(
corsProperties.toCorsConfiguration());
}

@Bean
@ConditionalOnBean(EndpointObjectMapper.class)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static ServerCodecConfigurerEndpointObjectMapperBeanPostProcessor serverCodecConfigurerEndpointObjectMapperBeanPostProcessor(
EndpointObjectMapper endpointObjectMapper) {
return new ServerCodecConfigurerEndpointObjectMapperBeanPostProcessor(endpointObjectMapper);
}

/**
* {@link BeanPostProcessor} to apply {@link EndpointObjectMapper} for
* {@link OperationResponseBody} to {@link Jackson2JsonEncoder} instances.
*/
private static class ServerCodecConfigurerEndpointObjectMapperBeanPostProcessor implements BeanPostProcessor {

private static final List<MediaType> MEDIA_TYPES = Collections
.unmodifiableList(Arrays.asList(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")));

private final EndpointObjectMapper endpointObjectMapper;

ServerCodecConfigurerEndpointObjectMapperBeanPostProcessor(EndpointObjectMapper endpointObjectMapper) {
this.endpointObjectMapper = endpointObjectMapper;
}

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ServerCodecConfigurer) {
process((ServerCodecConfigurer) bean);
}
return bean;
}

private void process(ServerCodecConfigurer configurer) {
for (HttpMessageWriter<?> writer : configurer.getWriters()) {
if (writer instanceof EncoderHttpMessageWriter) {
process(((EncoderHttpMessageWriter<?>) writer).getEncoder());
}
}
}

private void process(Encoder<?> encoder) {
if (encoder instanceof Jackson2JsonEncoder) {
Jackson2JsonEncoder jackson2JsonEncoder = (Jackson2JsonEncoder) encoder;
jackson2JsonEncoder.registerObjectMappersForType(OperationResponseBody.class, (associations) -> {
ObjectMapper objectMapper = this.endpointObjectMapper.get();
MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, objectMapper));
});
}
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@
package org.springframework.boot.actuate.autoconfigure.endpoint.web.servlet;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
Expand All @@ -28,7 +33,9 @@
import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
Expand All @@ -49,9 +56,14 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Role;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
* {@link ManagementContextConfiguration @ManagementContextConfiguration} for Spring MVC
Expand Down Expand Up @@ -116,4 +128,46 @@ public ControllerEndpointHandlerMapping controllerEndpointHandlerMapping(
corsProperties.toCorsConfiguration());
}

@Bean
@ConditionalOnBean(EndpointObjectMapper.class)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static EndpointObjectMapperWebMvcConfigurer endpointObjectMapperWebMvcConfigurer(
EndpointObjectMapper endpointObjectMapper) {
return new EndpointObjectMapperWebMvcConfigurer(endpointObjectMapper);
}

/**
* {@link WebMvcConfigurer} to apply {@link EndpointObjectMapper} for
* {@link OperationResponseBody} to {@link MappingJackson2HttpMessageConverter}
* instances.
*/
static class EndpointObjectMapperWebMvcConfigurer implements WebMvcConfigurer {

private static final List<MediaType> MEDIA_TYPES = Collections
.unmodifiableList(Arrays.asList(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")));

private final EndpointObjectMapper endpointObjectMapper;

EndpointObjectMapperWebMvcConfigurer(EndpointObjectMapper endpointObjectMapper) {
this.endpointObjectMapper = endpointObjectMapper;
}

@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof MappingJackson2HttpMessageConverter) {
configure((MappingJackson2HttpMessageConverter) converter);
}
}
}

private void configure(MappingJackson2HttpMessageConverter converter) {
converter.registerObjectMappersForType(OperationResponseBody.class, (associations) -> {
ObjectMapper objectMapper = this.endpointObjectMapper.get();
MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, objectMapper));
});
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@
"type": "java.lang.Boolean",
"description": "Whether to enable or disable all endpoints by default."
},
{
"name": "management.endpoints.jackson.isolated-object-mapper",
"type": "java.lang.Boolean",
"description": "Whether to use an isolated object mapper to serialize endpoint JSON.",
"defaultValue": true
},
{
"name": "management.endpoints.jmx.domain",
"defaultValue": "org.springframework.boot"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ org.springframework.boot.actuate.autoconfigure.couchbase.CouchbaseReactiveHealth
org.springframework.boot.actuate.autoconfigure.data.elasticsearch.ElasticsearchReactiveHealthContributorAutoConfiguration
org.springframework.boot.actuate.autoconfigure.elasticsearch.ElasticsearchRestHealthContributorAutoConfiguration
org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.endpoint.jackson.JacksonEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.endpoint.jmx.JmxEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration
org.springframework.boot.actuate.autoconfigure.env.EnvironmentEndpointAutoConfiguration
Expand Down
Loading

0 comments on commit 1f8493f

Please sign in to comment.